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];