From cb621c3d87300f097f821b0b011857a644908dda Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sat, 18 Oct 2025 02:36:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BC=A0=E8=AE=BA=E9=A1=B6?= =?UTF-8?q?=E4=BD=8E=E5=88=86=E5=9E=8B=E5=88=A4=E6=96=AD=E5=92=8C=E7=AC=94?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=A1=B6=E4=BD=8E=E5=88=86=E5=9E=8B=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChanLun.py | 40 ++-- config/HeikinAshi_BTC.json | 83 +++++++ strategies/HeikinAshi_BTC.py | 147 ++++++++++++ web/templates/index.html | 428 +++++++++++++++++++++++++---------- 4 files changed, 571 insertions(+), 127 deletions(-) create mode 100644 config/HeikinAshi_BTC.json create mode 100644 strategies/HeikinAshi_BTC.py diff --git a/ChanLun.py b/ChanLun.py index e760637..29ad727 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -103,16 +103,16 @@ class ChanLun(): return def check_fx(self, klc): if klc.pre and klc.next: - if klc.high > klc.pre.high and klc.high > klc.next.high: - if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0 and klc.macd > klc.macdhist: - klc.set_fx(Chan_FX_TYPE.TOP) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") - return Chan_FX_TYPE.TOP - elif klc.low < klc.pre.low and klc.low < klc.next.low: - if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0 and klc.macd < klc.macdhist: - klc.set_fx(Chan_FX_TYPE.BOTTOM) - #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") - return Chan_FX_TYPE.BOTTOM + if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: + #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0 and klc.macd > klc.macdhist: + klc.set_fx(Chan_FX_TYPE.TOP) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") + return Chan_FX_TYPE.TOP + elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high: + #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0 and klc.macd < klc.macdhist: + klc.set_fx(Chan_FX_TYPE.BOTTOM) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") + return Chan_FX_TYPE.BOTTOM return Chan_FX_TYPE.UNKNOWN def add_indicators1(self, df): fast = 12 @@ -787,13 +787,28 @@ class ChanLun(): #print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff, score) #print(klc.start_time, klc.end_time, klc.trend, price_diff, score) return klc_list + def check_top_fx(self, last_bottom, klc): + if last_bottom.high > klc.pre.low or last_bottom.high > klc.next.low: + return False + return True + def check_bottom_fx(self, last_top, klc): + if last_top.low < klc.pre.high or last_top.low < klc.next.high: + return False + return True def cal_bi_list(self, klc_list): bi_list = [] last_top = None last_bottom = None for klc in klc_list: fx = self.check_fx(klc) - + if fx == Chan_FX_TYPE.TOP: + if last_bottom: + if self.check_top_fx(last_bottom, klc) == False: + fx = Chan_FX_TYPE.UNKNOWN + if fx == Chan_FX_TYPE.BOTTOM: + if last_top: + if self.check_bottom_fx(last_top, klc) == False: + fx = Chan_FX_TYPE.UNKNOWN # Do nothing if fx == Chan_FX_TYPE.UNKNOWN: continue @@ -1115,7 +1130,6 @@ class ChanLun(): zd = max(seg.low, seg.next.low, seg.next.next.low) gg = max(seg.high, seg.next.high, seg.next.next.high) dd = min(seg.low, seg.next.low, seg.next.next.low) - ddir = Chan_ZS_DIR.UP ddir = None if last_zs: if zg < last_zs.zd: @@ -1156,7 +1170,7 @@ class ChanLun(): # SEG is not in ZS if seg.is_sure: if ((seg.low > last_zs.zg and seg.high > last_zs.zg) or (seg.high < last_zs.zd and seg.low < last_zs.zd)): - last_zs.set_end_klc(last_zs.last_bi_in.end_klc, seg.sure_time, bi_out_count, seg) + last_zs.set_end_klc(seg.pre.pre.end_bi.end_klc, seg.sure_time, bi_out_count, seg) bi_out_count = 0 #print(seg.start_bi.start_klc.start_time) first_bi_out = None diff --git a/config/HeikinAshi_BTC.json b/config/HeikinAshi_BTC.json new file mode 100644 index 0000000..0b075bc --- /dev/null +++ b/config/HeikinAshi_BTC.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "max_open_trades": 1, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "dry_run": true, + "db_url": "sqlite:///tradesv3.heikinashi_btc.sqlite", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "can_short" : true, + "timeframe" : "1m", + "process_only_new_candles" : false, + "unfilledtimeout": { + "entry": 1, + "exit": 1, + "exit_timeout_count": 5, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing":{ + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8", + "secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT" + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList", + "number_assets": 1, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + "telegram": { + "enabled": true, + "token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y", + "chat_id": "580807463" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8814, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d", + "ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg", + "CORS_origins": [], + "username": "freqtrader", + "password": "FreqTrade007" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 2 + } +} \ No newline at end of file diff --git a/strategies/HeikinAshi_BTC.py b/strategies/HeikinAshi_BTC.py new file mode 100644 index 0000000..6c4d041 --- /dev/null +++ b/strategies/HeikinAshi_BTC.py @@ -0,0 +1,147 @@ +# --- Do not remove these libs --- +from statistics import median +from freqtrade.strategy import IStrategy +from technical.util import resample_to_interval, resampled_merge +from pandas import DataFrame +import talib.abstract as ta +from technical import qtpylib + +### Now you can use logger.info('asfd') to log +# freqtrade plot-dataframe --strategy HeikinAshi_BTC --datadir user_data/data/binance -c ./user_data/Chan/HeikinAshi_BTC.json --timerange=20250309- +# freqtrade backtesting -c ./user_data/Chan/config/HeikinAshi_BTC.json --strategy HeikinAshi_BTC --strategy-path ./user_data/Chan/strategies --timerange=20251008- +# freqtrade download-data -c ./user_data/Chan/config/HeikinAshi_BTC.json -t 1m 1m 1h 1d 1M --pairs BTC/USDT:USDT --timerange=20250405- +# freqtrade download-data -c ./user_data/Chan/config/HeikinAshi_BTC.json -t 1m 1h 1d 1M --pairs BTC/USDT --timerange=20170101- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy HeikinAshi_BTC --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/HeikinAshi_BTC.json -e 200 --timerange=20250201-20250901 +# freqtrade edge -c ./user_data/Chan/config/HeikinAshi_BTC.json --strategy HeikinAshi_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 +# freqtrade plot-dataframe -c ./user_data/Chan/config/HeikinAshi_BTC.json --strategy HeikinAshi_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 + +# sudo docker compose run --rm heikinashi_btc backtesting -c ./user_data/Chan/config/HeikinAshi_BTC.json --strategy HeikinAshi_BTC --strategy-path ./user_data/Chan/strategies --timerange=20250721- +# sudo docker compose run --rm heikinashi_btc download-data -c ./user_data/Chan/config/HeikinAshi_BTC.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm heikinashi_btc trade -c ./user_data/Chan/config/HeikinAshi_BTC.json --strategy HeikinAshi_BTC --strategy-path ./user_data/Chan/strategies + +class HeikinAshi_BTC(IStrategy): + """ + 使用 Heikin Ashi 蜡烛的双均线趋势策略,支持多/空。 + + 逻辑摘要: + - 指标:ha_open/ha_close/ha_high/ha_low + ha_close 的快/慢 EMA + - 做多:ha_close > ha_open 且 ha_ema_fast > ha_ema_slow + - 做空:ha_close < ha_open 且 ha_ema_fast < ha_ema_slow + - 退出:相反信号或均线反转 + """ + + INTERFACE_VERSION: int = 3 + time1h = 1440 + can_short: bool = True + timeframe: str = "1h" + process_only_new_candles: bool = True + + # ROI 与止损可根据需要在配置中覆盖 + minimal_roi = { + "120": 0.01, + "60": 0.02, + "0": 0.03, + } + + stoploss: float = -0.30 + + use_exit_signal: bool = True + exit_profit_only: bool = False + ignore_roi_if_entry_signal: bool = False + + # 需要的历史K线数量(包含EMA等指标预热) + startup_candle_count: int = 200 + + plot_config = { + "main_plot": { + "ha_close": {"color": "orange"}, + "ha_ema_fast": {"color": "green"}, + "ha_ema_slow": {"color": "red"}, + }, + "subplots": {}, + } + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """计算 Heikin Ashi 相关指标与均线。""" + if dataframe is None or dataframe.empty: + return dataframe + dataframe_1h = resample_to_interval(dataframe, self.get_ticker_indicator() * self.time1h) + ha = qtpylib.heikinashi(dataframe_1h) + dataframe_1h["ha_open"] = ha["open"] + dataframe_1h["ha_close"] = ha["close"] + dataframe_1h["ha_high"] = ha["high"] + dataframe_1h["ha_low"] = ha["low"] + + # 对 Heikin Ashi close 做 EMA 平滑,减少噪声 + dataframe_1h["ha_ema_fast"] = ta.EMA(dataframe_1h["ha_close"], timeperiod=24) + dataframe_1h["ha_ema_slow"] = ta.EMA(dataframe_1h["ha_close"], timeperiod=52) + dataframe = resampled_merge(dataframe, dataframe_1h) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """根据 HA 趋势定义进场信号(多/空)。""" + if dataframe is None or dataframe.empty: + return dataframe + ha_close = 'resample_{}_ha_close'.format(self.get_ticker_indicator()*self.time1h) + ha_open = 'resample_{}_ha_open'.format(self.get_ticker_indicator()*self.time1h) + ha_ema_fast = 'resample_{}_ha_ema_fast'.format(self.get_ticker_indicator()*self.time1h) + ha_ema_slow = 'resample_{}_ha_ema_slow'.format(self.get_ticker_indicator()*self.time1h) + volume = 'resample_{}_volume'.format(self.get_ticker_indicator()*self.time1h) + # 做多:HA 实体向上 且 快线上穿慢线(金叉) + dataframe.loc[ + ( + (dataframe[ha_close] > dataframe[ha_open]) & + (qtpylib.crossed_above(dataframe[ha_ema_fast], dataframe[ha_ema_slow])) & + (dataframe["volume"] > 0) + ), + ["enter_long", "enter_tag"], + ] = (1, "ha_trend_long") + + # 做空:HA 实体向下 且 快线下穿慢线(死叉) + dataframe.loc[ + ( + (dataframe[ha_close] < dataframe[ha_open]) & + (qtpylib.crossed_below(dataframe[ha_ema_fast], dataframe[ha_ema_slow])) & + (dataframe["volume"] > 0) + ), + ["enter_short", "enter_tag"], + ] = (1, "ha_trend_short") + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """根据 HA 反转或均线反转定义出场信号(多/空)。""" + if dataframe is None or dataframe.empty: + return dataframe + ha_close = 'resample_{}_ha_close'.format(self.get_ticker_indicator()*self.time1h) + ha_open = 'resample_{}_ha_open'.format(self.get_ticker_indicator()*self.time1h) + ha_ema_fast = 'resample_{}_ha_ema_fast'.format(self.get_ticker_indicator()*self.time1h) + ha_ema_slow = 'resample_{}_ha_ema_slow'.format(self.get_ticker_indicator()*self.time1h) + volume = 'resample_{}_volume'.format(self.get_ticker_indicator()*self.time1h) + # 多单退出:HA 变为阴 或 快线下穿慢线(死叉) + dataframe.loc[ + ( + ( + (dataframe[ha_close] < dataframe[ha_open]) | + (qtpylib.crossed_below(dataframe[ha_ema_fast], dataframe[ha_ema_slow])) + ) + & (dataframe["volume"] > 0) + ), + ["exit_long", "exit_tag"], + ] = (1, "ha_long_exit") + + # 空单退出:HA 变为阳 或 快线上穿慢线(金叉) + dataframe.loc[ + ( + ( + (dataframe[ha_close] > dataframe[ha_open]) | + (qtpylib.crossed_above(dataframe[ha_ema_fast], dataframe[ha_ema_slow])) + ) + & (dataframe["volume"] > 0) + ), + ["exit_short", "exit_tag"], + ] = (1, "ha_short_exit") + + return dataframe + def get_ticker_indicator(self): + return int(self.timeframe[:-1]) \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html index e15a743..40f163b 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -811,8 +811,17 @@
- - + + +
@@ -1093,46 +1102,10 @@ // ======= 趋势筛选(币对) ======= let trendTable = null; - // 限长截断,减少前端长期持有历史数组导致的内存增长 + // 不在前端截断数据,保留完整历史,避免K线数量限制 function trimDataInPlace(payload, maxLen = 2000) { - const keepTail = (arr) => (Array.isArray(arr) && arr.length > maxLen) ? arr.slice(-maxLen) : arr; if (!payload || typeof payload !== 'object') return; - - // 主/次周期K线 - payload.kline_data = keepTail(payload.kline_data); - payload.element_kline_data = keepTail(payload.element_kline_data); - - // MACD - if (payload.macd) { - payload.macd.macd = keepTail(payload.macd.macd); - payload.macd.signal = keepTail(payload.macd.signal); - payload.macd.histogram = keepTail(payload.macd.histogram); - } - if (payload.element_macd) { - payload.element_macd.macd = keepTail(payload.element_macd.macd); - payload.element_macd.signal = keepTail(payload.element_macd.signal); - payload.element_macd.histogram = keepTail(payload.element_macd.histogram); - } - - // ATR - payload.atr = keepTail(payload.atr); - payload.element_atr = keepTail(payload.element_atr); - - // 分形/线段/中枢/买卖点 - payload.bi_list = keepTail(payload.bi_list); - payload.seg_list = keepTail(payload.seg_list); - payload.zs_list = keepTail(payload.zs_list); - payload.uncompleted_zs_list = keepTail(payload.uncompleted_zs_list); - - payload.element_bi_list = keepTail(payload.element_bi_list); - payload.element_seg_list = keepTail(payload.element_seg_list); - payload.element_zs_list = keepTail(payload.element_zs_list); - payload.element_uncompleted_zs_list = keepTail(payload.element_uncompleted_zs_list); - - payload.trade_points = keepTail(payload.trade_points); - payload.element_trade_points = keepTail(payload.element_trade_points); - - // 移除可能保存的旧快照,避免重复持有 + // 仅清理可能保留的旧快照引用 delete payload.original_kline_data; delete payload.original_macd; } @@ -1501,7 +1474,11 @@ chanMacdChart: null, // 新增ChanMACD图表 series: { candleSeries: null, + barSeries: null, lineSeries: null, + areaSeries: null, + baselineSeries: null, + renkoSeries: null, volumeSeries: null, atrLineSeries: null, macdLineSeries: null, @@ -1714,6 +1691,13 @@ updateChartDisplay(); }); + // 添加K线形态下拉变更事件(同步隐藏的原始K线开关并重绘) + $('#klineType').change(function() { + const type = $(this).val(); + $('#showOriginalKline').prop('checked', type === 'candlestick'); + updateChartDisplay(); + }); + // 添加笔复选框变更事件 $('#showMainBi').change(function() { updateChartDisplay(); @@ -1845,7 +1829,6 @@ setSmallerOrEqualTimeframe(mainTimeframe); } }); - function updateChartDisplay() { if (currentData) { // 保存当前的可见逻辑范围 @@ -2164,7 +2147,6 @@ candles = filterTradingHours(candles, symbolConfig); console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`); } - // 清除图表容器(释放旧 DOM 与 Canvas) const chartRoot = document.getElementById('tradingview_chart'); if (chartRoot) chartRoot.innerHTML = ''; @@ -2472,36 +2454,94 @@ chanMacdChart = LightweightCharts.createChart(chanMacdChartContainer, createChartOptions(false, 'chanmacd')); } - // 创建蜡烛图系列并设置数据 - if (showOriginalKline) { - const candleSeries = mainChart.addCandlestickSeries({ - upColor: '#dc3545', - downColor: '#28a745', - borderVisible: false, - wickUpColor: '#dc3545', - wickDownColor: '#28a745', - }); - candleSeries.setData(candles); - tvWidget.series.candleSeries = candleSeries; - } else { - // 如果不显示原始K线,只显示线图 - const lineSeries = mainChart.addLineSeries({ - color: '#2962FF', - lineWidth: 2, - crosshairMarkerVisible: true, - lastValueVisible: true, - priceLineVisible: true, - }); - - // 提取收盘价数据 - const lineData = candles.map(candle => ({ - time: candle.time, - value: candle.close - })); - - lineSeries.setData(lineData); - tvWidget.series.lineSeries = lineSeries; - } + // 创建主价格系列并设置数据(支持多种图表类型) + (function(){ + const klineType = ($('#klineType').val() || 'candlestick'); + // 先清空旧的主系列引用 + tvWidget.series.candleSeries = null; + tvWidget.series.lineSeries = null; + tvWidget.series.barSeries = null; + tvWidget.series.areaSeries = null; + tvWidget.series.baselineSeries = null; + tvWidget.series.renkoSeries = null; + tvWidget.series.heikinSeries = null; + + if (klineType === 'candlestick') { + const series = mainChart.addCandlestickSeries({ + upColor: '#dc3545', + downColor: '#28a745', + borderVisible: false, + wickUpColor: '#dc3545', + wickDownColor: '#28a745', + }); + series.setData(candles); + tvWidget.series.candleSeries = series; + } else if (klineType === 'renko') { + const series = mainChart.addCandlestickSeries({ + upColor: '#dc3545', + downColor: '#28a745', + borderVisible: false, + wickUpColor: '#dc3545', + wickDownColor: '#28a745', + }); + const bricks = buildRenkoFromCandles(candles); + series.setData(bricks); + tvWidget.series.renkoSeries = series; + } else if (klineType === 'heikin') { + const series = mainChart.addCandlestickSeries({ + upColor: '#dc3545', + downColor: '#28a745', + borderVisible: false, + wickUpColor: '#dc3545', + wickDownColor: '#28a745', + }); + const hk = buildHeikinFromCandles(candles); + series.setData(hk); + tvWidget.series.heikinSeries = series; + } else if (klineType === 'bar') { + const series = mainChart.addBarSeries({ + upColor: '#dc3545', + downColor: '#28a745', + thinBars: false + }); + series.setData(candles); + tvWidget.series.barSeries = series; + } else if (klineType === 'line') { + const series = mainChart.addLineSeries({ + color: '#2962FF', + lineWidth: 2, + crosshairMarkerVisible: true, + lastValueVisible: true, + priceLineVisible: true, + }); + const lineData = candles.map(c => ({ time: c.time, value: c.close })); + series.setData(lineData); + tvWidget.series.lineSeries = series; + } else if (klineType === 'area') { + const series = mainChart.addAreaSeries({ + topColor: 'rgba(41, 98, 255, 0.4)', + bottomColor: 'rgba(41, 98, 255, 0.0)', + lineColor: '#2962FF', + lineWidth: 2, + }); + const areaData = candles.map(c => ({ time: c.time, value: c.close })); + series.setData(areaData); + tvWidget.series.areaSeries = series; + } else if (klineType === 'baseline') { + const series = mainChart.addBaselineSeries({ + baseValue: { type: 'price', price: candles.length ? candles[candles.length - 1].close : 0 }, + topLineColor: '#26a69a', + bottomLineColor: '#ef5350', + topFillColor1: 'rgba(38, 166, 154, 0.28)', + topFillColor2: 'rgba(38, 166, 154, 0.05)', + bottomFillColor1: 'rgba(239, 83, 80, 0.28)', + bottomFillColor2: 'rgba(239, 83, 80, 0.05)' + }); + const baseData = candles.map(c => ({ time: c.time, value: c.close })); + series.setData(baseData); + tvWidget.series.baselineSeries = series; + } + })(); // 转换成交量数据 - 始终使用主K线周期数据 let volumes = []; @@ -2797,7 +2837,7 @@ console.log('✅ ChanMACD图表系列已保存到tvWidget'); // 添加ChanMACD分析标注 - // 根据主/次周期开关与各自的“显示U”独立控制 + // 根据主/次周期开关与各自的"显示U"独立控制 const cm = useElementPeriod ? (currentData.element_chan_macd || currentData.chan_macd) : currentData.chan_macd; // 默认不显示,必须用户勾选对应复选框 const allowU = useElementPeriod ? !!window.showUOnElement : !!window.showUOnMain; @@ -2918,7 +2958,6 @@ } else { console.log('⚠️ ChanMACD图表创建条件不满足'); } - // 独立于当前显示周期:计算主/次周期 SD/CD 标记(用于主图合并显示) try { const mainCmAll = currentData.chan_macd || {}; @@ -4753,15 +4792,38 @@ // 获取当前价格 - 通过param.seriesPrices获取 let priceInfo = ''; if (param.seriesPrices && param.seriesPrices.size > 0) { - // 尝试从蜡烛图系列获取价格 + // 依次从当前可能的主系列中获取价格 if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) { const price = param.seriesPrices.get(tvWidget.series.candleSeries); priceInfo = `价格: ${price.toFixed(2)}`; - } - // 如果没有蜡烛图系列价格,尝试从线图系列获取 - else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) { + } else if (tvWidget.series.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) { + const price = param.seriesPrices.get(tvWidget.series.renkoSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) { + const price = param.seriesPrices.get(tvWidget.series.heikinSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) { + const price = param.seriesPrices.get(tvWidget.series.barSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) { const price = param.seriesPrices.get(tvWidget.series.lineSeries); priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { + const price = param.seriesPrices.get(tvWidget.series.areaSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { + const price = param.seriesPrices.get(tvWidget.series.baselineSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } + // 如果没有蜡烛图系列价格,尝试从区域图系列获取 + else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { + const price = param.seriesPrices.get(tvWidget.series.areaSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } + // 如果没有蜡烛图系列价格,尝试从基线图系列获取 + else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { + const price = param.seriesPrices.get(tvWidget.series.baselineSeries); + priceInfo = `价格: ${price.toFixed(2)}`; } } @@ -5342,13 +5404,18 @@ if (combinedMarkers.length > 0) { console.log('合并设置', combinedMarkers.length, '个标记(主周期分型:', (window.mainFxMarkers || []).length, '个,小周期分型:', allElementFxMarkers.length, '个,UnitTF:', (window.unittfMarkers || []).length, '个)'); - // 尝试在K线系列上设置合并后的标记 - if (showOriginalKline && tvWidget.series.candleSeries) { - tvWidget.series.candleSeries.setMarkers(combinedMarkers); - console.log('合并标记已设置到蜡烛图系列'); - } else if (!showOriginalKline && tvWidget.series.lineSeries) { - tvWidget.series.lineSeries.setMarkers(combinedMarkers); - console.log('合并标记已设置到线图系列'); + // 根据当前主系列类型设置标记 + const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); + let targetSeries = null; + if (klineType === 'candlestick') targetSeries = tvWidget.series.candleSeries; + else if (klineType === 'renko') targetSeries = tvWidget.series.renkoSeries; + else if (klineType === 'heikin') targetSeries = tvWidget.series.heikinSeries; + else if (klineType === 'bar') targetSeries = tvWidget.series.barSeries; + else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries; + else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries; + else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries; + if (targetSeries) { + targetSeries.setMarkers(combinedMarkers); } else { console.log('未找到主数据系列,无法设置标记'); } @@ -5431,23 +5498,35 @@ if (onlyMainAndU.length > 0) { console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')'); - // 尝试在K线系列上设置标记 - if (showOriginalKline && tvWidget.series.candleSeries) { - tvWidget.series.candleSeries.setMarkers(onlyMainAndU); - console.log('主周期标记已设置到蜡烛图系列'); - } else if (!showOriginalKline && tvWidget.series.lineSeries) { - tvWidget.series.lineSeries.setMarkers(onlyMainAndU); - console.log('主周期标记已设置到线图系列'); + // 根据当前主系列类型设置标记 + const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); + let targetSeries2 = null; + if (klineType2 === 'candlestick') targetSeries2 = tvWidget.series.candleSeries; + else if (klineType2 === 'renko') targetSeries2 = tvWidget.series.renkoSeries; + else if (klineType2 === 'heikin') targetSeries2 = tvWidget.series.heikinSeries; + else if (klineType2 === 'bar') targetSeries2 = tvWidget.series.barSeries; + else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries; + else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries; + else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries; + if (targetSeries2) { + targetSeries2.setMarkers(onlyMainAndU); } else { console.log('未找到主数据系列,无法设置标记'); } } else { console.log('没有分型标记需要显示,清空图表标记'); - // 清空图表上的所有标记 - if (showOriginalKline && tvWidget.series.candleSeries) { - tvWidget.series.candleSeries.setMarkers([]); - } else if (!showOriginalKline && tvWidget.series.lineSeries) { - tvWidget.series.lineSeries.setMarkers([]); + // 清空图表上的所有主系列标记 + const klineType3 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); + let targetSeries3 = null; + if (klineType3 === 'candlestick') targetSeries3 = tvWidget.series.candleSeries; + else if (klineType3 === 'renko') targetSeries3 = tvWidget.series.renkoSeries; + else if (klineType3 === 'heikin') targetSeries3 = tvWidget.series.heikinSeries; + else if (klineType3 === 'bar') targetSeries3 = tvWidget.series.barSeries; + else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries; + else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries; + else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries; + if (targetSeries3) { + targetSeries3.setMarkers([]); } } } @@ -5590,7 +5669,6 @@ console.log('🔧 最终时间轴对齐完成'); } }, 150); - // 只有在时间输入框都为空时才设置图表默认时间范围 if (!$('#start_time').val() && !$('#end_time').val()) { setDefaultTimeRange(); @@ -5673,17 +5751,27 @@ }); } - // 更新K线数据 - if (showOriginalKline && tvWidget.series.candleSeries) { + // 更新主系列数据(根据klineType) + const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); + if (klineType === 'candlestick' && tvWidget.series.candleSeries) { tvWidget.series.candleSeries.setData(candles); - } else if (!showOriginalKline && tvWidget.series.lineSeries) { - // 提取收盘价数据 - const lineData = candles.map(candle => ({ - time: candle.time, - value: candle.close - })); - + } else if (klineType === 'renko' && tvWidget.series.renkoSeries) { + const bricks = buildRenkoFromCandles(candles); + tvWidget.series.renkoSeries.setData(bricks); + } else if (klineType === 'heikin' && tvWidget.series.heikinSeries) { + const hk = buildHeikinFromCandles(candles); + tvWidget.series.heikinSeries.setData(hk); + } else if (klineType === 'bar' && tvWidget.series.barSeries) { + tvWidget.series.barSeries.setData(candles); + } else if (klineType === 'line' && tvWidget.series.lineSeries) { + const lineData = candles.map(c => ({ time: c.time, value: c.close })); tvWidget.series.lineSeries.setData(lineData); + } else if (klineType === 'area' && tvWidget.series.areaSeries) { + const areaData = candles.map(c => ({ time: c.time, value: c.close })); + tvWidget.series.areaSeries.setData(areaData); + } else if (klineType === 'baseline' && tvWidget.series.baselineSeries) { + const baseData = candles.map(c => ({ time: c.time, value: c.close })); + tvWidget.series.baselineSeries.setData(baseData); } // 更新均线数据 @@ -6310,15 +6398,38 @@ // 获取当前价格 - 通过param.seriesPrices获取 let priceInfo = ''; if (param.seriesPrices && param.seriesPrices.size > 0) { - // 尝试从蜡烛图系列获取价格 + // 依次从当前可能的主系列中获取价格 if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) { const price = param.seriesPrices.get(tvWidget.series.candleSeries); priceInfo = `价格: ${price.toFixed(2)}`; - } - // 如果没有蜡烛图系列价格,尝试从线图系列获取 - else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) { + } else if (tvWidget.series.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) { + const price = param.seriesPrices.get(tvWidget.series.renkoSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) { + const price = param.seriesPrices.get(tvWidget.series.heikinSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) { + const price = param.seriesPrices.get(tvWidget.series.barSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) { const price = param.seriesPrices.get(tvWidget.series.lineSeries); priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { + const price = param.seriesPrices.get(tvWidget.series.areaSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { + const price = param.seriesPrices.get(tvWidget.series.baselineSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } + // 如果没有蜡烛图系列价格,尝试从区域图系列获取 + else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { + const price = param.seriesPrices.get(tvWidget.series.areaSeries); + priceInfo = `价格: ${price.toFixed(2)}`; + } + // 如果没有蜡烛图系列价格,尝试从基线图系列获取 + else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { + const price = param.seriesPrices.get(tvWidget.series.baselineSeries); + priceInfo = `价格: ${price.toFixed(2)}`; } } @@ -7068,6 +7179,10 @@ tvWidget.series = { candleSeries: null, lineSeries: null, + barSeries: null, + areaSeries: null, + baselineSeries: null, + renkoSeries: null, volumeSeries: null, atrLineSeries: null, macdLineSeries: null, @@ -7155,7 +7270,6 @@ } }); } - // 开始自动刷新 function startAutoRefresh() { // 停止已有的刷新定时器 @@ -7954,7 +8068,6 @@ } }); } - // 获取随机颜色 function getRandomColor() { const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', @@ -8476,6 +8589,93 @@ return candles; } + // 从蜡烛数据生成 Heikin-Ashi(平均K) + function buildHeikinFromCandles(candles) { + if (!Array.isArray(candles) || candles.length === 0) return []; + const result = []; + let prevHaClose = (candles[0].open + candles[0].high + candles[0].low + candles[0].close) / 4; + let prevHaOpen = (candles[0].open + candles[0].close) / 2; + const firstHaHigh = Math.max(candles[0].high, prevHaOpen, prevHaClose); + const firstHaLow = Math.min(candles[0].low, prevHaOpen, prevHaClose); + result.push({ time: candles[0].time, open: prevHaOpen, high: firstHaHigh, low: firstHaLow, close: prevHaClose }); + for (let i = 1; i < candles.length; i++) { + const c = candles[i]; + const haClose = (c.open + c.high + c.low + c.close) / 4; + const haOpen = (prevHaOpen + prevHaClose) / 2; + const haHigh = Math.max(c.high, haOpen, haClose); + const haLow = Math.min(c.low, haOpen, haClose); + result.push({ time: c.time, open: haOpen, high: haHigh, low: haLow, close: haClose }); + prevHaOpen = haOpen; + prevHaClose = haClose; + } + return result; + } + + // 计算默认砖大小(优先使用ATR的最新非零值,否则按收盘价的0.5%) + function computeDefaultBrickSize(candles) { + try { + const useElementPeriod = $('#elementPeriodKline').is(':checked') && currentData.element_kline_data && Array.isArray(currentData.element_kline_data); + const atrArr = useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr; + if (Array.isArray(atrArr) && atrArr.length > 0) { + for (let i = atrArr.length - 1; i >= 0; i--) { + const v = parseFloat(atrArr[i]); + if (!isNaN(v) && v > 0) { + return v; + } + } + } + } catch(e) {} + const last = candles && candles.length ? candles[candles.length - 1].close : 0; + const size = last * 0.005; // 默认0.5% + return size > 0 ? size : 1; + } + + // 从蜡烛数据生成Renko砖(返回伪K线数据用于CandlestickSeries显示) + function buildRenkoFromCandles(candles) { + if (!Array.isArray(candles) || candles.length === 0) return []; + + const tryBuild = (size) => { + const bricks = []; + let lastClose = candles[0].close; + for (let i = 1; i < candles.length; i++) { + // 确保同一根K内多砖的时间唯一且递增 + let nextTime = candles[i].time; + const price = candles[i].close; + let diff = price - lastClose; + // 向上生成砖 + while (diff >= size) { + const open = lastClose; + const close = lastClose + size; + bricks.push({ time: nextTime, open: open, high: close, low: open, close: close }); + lastClose = close; + diff = price - lastClose; + nextTime += 1; // 递增1秒,保证唯一 + } + // 向下生成砖 + while (diff <= -size) { + const open = lastClose; + const close = lastClose - size; + bricks.push({ time: nextTime, open: open, high: open, low: close, close: close }); + lastClose = close; + diff = price - lastClose; + nextTime += 1; // 递增1秒,保证唯一 + } + } + return bricks; + }; + + // 计算初始砖大小,若无砖则逐步缩小后重试,避免不显示 + let brickSize = computeDefaultBrickSize(candles); + let result = tryBuild(brickSize); + let attempts = 0; + while (result.length === 0 && attempts < 4) { + brickSize *= 0.5; // 缩小一半后重试 + result = tryBuild(brickSize); + attempts += 1; + } + return result; + } + // 指标统一刷新(若图表未就绪则自动重试) function refreshIndicatorsNowOrLater(retries) { try { @@ -9062,7 +9262,7 @@ }); } }); - // 识别“U 结束与新 U 开始同一根K”的边界,并显示合成标记(即使前一个U是 invalid 也显示边界) + // 识别"U 结束与新 U 开始同一根K"的边界,并显示合成标记(即使前一个U是 invalid 也显示边界) for (let i = 0; i + 1 < unittfList.length; i++) { const cur = unittfList[i]; const nxt = unittfList[i + 1];