From fe4193b153f3e452f647ed2471ffae79977804c7 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Wed, 29 Oct 2025 01:16:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A3=B8K=E5=BD=A2=E6=80=81?= =?UTF-8?q?=EF=BC=8C=E5=89=8D=E7=AB=AF=E6=B7=BB=E5=8A=A0KLC=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChanEnum.py | 74 +++++++++++ ChanKLU.py | 8 +- ChanLun.py | 76 ++++++++++- ChanSEG.py | 27 ++-- strategies/PatternTrader.json | 35 +++++ strategies/PatternTrader.py | 233 ++++++++++++++++++++++++++++++++++ web/app.py | 26 ++++ web/templates/index.html | 224 ++++++++++++++++++-------------- 8 files changed, 589 insertions(+), 114 deletions(-) create mode 100644 strategies/PatternTrader.json create mode 100644 strategies/PatternTrader.py diff --git a/ChanEnum.py b/ChanEnum.py index 22f0c2a..70b12e5 100644 --- a/ChanEnum.py +++ b/ChanEnum.py @@ -51,6 +51,80 @@ class Chan_KLU_TYPE(Enum): SmallBear = auto() Cross = auto() +class Chan_KLU_PATTERN(Enum): + # 单根K线形态 + HAMMER = auto() # 锤子线 + INVERTED_HAMMER = auto() # 倒锤子线 + SHOOTING_STAR = auto() # 射击之星 + HANGING_MAN = auto() # 上吊线 + DOJI = auto() # 十字星 + LONG_LEGGED_DOJI = auto() # 长腿十字星 + GRAVESTONE_DOJI = auto() # 墓碑十字星 + DRAGONFLY_DOJI = auto() # 蜻蜓十字星 + MARUBOZU = auto() # 光头光脚 + SPINNING_TOP = auto() # 纺锤线 + + # 双根K线形态 + BULLISH_ENGULFING = auto() # 看涨吞没 + BEARISH_ENGULFING = auto() # 看跌吞没 + PIERCING_LINE = auto() # 刺透形态 + DARK_CLOUD_COVER = auto() # 乌云盖顶 + TWEEZER_TOP = auto() # 镊子顶 + TWEEZER_BOTTOM = auto() # 镊子底 + HARAMI = auto() # 孕线 + BULLISH_HARAMI = auto() # 看涨孕线 + BEARISH_HARAMI = auto() # 看跌孕线 + + # 三根K线形态 + MORNING_STAR = auto() # 早晨之星 + EVENING_STAR = auto() # 黄昏之星 + THREE_WHITE_SOLDIERS = auto() # 红三兵 + THREE_BLACK_CROWS = auto() # 三只乌鸦 + THREE_INNER_UP = auto() # 上升三法 + THREE_INNER_DOWN = auto() # 下降三法 + ABANDONED_BABY = auto() # 弃婴形态 + + # 多根K线形态 + DOUBLE_TOP = auto() # 双顶 + DOUBLE_BOTTOM = auto() # 双底 + TRIPLE_TOP = auto() # 三顶 + TRIPLE_BOTTOM = auto() # 三底 + HEAD_AND_SHOULDERS = auto() # 头肩顶 + INVERSE_HEAD_SHOULDERS = auto() # 头肩底 + ROUNDING_BOTTOM = auto() # 圆弧底 + ROUNDING_TOP = auto() # 圆弧顶 + + # 缺口形态 + BREAKAWAY_GAP = auto() # 突破缺口 + RUNAWAY_GAP = auto() # 持续缺口 + EXHAUSTION_GAP = auto() # 衰竭缺口 + + # 特殊形态 + ISLAND_REVERSAL = auto() # 岛形反转 + KEY_REVERSAL = auto() # 关键反转 + INSIDE_BAR = auto() # 内包线 + OUTSIDE_BAR = auto() # 外包线 + + # 趋势形态 + HIGHER_HIGH = auto() # 更高高点 + HIGHER_LOW = auto() # 更高低点 + LOWER_HIGH = auto() # 更低高点 + LOWER_LOW = auto() # 更低低点 + + # 支撑阻力形态 + SUPPORT_BOUNCE = auto() # 支撑反弹 + RESISTANCE_REJECTION = auto() # 阻力拒绝 + BREAKOUT = auto() # 突破 + BREAKDOWN = auto() # 跌破 + + # 成交量相关形态 + VOLUME_SPIKE = auto() # 成交量激增 + VOLUME_DECLINE = auto() # 成交量萎缩 + + # 未知/无形态 + UNKNOWN = auto() # 未知形态 + + class Chan_FX_TYPE(Enum): BOTTOM = auto() TOP = auto() diff --git a/ChanKLU.py b/ChanKLU.py index 552a1a0..aa5d1a5 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -1,4 +1,4 @@ -from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND +from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN class ChanKLU: def __init__(self, time, open, high, low, close, volume): # _time, _close, _open, _high, _low, _extra_info={} @@ -21,7 +21,8 @@ class ChanKLU: self.bb52lower = 0 # === 新增:K线类型 === self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线 - + self.pattern = Chan_KLU_PATTERN.UNKNOWN + # === 新增:实时分型相关属性 === self.pre = None # 前一根K线 self.next = None # 后一根K线 @@ -67,10 +68,13 @@ class ChanKLU: #print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength) def set_macd_state(self, state): self.macd_state = state + def set_pattern(self, pattern): + self.pattern = pattern def cal_exception(self): if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5: self.exception = True #print(self.time, self.upper_shadow_ratio, self.lower_shadow_ratio, self.body, self.lower_shadow, self.upper_shadow, self.high, self.low, self.close, self.open) + self.exception = False def set_trend(self, trend): self.trend = trend def set_next(self, next): diff --git a/ChanLun.py b/ChanLun.py index b97fd4d..6c005d2 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -1,6 +1,6 @@ from datetime import timedelta from pandas import DataFrame -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_KLU_PATTERN from ChanKLU import ChanKLU from ChanKLC import ChanKLC from ChanBI import ChanBI @@ -576,7 +576,7 @@ class ChanLun(): if near_resistance_touch: # 若动量不强,则更偏空 score -= 1 if (hist is None or pre_hist is None or hist <= pre_hist) else 0 - # 3.4) 多次对 EMA52 的“拒绝”配合 MACD 逆向:易形成压/支并反向 + # 3.4) 多次对 EMA52 的"拒绝"配合 MACD 逆向:易形成压/支并反向 # 统计近窗口内的上/下拒绝次数: # - 上拒绝:价格位于 EMA52 下方,最高触及/越过 EMA52 但收盘仍在下方 # - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方 @@ -701,7 +701,7 @@ class ChanLun(): for i in range(1, len(hist_seq)): if abs(hist_seq[i]) < abs(hist_seq[i-1]): weaken_steps += 1 - # 近窗口对 EMA52 的“未能站上/跌破”统计(放宽窗口与条件) + # 近窗口对 EMA52 的"未能站上/跌破"统计(放宽窗口与条件) window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else [] no_up_break = False no_down_break = False @@ -1343,8 +1343,70 @@ class ChanLun(): return klc_list def get_klu_list(self, dataframe): - return self.get_kl_data(dataframe) - - def get_decimal(self, value): - return Decimal("{:.2f}".format(value)) + klu_list = self.get_kl_data(dataframe) + return self.cal_klu_pattern(klu_list) + def cal_klu_pattern(self, klu_list): + """ + 计算裸K的pattern - 只识别反转形态 + """ + if not klu_list or len(klu_list) < 3: + return klu_list + + for i, klu in enumerate(klu_list): + # 单根K线反转模式识别 + self._detect_single_reversal_pattern(klu) + + + # 验证形态是否成立 + if klu.pattern != Chan_KLU_PATTERN.UNKNOWN: + print(klu.time, klu.pattern) + return klu_list + + def _detect_single_reversal_pattern(self, klu): + """检测单根K线反转模式""" + body = abs(klu.close - klu.open) + upper_shadow = klu.high - max(klu.close, klu.open) + lower_shadow = min(klu.close, klu.open) - klu.low + total_range = klu.high - klu.low + + # 避免除零 + if total_range == 0: + return + + body_ratio = body / total_range + upper_ratio = upper_shadow / total_range + lower_ratio = lower_shadow / total_range + + # 锤子线/上吊线 - 反转信号 + if lower_ratio / body_ratio >= 2: + # 锤子线:底部反转,需要前一根是下跌趋势 + if klu.close > klu.open and klu.pre and klu.pre.close < klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.HAMMER) # 底部反转 + # 上吊线:顶部反转,需要前一根是上涨趋势 + elif klu.close < klu.open and klu.pre and klu.pre.close > klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.HANGING_MAN) # 顶部反转 + + # 倒锤子线/射击之星 - 反转信号 + elif upper_ratio / body_ratio >= 2: + # 倒锤子线:底部反转,需要前一根是下跌趋势 + if klu.close > klu.open and klu.pre and klu.pre.close < klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.INVERTED_HAMMER) # 底部反转 + # 射击之星:顶部反转,需要前一根是上涨趋势 + elif klu.close < klu.open and klu.pre and klu.pre.close > klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.SHOOTING_STAR) # 顶部反转 + + # 十字星 - 反转信号 + elif body_ratio <= 0.1: + if upper_ratio > 0.4 and lower_ratio > 0.4: + klu.set_pattern(Chan_KLU_PATTERN.LONG_LEGGED_DOJI) # 强烈反转信号 + elif upper_ratio > 0.4 and lower_ratio <= 0.1: + # 墓碑十字星:顶部反转,需要前一根是上涨趋势 + if klu.pre and klu.pre.close > klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.GRAVESTONE_DOJI) # 顶部反转 + elif lower_ratio > 0.4 and upper_ratio <= 0.1: + # 蜻蜓十字星:底部反转,需要前一根是下跌趋势 + if klu.pre and klu.pre.close < klu.pre.open: + klu.set_pattern(Chan_KLU_PATTERN.DRAGONFLY_DOJI) # 底部反转 + else: + klu.set_pattern(Chan_KLU_PATTERN.DOJI) # 一般反转信号 \ No newline at end of file diff --git a/ChanSEG.py b/ChanSEG.py index 4cb8e75..4412085 100644 --- a/ChanSEG.py +++ b/ChanSEG.py @@ -112,10 +112,7 @@ class ChanSEG(): last_zs = zs else: if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure: - if bi.low < last_zs.zg: - last_zs.add_bi(bi.pre) - last_zs.add_bi(bi) - else: + if bi.low > last_zs.zg or bi.high < last_zs.zd: last_zs.set_end_bi(last_zs.bi_list[-1], bi) if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: zg = min(bi.high, bi.next.high, bi.next.next.high) @@ -131,6 +128,12 @@ class ChanSEG(): zs.add_bi(bi.next.next) zs_list.append(zs) last_zs = zs + else: + last_zs.add_bi(bi.pre) + last_zs.add_bi(bi) + if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: + #print(bi.start_time, "BI", last_zs.is_sure) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1]) else: for index in range(1, len(self.bi_list)): bi = self.bi_list[index] @@ -151,10 +154,7 @@ class ChanSEG(): last_zs = zs else: if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure: - if bi.high > last_zs.zd: - last_zs.add_bi(bi.pre) - last_zs.add_bi(bi) - else: + if bi.low > last_zs.zg or bi.high < last_zs.zd: last_zs.set_end_bi(last_zs.bi_list[-1], bi) if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: zg = min(bi.high, bi.next.high, bi.next.next.high) @@ -170,8 +170,13 @@ class ChanSEG(): zs.add_bi(bi.next.next) zs_list.append(zs) last_zs = zs - if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1]) + else: + last_zs.add_bi(bi.pre) + last_zs.add_bi(bi) + if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: + #print(bi.start_time, "BI", last_zs.is_sure) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1]) + #print(self.start_time, len(zs_list)) - print(self.bi_list[-1].end_time, "end_bi") + #print(self.bi_list[-1].end_time, "end_bi") return zs_list \ No newline at end of file diff --git a/strategies/PatternTrader.json b/strategies/PatternTrader.json new file mode 100644 index 0000000..b5dc634 --- /dev/null +++ b/strategies/PatternTrader.json @@ -0,0 +1,35 @@ +{ + "strategy_name": "PatternTrader", + "params": { + "trailing": { + "trailing_stop": false, + "trailing_stop_positive": null, + "trailing_stop_positive_offset": 0.0, + "trailing_only_offset_is_reached": false + }, + "max_open_trades": { + "max_open_trades": 1 + }, + "buy": { + "fast_ma": 9, + "lev": 2.9, + "slow_ma": 31, + "time": 29 + }, + "sell": { + "exit_delay": 6 + }, + "protection": {}, + "roi": { + "0": 0.062, + "6": 0.019, + "16": 0.014, + "40": 0 + }, + "stoploss": { + "stoploss": -0.316 + } + }, + "ft_stratparam_v": 1, + "export_time": "2025-10-28 07:39:54.768566+00:00" +} \ No newline at end of file diff --git a/strategies/PatternTrader.py b/strategies/PatternTrader.py new file mode 100644 index 0000000..b229d17 --- /dev/null +++ b/strategies/PatternTrader.py @@ -0,0 +1,233 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, CategoricalParameter +from typing import Dict, List +from functools import reduce +from pandas import DataFrame +import numpy as np +import pandas as pd +# -------------------------------- + +# 设置pandas选项以避免FutureWarning +pd.set_option('future.no_silent_downcasting', True) + +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +from technical.util import resample_to_interval, resampled_merge +from freqtrade.persistence import Trade, Order +from datetime import datetime, timedelta +from typing import Optional +import logging +logger = logging.getLogger(__name__) +# freqtrade plot-dataframe --strategy PatternTrader --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309- + +# freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies --timerange=20251023- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m --pairs BTC/USDT:USDT --timerange=20250501- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss --strategy PatternTrader --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 200 --timerange=20250201-20250401 +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces buy sell roi stoploss --strategy PatternTrader --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 600 --timerange=20250201-20250401 +# sudo docker compose run --rm chan_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies --timerange=20250101- +# sudo docker compose run --rm chan_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm chan_btc trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy PatternTrader --strategy-path ./user_data/Chan/strategies + +class PatternTrader(IStrategy): + """ + 极简双均线策略 + 只使用双均线交叉作为唯一信号 + """ + + INTERFACE_VERSION: int = 3 + + # 极简参数 + fast_ma: IntParameter = IntParameter(5, 15, default=8, space='buy') # 快速均线 + slow_ma: IntParameter = IntParameter(20, 50, default=30, space='buy') # 慢速均线 + + # 添加一个简单的sell空间参数 + exit_delay: IntParameter = IntParameter(1, 10, default=3, space='sell') # 出场延迟 + + # 时间框架 + time: IntParameter = IntParameter(15, 60, default=30, space='buy') + + # ROI 超参 + roi_t1: IntParameter = IntParameter(10, 60, default=30, space='roi') + roi_t2: IntParameter = IntParameter(60, 240, default=120, space='roi') + roi_p1: DecimalParameter = DecimalParameter(0.02, 0.08, default=0.05, decimals=3, space='roi') + roi_p2: DecimalParameter = DecimalParameter(0.005, 0.03, default=0.01, decimals=3, space='roi') + + # 合约交易参数 + can_short = True + stoploss = -0.02 # 2% 止损 + + # 杠杆设置 + lev: DecimalParameter = DecimalParameter(1.0, 3.0, default=2.0, decimals=1, space='buy') + + # 运行设置 + process_only_new_candles = False + startup_candle_count: int = 100 + + # ROI 外部覆盖 + _roi_override: Optional[Dict[str, float]] = None + + @property + def minimal_roi(self) -> Dict[str, float]: + """ + 基于超参动态生成 ROI 梯度 + """ + if self._roi_override is not None: + return self._roi_override + t1 = int(self.roi_t1.value) + t2 = int(self.roi_t2.value) + times = sorted([t1, t2]) + p1 = float(self.roi_p1.value) + p2 = float(self.roi_p2.value) + profits = sorted([p1, p2], reverse=True) + return { + "0": profits[0], + str(times[0]): profits[1], + str(times[1]): 0.0, + } + + @minimal_roi.setter + def minimal_roi(self, value: Dict[str, float]) -> None: + # 允许框架在解析时覆盖 ROI 设置 + self._roi_override = value + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 计算技术指标(极简版) + 只计算双均线 + """ + res = self.get_ticker_indicator() * int(self.time.value) + dataframe_3 = resample_to_interval(dataframe, res) + + # 只计算双均线 + dataframe_3['fast_ma'] = ta.SMA(dataframe_3['close'], timeperiod=int(self.fast_ma.value)) + dataframe_3['slow_ma'] = ta.SMA(dataframe_3['close'], timeperiod=int(self.slow_ma.value)) + + # 计算金叉和死叉 + dataframe_3['fast_ma_cross_slow_ma'] = (dataframe_3['fast_ma'] > dataframe_3['slow_ma']) & (dataframe_3['fast_ma'].shift(1) <= dataframe_3['slow_ma'].shift(1)) + dataframe_3['fast_ma_cross_slow_ma_down'] = (dataframe_3['fast_ma'] < dataframe_3['slow_ma']) & (dataframe_3['fast_ma'].shift(1) >= dataframe_3['slow_ma'].shift(1)) + + dataframe = resampled_merge(dataframe, dataframe_3) + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 基于TA指标,填充进场趋势列(极简版) + 只使用双均线交叉 + """ + res = self.get_ticker_indicator() * int(self.time.value) + def _pick(df: DataFrame, name: str) -> str: + col = f"resample_{res}_{name}" + if col in df.columns: + return col + col2 = f"resample_{float(res)}_{name}" + if col2 in df.columns: + return col2 + cand = [c for c in df.columns if c.endswith(f"_{name}")] + return cand[0] if len(cand) else col + + fast_ma_cross_slow_ma_str = _pick(dataframe, 'fast_ma_cross_slow_ma') + fast_ma_cross_slow_ma_down_str = _pick(dataframe, 'fast_ma_cross_slow_ma_down') + + # 检测多头信号:快线上穿慢线 + dataframe.loc[ + ( + (dataframe[fast_ma_cross_slow_ma_str] == True) & + (pd.notna(dataframe[fast_ma_cross_slow_ma_str])) + ), + ['enter_long', 'enter_tag']] = (1, 'long_signal_simple') + + # 检测空头信号:快线下穿慢线 + dataframe.loc[ + ( + (dataframe[fast_ma_cross_slow_ma_down_str] == True) & + (pd.notna(dataframe[fast_ma_cross_slow_ma_down_str])) + ), + ['enter_short', 'enter_tag']] = (1, 'short_signal_simple') + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 基于TA指标,填充出场趋势列(极简版) + 反向交叉出场 + """ + res = self.get_ticker_indicator() * int(self.time.value) + def _pick(df: DataFrame, name: str) -> str: + col = f"resample_{res}_{name}" + if col in df.columns: + return col + col2 = f"resample_{float(res)}_{name}" + if col2 in df.columns: + return col2 + cand = [c for c in df.columns if c.endswith(f"_{name}")] + return cand[0] if len(cand) else col + + fast_ma_cross_slow_ma_str = _pick(dataframe, 'fast_ma_cross_slow_ma') + fast_ma_cross_slow_ma_down_str = _pick(dataframe, 'fast_ma_cross_slow_ma_down') + + # 做多出场:出现死叉 + dataframe.loc[ + ( + (dataframe[fast_ma_cross_slow_ma_down_str] == True) & + (pd.notna(dataframe[fast_ma_cross_slow_ma_down_str])) + ), + ['exit_long', 'exit_tag']] = (1, 'long_exit_simple') + + # 做空出场:出现金叉 + dataframe.loc[ + ( + (dataframe[fast_ma_cross_slow_ma_str] == True) & + (pd.notna(dataframe[fast_ma_cross_slow_ma_str])) + ), + ['exit_short', 'exit_tag']] = (1, 'short_exit_simple') + + return dataframe + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, + **kwargs) -> float: + """ + 杠杆设置 + """ + return float(self.lev.value) + + def get_ticker_indicator(self): + return int(self.timeframe[:-1]) + + +# 简单的测试函数 +def test_strategy(): + """ + 测试策略基本功能 + """ + try: + # 创建策略实例 + strategy = PatternTrader() + + # 检查基本属性 + print("✅ 策略实例化成功") + print(f"策略名称: {strategy.__class__.__name__}") + print(f"接口版本: {strategy.INTERFACE_VERSION}") + print(f"支持做空: {strategy.can_short}") + print(f"默认止损: {strategy.stoploss}") + + # 检查参数 + print("\n✅ 策略参数检查:") + print(f"布林带长度: {strategy.bb_length.value}") + print(f"杠杆: {strategy.lev.value}") + print(f"仓位比例: {strategy.position_size_pct.value}") + + print("\n🎉 策略测试通过!") + return True + + except Exception as e: + print(f"❌ 策略测试失败: {e}") + import traceback + traceback.print_exc() + return False + + +if __name__ == "__main__": + test_strategy() \ No newline at end of file diff --git a/web/app.py b/web/app.py index b813cf7..d63b576 100644 --- a/web/app.py +++ b/web/app.py @@ -1167,6 +1167,18 @@ def analyze(): # 添加主周期分析结果到返回数据 result.update({ 'kline_data': clean_dataframe_for_json(df).to_dict('records'), + 'klc_list': [{ + 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), + 'open': float(klc.open), + 'high': float(klc.high), + 'low': float(klc.low), + 'close': float(klc.close), + 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, + 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), + 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), + 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), + 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') + } for klc in analysis_result['klc_list'] if hasattr(klc, 'end_time') and klc.end_time], 'bi_list': [{ 'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(), 'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None, @@ -1354,6 +1366,20 @@ def analyze(): # 添加小周期K线数据 result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records') + # 添加小周期KLC列表 + result['element_klc_list'] = [{ + 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), + 'open': float(klc.open), + 'high': float(klc.high), + 'low': float(klc.low), + 'close': float(klc.close), + 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, + 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), + 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), + 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), + 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') + } for klc in element_analysis['klc_list'] if hasattr(klc, 'end_time') and klc.end_time] + result['element_seg_list'] = [{ 'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(), 'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None, diff --git a/web/templates/index.html b/web/templates/index.html index f9b71bd..0d20d13 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -648,7 +648,6 @@ align-items: center; z-index: 1000; } - .bb-config-content { background: white; border-radius: 8px; @@ -817,6 +816,7 @@ + @@ -2548,6 +2548,19 @@ const baseData = candles.map(c => ({ time: c.time, value: c.close })); series.setData(baseData); tvWidget.series.baselineSeries = series; + } else if (klineType === 'klc') { + // KLC显示模式 - 使用蜡烛线显示KLC数据 + const series = mainChart.addCandlestickSeries({ + upColor: '#dc3545', + downColor: '#28a745', + borderVisible: false, + wickUpColor: '#dc3545', + wickDownColor: '#28a745', + }); + // 使用KLC数据创建蜡烛图 + const klcCandles = buildKLCFromAnalysis(currentData); + series.setData(klcCandles); + tvWidget.series.klcSeries = series; } })(); @@ -2717,7 +2730,6 @@ tvWidget.series.signalLineSeries = signalLineSeries; tvWidget.series.histogramSeries = histogramSeries; } - // 添加ChanMACD图表 console.log('ChanMACD图表创建条件检查:', { showMacd: showMacd, @@ -2733,7 +2745,6 @@ if (typeof window.showUOnElement === 'undefined') { window.showUOnElement = $('#toggleUOnElement').is(':checked'); } - if (showMacd && chanMacdChart && ((useElementPeriod && currentData.element_macd) || currentData.macd) && (useElementPeriod ? currentData.element_kline_data : currentData.kline_data)) { console.log('✅ 开始创建 ChanMACD 系列'); // 创建ChanMACD线系列 @@ -4338,7 +4349,6 @@ }); } } - // 主周期未完成中枢 if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) { console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`); @@ -5305,7 +5315,6 @@ // 收集所有主周期分型标记 const allMainFxMarkers = []; const mainFxMarkers = []; // 用于tooltip支持 - // 处理主周期KLC分型 if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`); @@ -5671,6 +5680,7 @@ else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries; else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries; else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries; + else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries; if (targetSeries) { targetSeries.setMarkers(combinedMarkers); } else { @@ -5765,6 +5775,7 @@ else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries; else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries; else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries; + else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries; if (targetSeries2) { targetSeries2.setMarkers(onlyMainAndU); } else { @@ -5782,6 +5793,7 @@ else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries; else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries; else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries; + else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries; if (targetSeries3) { targetSeries3.setMarkers([]); } @@ -6024,6 +6036,9 @@ } else if (klineType === 'baseline' && tvWidget.series.baselineSeries) { const baseData = candles.map(c => ({ time: c.time, value: c.close })); tvWidget.series.baselineSeries.setData(baseData); + } else if (klineType === 'klc' && tvWidget.series.klcSeries) { + const klcCandles = buildKLCFromAnalysis(currentData); + tvWidget.series.klcSeries.setData(klcCandles); } // 更新均线数据 @@ -7010,7 +7025,6 @@ // 更新数据源信息 setupDataSourceInfo(data); } - // 设置数据源信息显示 function setupDataSourceInfo(data) { // 获取用户当前的周期选择 @@ -7103,7 +7117,6 @@ $('#end_time').val(formatDatetimeLocal(now)); $('#start_time').val(formatDatetimeLocal(oneDayAgo)); } - // 格式化日期为datetime-local输入框格式 function formatDatetimeLocal(date) { const year = date.getFullYear(); @@ -7753,7 +7766,6 @@ console.error('加载A股股票列表失败'); }); } - // 检测交易对类型并返回相应的配置 function getSymbolConfig(symbol) { const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol); @@ -7811,7 +7823,6 @@ return chartOptions; } - // 过滤非交易时间的数据(仅用于显示优化) function filterTradingHours(data, symbolConfig) { if (symbolConfig.type !== 'a_stock') { @@ -8402,7 +8413,6 @@ function updateMAPanel() { updateIndicatorPanel(); } - // 切换均线可见性 function toggleMAVisibility(maId) { console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId); @@ -8605,7 +8615,6 @@ return candles; } - // 从蜡烛数据生成 Heikin-Ashi(平均K) function buildHeikinFromCandles(candles) { if (!Array.isArray(candles) || candles.length === 0) return []; @@ -8627,7 +8636,45 @@ } return result; } - + + // 从分析数据生成KLC蜡烛数据 + function buildKLCFromAnalysis(data) { + if (!data) return []; + + // 检查是否使用小周期数据 + const useElementPeriod = $('#elementPeriodKline').is(':checked') && + data.element_klc_list && + Array.isArray(data.element_klc_list); + + const klcList = useElementPeriod ? data.element_klc_list : data.klc_list; + + if (!klcList) return []; + + const klcCandles = []; + + // 遍历KLC列表,转换为蜡烛数据格式 + klcList.forEach(klc => { + if (!klc || !klc.date) return; + + // 使用KLC的date字段,转换为时间戳格式 + const date = new Date(klc.date); + const timestamp = date.getTime() / 1000; + + // 创建KLC蜡烛数据 + const candle = { + time: timestamp, + open: klc.open || 0, + high: klc.high || 0, + low: klc.low || 0, + close: klc.close || 0 + }; + + klcCandles.push(candle); + }); + + return klcCandles; + } + // 计算默认砖大小(优先使用ATR的最新非零值,否则按收盘价的0.5%) function computeDefaultBrickSize(candles) { try { @@ -8881,10 +8928,10 @@ console.log('🔄 按钮已切换为更新模式'); - $('#bbConfigModal').css('display', 'flex'); - - // 更新预览 - setTimeout(updateBBLinePreview, 50); + $('#bbConfigModal').css('display', 'flex'); + + // 更新预览 + setTimeout(updateBBLinePreview, 50); } // 更新布林带 function updateBollingerBand(bbId) { @@ -9008,78 +9055,78 @@ console.warn('addBollingerBandsToChart 未就绪'); } - // 更新均线预览 + // 更新均线预览 function updateLinePreview() { - // 检查是否在均线配置窗口 - if ($('#maConfigModal').is(':visible')) { - const color = $('#maColor').val(); - const width = $('#maLineWidth').val(); - const style = $('#maLineStyle').val(); + // 检查是否在均线配置窗口 + if ($('#maConfigModal').is(':visible')) { + const color = $('#maColor').val(); + const width = $('#maLineWidth').val(); + const style = $('#maLineStyle').val(); + + const line = $('#previewLine'); + line.attr('stroke', color); + line.attr('stroke-width', width); + + // 设置线条样式 + switch(parseInt(style)) { + case 0: // 实线 + line.attr('stroke-dasharray', 'none'); + break; + case 1: // 点线 + line.attr('stroke-dasharray', '2,3'); + break; + case 2: // 虚线 + line.attr('stroke-dasharray', '5,5'); + break; + case 3: // 大虚线 + line.attr('stroke-dasharray', '10,5'); + break; + } + } + } + // 更新布林带预览 + function updateBBLinePreview() { + const upperColor = $('#bbUpperColor').val(); + const middleColor = $('#bbMiddleColor').val(); + const lowerColor = $('#bbLowerColor').val(); + const width = $('#bbLineWidth').val(); + const style = $('#bbLineStyle').val(); - const line = $('#previewLine'); - line.attr('stroke', color); - line.attr('stroke-width', width); + const upperLine = $('#bbPreviewUpper'); + const middleLine = $('#bbPreviewMiddle'); + const lowerLine = $('#bbPreviewLower'); - // 设置线条样式 - switch(parseInt(style)) { - case 0: // 实线 - line.attr('stroke-dasharray', 'none'); - break; - case 1: // 点线 - line.attr('stroke-dasharray', '2,3'); - break; - case 2: // 虚线 - line.attr('stroke-dasharray', '5,5'); - break; - case 3: // 大虚线 - line.attr('stroke-dasharray', '10,5'); - break; - } - } - } - - // 更新布林带预览 - function updateBBLinePreview() { - const upperColor = $('#bbUpperColor').val(); - const middleColor = $('#bbMiddleColor').val(); - const lowerColor = $('#bbLowerColor').val(); - const width = $('#bbLineWidth').val(); - const style = $('#bbLineStyle').val(); - - const upperLine = $('#bbPreviewUpper'); - const middleLine = $('#bbPreviewMiddle'); - const lowerLine = $('#bbPreviewLower'); - - // 设置各条线的颜色 - upperLine.attr('stroke', upperColor); - middleLine.attr('stroke', middleColor); - lowerLine.attr('stroke', lowerColor); - - // 设置线条宽度和样式 - [upperLine, middleLine, lowerLine].forEach(line => { - line.attr('stroke-width', width); - - // 设置线条样式 - switch(parseInt(style)) { - case 0: // 实线 - line.attr('stroke-dasharray', 'none'); - break; - case 1: // 点线 - line.attr('stroke-dasharray', '2,3'); - break; - case 2: // 虚线 - line.attr('stroke-dasharray', '5,5'); - break; - case 3: // 大虚线 - line.attr('stroke-dasharray', '10,5'); - break; - } - }); - } + // 设置各条线的颜色 + upperLine.attr('stroke', upperColor); + middleLine.attr('stroke', middleColor); + lowerLine.attr('stroke', lowerColor); + + // 设置线条宽度和样式 + [upperLine, middleLine, lowerLine].forEach(line => { + line.attr('stroke-width', width); + + // 设置线条样式 + switch(parseInt(style)) { + case 0: // 实线 + line.attr('stroke-dasharray', 'none'); + break; + case 1: // 点线 + line.attr('stroke-dasharray', '2,3'); + break; + case 2: // 虚线 + line.attr('stroke-dasharray', '5,5'); + break; + case 3: // 大虚线 + line.attr('stroke-dasharray', '10,5'); + break; + } + }); + } // 监听配置变化以更新预览 $(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview); - $(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview); + $(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview); + // 点击弹窗外部关闭 $(document).on('click', '#bbConfigModal', function(e) { if (e.target === this) { @@ -9128,8 +9175,6 @@ const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - - // 添加页面滚动事件监听器,清除十字线延长线 $(window).on('scroll', function() { try { @@ -9146,18 +9191,10 @@ console.debug('清除滚动中的十字线时出错:', e); } }); - - }); // ====== ChanMACD图表相关函数 ====== - - - - - - // 清除ChanMACD标注 function clearChanMacdMarkers() { // 清除所有系列的标记 @@ -9219,7 +9256,7 @@ color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50', shape: 'square', text: `S${index}E`, - size: 0.5 + size: 0.5 }); } }); @@ -9369,7 +9406,6 @@ addStateMarkers(stateMarkers); } } - // 添加状态标记 function addStateMarkers(stateMarkers) { const stateMarkersList = [];