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