修改缠论顶低分型判断和笔生成顶低分型判断

This commit is contained in:
jackyu66git
2025-10-18 02:36:29 +08:00
parent a1f8c6c605
commit cb621c3d87
4 changed files with 571 additions and 127 deletions
+27 -13
View File
@@ -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
+83
View File
@@ -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
}
}
+147
View File
@@ -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])
+314 -114
View File
@@ -811,8 +811,17 @@
<div class="d-flex align-items-center">
<label class="form-label me-3 mb-0">基础显示:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="showOriginalKline" checked>
<label class="form-check-label" for="showOriginalKline">原始K线</label>
<select id="klineType" class="form-select form-select-sm me-2" style="width: 120px;">
<option value="candlestick" selected>蜡烛线</option>
<option value="bar">柱条</option>
<option value="line">折线</option>
<option value="area">面积</option>
<option value="baseline">基线</option>
<option value="renko">砖型</option>
<option value="heikin">平均K</option>
</select>
<!-- 为兼容现有逻辑,保留隐藏的原始K线开关,与下拉同步 -->
<input class="form-check-input" type="checkbox" id="showOriginalKline" checked style="display:none;">
</div>
<!-- 添加K线周期切换 -->
@@ -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];