添加裸K形态,前端添加KLC显示

This commit is contained in:
jackyu66git
2025-10-29 01:16:50 +08:00
parent abe663e1a6
commit fe4193b153
8 changed files with 589 additions and 114 deletions
+74
View File
@@ -51,6 +51,80 @@ class Chan_KLU_TYPE(Enum):
SmallBear = auto() SmallBear = auto()
Cross = 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): class Chan_FX_TYPE(Enum):
BOTTOM = auto() BOTTOM = auto()
TOP = auto() TOP = auto()
+5 -1
View File
@@ -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: class ChanKLU:
def __init__(self, time, open, high, low, close, volume): def __init__(self, time, open, high, low, close, volume):
# _time, _close, _open, _high, _low, _extra_info={} # _time, _close, _open, _high, _low, _extra_info={}
@@ -21,6 +21,7 @@ class ChanKLU:
self.bb52lower = 0 self.bb52lower = 0
# === 新增:K线类型 === # === 新增:K线类型 ===
self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线 self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线
self.pattern = Chan_KLU_PATTERN.UNKNOWN
# === 新增:实时分型相关属性 === # === 新增:实时分型相关属性 ===
self.pre = None # 前一根K线 self.pre = None # 前一根K线
@@ -67,10 +68,13 @@ class ChanKLU:
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength) #print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
def set_macd_state(self, state): def set_macd_state(self, state):
self.macd_state = state self.macd_state = state
def set_pattern(self, pattern):
self.pattern = pattern
def cal_exception(self): def cal_exception(self):
if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5: if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5:
self.exception = True 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) #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): def set_trend(self, trend):
self.trend = trend self.trend = trend
def set_next(self, next): def set_next(self, next):
+68 -6
View File
@@ -1,6 +1,6 @@
from datetime import timedelta from datetime import timedelta
from pandas import DataFrame 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 ChanKLU import ChanKLU
from ChanKLC import ChanKLC from ChanKLC import ChanKLC
from ChanBI import ChanBI from ChanBI import ChanBI
@@ -576,7 +576,7 @@ class ChanLun():
if near_resistance_touch: if near_resistance_touch:
# 若动量不强,则更偏空 # 若动量不强,则更偏空
score -= 1 if (hist is None or pre_hist is None or hist <= pre_hist) else 0 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 但收盘仍在下方
# - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方 # - 下拒绝:价格位于 EMA52 上方,最低触及/跌破 EMA52 但收盘仍在上方
@@ -701,7 +701,7 @@ class ChanLun():
for i in range(1, len(hist_seq)): for i in range(1, len(hist_seq)):
if abs(hist_seq[i]) < abs(hist_seq[i-1]): if abs(hist_seq[i]) < abs(hist_seq[i-1]):
weaken_steps += 1 weaken_steps += 1
# 近窗口对 EMA52 的未能站上/跌破统计(放宽窗口与条件) # 近窗口对 EMA52 的"未能站上/跌破"统计(放宽窗口与条件)
window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else [] window_ema = prev_klcs[-4:] if len(prev_klcs) > 0 else []
no_up_break = False no_up_break = False
no_down_break = False no_down_break = False
@@ -1343,8 +1343,70 @@ class ChanLun():
return klc_list return klc_list
def get_klu_list(self, dataframe): def get_klu_list(self, dataframe):
return self.get_kl_data(dataframe) 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
def get_decimal(self, value): for i, klu in enumerate(klu_list):
return Decimal("{:.2f}".format(value)) # 单根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) # 一般反转信号
+16 -11
View File
@@ -112,10 +112,7 @@ class ChanSEG():
last_zs = zs last_zs = zs
else: else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure: 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: if bi.low > last_zs.zg or bi.high < last_zs.zd:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
else:
last_zs.set_end_bi(last_zs.bi_list[-1], bi) 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: 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) zg = min(bi.high, bi.next.high, bi.next.next.high)
@@ -131,6 +128,12 @@ class ChanSEG():
zs.add_bi(bi.next.next) zs.add_bi(bi.next.next)
zs_list.append(zs) zs_list.append(zs)
last_zs = 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: else:
for index in range(1, len(self.bi_list)): for index in range(1, len(self.bi_list)):
bi = self.bi_list[index] bi = self.bi_list[index]
@@ -151,10 +154,7 @@ class ChanSEG():
last_zs = zs last_zs = zs
else: else:
if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure: 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: if bi.low > last_zs.zg or bi.high < last_zs.zd:
last_zs.add_bi(bi.pre)
last_zs.add_bi(bi)
else:
last_zs.set_end_bi(last_zs.bi_list[-1], bi) 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: 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) zg = min(bi.high, bi.next.high, bi.next.next.high)
@@ -170,8 +170,13 @@ class ChanSEG():
zs.add_bi(bi.next.next) zs.add_bi(bi.next.next)
zs_list.append(zs) zs_list.append(zs)
last_zs = zs last_zs = zs
if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: else:
last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1]) 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.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 return zs_list
+35
View File
@@ -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"
}
+233
View File
@@ -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()
+26
View File
@@ -1167,6 +1167,18 @@ def analyze():
# 添加主周期分析结果到返回数据 # 添加主周期分析结果到返回数据
result.update({ result.update({
'kline_data': clean_dataframe_for_json(df).to_dict('records'), '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': [{ '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(), '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, '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线数据 # 添加小周期K线数据
result['element_kline_data'] = clean_dataframe_for_json(element_df).to_dict('records') 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'] = [{ 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(), '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, '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,
+124 -88
View File
@@ -648,7 +648,6 @@
align-items: center; align-items: center;
z-index: 1000; z-index: 1000;
} }
.bb-config-content { .bb-config-content {
background: white; background: white;
border-radius: 8px; border-radius: 8px;
@@ -817,6 +816,7 @@
<option value="baseline">基线</option> <option value="baseline">基线</option>
<option value="renko">砖型</option> <option value="renko">砖型</option>
<option value="heikin">平均K</option> <option value="heikin">平均K</option>
<option value="klc">KLC</option>
</select> </select>
<!-- 为兼容现有逻辑,保留隐藏的原始K线开关,与下拉同步 --> <!-- 为兼容现有逻辑,保留隐藏的原始K线开关,与下拉同步 -->
<input class="form-check-input" type="checkbox" id="showOriginalKline" checked style="display:none;"> <input class="form-check-input" type="checkbox" id="showOriginalKline" checked style="display:none;">
@@ -2548,6 +2548,19 @@
const baseData = candles.map(c => ({ time: c.time, value: c.close })); const baseData = candles.map(c => ({ time: c.time, value: c.close }));
series.setData(baseData); series.setData(baseData);
tvWidget.series.baselineSeries = series; 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.signalLineSeries = signalLineSeries;
tvWidget.series.histogramSeries = histogramSeries; tvWidget.series.histogramSeries = histogramSeries;
} }
// 添加ChanMACD图表 // 添加ChanMACD图表
console.log('ChanMACD图表创建条件检查:', { console.log('ChanMACD图表创建条件检查:', {
showMacd: showMacd, showMacd: showMacd,
@@ -2733,7 +2745,6 @@
if (typeof window.showUOnElement === 'undefined') { if (typeof window.showUOnElement === 'undefined') {
window.showUOnElement = $('#toggleUOnElement').is(':checked'); window.showUOnElement = $('#toggleUOnElement').is(':checked');
} }
if (showMacd && chanMacdChart && ((useElementPeriod && currentData.element_macd) || currentData.macd) && (useElementPeriod ? currentData.element_kline_data : currentData.kline_data)) { if (showMacd && chanMacdChart && ((useElementPeriod && currentData.element_macd) || currentData.macd) && (useElementPeriod ? currentData.element_kline_data : currentData.kline_data)) {
console.log('✅ 开始创建 ChanMACD 系列'); console.log('✅ 开始创建 ChanMACD 系列');
// 创建ChanMACD线系列 // 创建ChanMACD线系列
@@ -4338,7 +4349,6 @@
}); });
} }
} }
// 主周期未完成中枢 // 主周期未完成中枢
if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) { if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) {
console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`); console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`);
@@ -5305,7 +5315,6 @@
// 收集所有主周期分型标记 // 收集所有主周期分型标记
const allMainFxMarkers = []; const allMainFxMarkers = [];
const mainFxMarkers = []; // 用于tooltip支持 const mainFxMarkers = []; // 用于tooltip支持
// 处理主周期KLC分型 // 处理主周期KLC分型
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`); console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`);
@@ -5671,6 +5680,7 @@
else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries; else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries;
else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries; else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries;
else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries; else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries;
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
if (targetSeries) { if (targetSeries) {
targetSeries.setMarkers(combinedMarkers); targetSeries.setMarkers(combinedMarkers);
} else { } else {
@@ -5765,6 +5775,7 @@
else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries; else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries;
else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries; else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries;
else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries; else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries;
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
if (targetSeries2) { if (targetSeries2) {
targetSeries2.setMarkers(onlyMainAndU); targetSeries2.setMarkers(onlyMainAndU);
} else { } else {
@@ -5782,6 +5793,7 @@
else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries; else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries;
else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries; else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries;
else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries; else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries;
else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries;
if (targetSeries3) { if (targetSeries3) {
targetSeries3.setMarkers([]); targetSeries3.setMarkers([]);
} }
@@ -6024,6 +6036,9 @@
} else if (klineType === 'baseline' && tvWidget.series.baselineSeries) { } else if (klineType === 'baseline' && tvWidget.series.baselineSeries) {
const baseData = candles.map(c => ({ time: c.time, value: c.close })); const baseData = candles.map(c => ({ time: c.time, value: c.close }));
tvWidget.series.baselineSeries.setData(baseData); 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); setupDataSourceInfo(data);
} }
// 设置数据源信息显示 // 设置数据源信息显示
function setupDataSourceInfo(data) { function setupDataSourceInfo(data) {
// 获取用户当前的周期选择 // 获取用户当前的周期选择
@@ -7103,7 +7117,6 @@
$('#end_time').val(formatDatetimeLocal(now)); $('#end_time').val(formatDatetimeLocal(now));
$('#start_time').val(formatDatetimeLocal(oneDayAgo)); $('#start_time').val(formatDatetimeLocal(oneDayAgo));
} }
// 格式化日期为datetime-local输入框格式 // 格式化日期为datetime-local输入框格式
function formatDatetimeLocal(date) { function formatDatetimeLocal(date) {
const year = date.getFullYear(); const year = date.getFullYear();
@@ -7753,7 +7766,6 @@
console.error('加载A股股票列表失败'); console.error('加载A股股票列表失败');
}); });
} }
// 检测交易对类型并返回相应的配置 // 检测交易对类型并返回相应的配置
function getSymbolConfig(symbol) { function getSymbolConfig(symbol) {
const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol); const isAStock = symbol && symbol.length === 6 && /^\d+$/.test(symbol);
@@ -7811,7 +7823,6 @@
return chartOptions; return chartOptions;
} }
// 过滤非交易时间的数据(仅用于显示优化) // 过滤非交易时间的数据(仅用于显示优化)
function filterTradingHours(data, symbolConfig) { function filterTradingHours(data, symbolConfig) {
if (symbolConfig.type !== 'a_stock') { if (symbolConfig.type !== 'a_stock') {
@@ -8402,7 +8413,6 @@
function updateMAPanel() { function updateMAPanel() {
updateIndicatorPanel(); updateIndicatorPanel();
} }
// 切换均线可见性 // 切换均线可见性
function toggleMAVisibility(maId) { function toggleMAVisibility(maId) {
console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId); console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId);
@@ -8605,7 +8615,6 @@
return candles; return candles;
} }
// 从蜡烛数据生成 Heikin-Ashi(平均K // 从蜡烛数据生成 Heikin-Ashi(平均K
function buildHeikinFromCandles(candles) { function buildHeikinFromCandles(candles) {
if (!Array.isArray(candles) || candles.length === 0) return []; if (!Array.isArray(candles) || candles.length === 0) return [];
@@ -8628,6 +8637,44 @@
return result; 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%) // 计算默认砖大小(优先使用ATR的最新非零值,否则按收盘价的0.5%)
function computeDefaultBrickSize(candles) { function computeDefaultBrickSize(candles) {
try { try {
@@ -8881,10 +8928,10 @@
console.log('🔄 按钮已切换为更新模式'); console.log('🔄 按钮已切换为更新模式');
$('#bbConfigModal').css('display', 'flex'); $('#bbConfigModal').css('display', 'flex');
// 更新预览 // 更新预览
setTimeout(updateBBLinePreview, 50); setTimeout(updateBBLinePreview, 50);
} }
// 更新布林带 // 更新布林带
function updateBollingerBand(bbId) { function updateBollingerBand(bbId) {
@@ -9008,78 +9055,78 @@
console.warn('addBollingerBandsToChart 未就绪'); console.warn('addBollingerBandsToChart 未就绪');
} }
// 更新均线预览 // 更新均线预览
function updateLinePreview() { function updateLinePreview() {
// 检查是否在均线配置窗口 // 检查是否在均线配置窗口
if ($('#maConfigModal').is(':visible')) { if ($('#maConfigModal').is(':visible')) {
const color = $('#maColor').val(); const color = $('#maColor').val();
const width = $('#maLineWidth').val(); const width = $('#maLineWidth').val();
const style = $('#maLineStyle').val(); const style = $('#maLineStyle').val();
const line = $('#previewLine'); const line = $('#previewLine');
line.attr('stroke', color); line.attr('stroke', color);
line.attr('stroke-width', width); line.attr('stroke-width', width);
// 设置线条样式 // 设置线条样式
switch(parseInt(style)) { switch(parseInt(style)) {
case 0: // 实线 case 0: // 实线
line.attr('stroke-dasharray', 'none'); line.attr('stroke-dasharray', 'none');
break; break;
case 1: // 点线 case 1: // 点线
line.attr('stroke-dasharray', '2,3'); line.attr('stroke-dasharray', '2,3');
break; break;
case 2: // 虚线 case 2: // 虚线
line.attr('stroke-dasharray', '5,5'); line.attr('stroke-dasharray', '5,5');
break; break;
case 3: // 大虚线 case 3: // 大虚线
line.attr('stroke-dasharray', '10,5'); line.attr('stroke-dasharray', '10,5');
break; 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');
function updateBBLinePreview() { const middleLine = $('#bbPreviewMiddle');
const upperColor = $('#bbUpperColor').val(); const lowerLine = $('#bbPreviewLower');
const middleColor = $('#bbMiddleColor').val();
const lowerColor = $('#bbLowerColor').val();
const width = $('#bbLineWidth').val();
const style = $('#bbLineStyle').val();
const upperLine = $('#bbPreviewUpper'); // 设置各条线的颜色
const middleLine = $('#bbPreviewMiddle'); upperLine.attr('stroke', upperColor);
const lowerLine = $('#bbPreviewLower'); middleLine.attr('stroke', middleColor);
lowerLine.attr('stroke', lowerColor);
// 设置各条线的颜色 // 设置线条宽度和样式
upperLine.attr('stroke', upperColor); [upperLine, middleLine, lowerLine].forEach(line => {
middleLine.attr('stroke', middleColor); line.attr('stroke-width', width);
lowerLine.attr('stroke', lowerColor);
// 设置线条宽度和样式 // 设置线条样式
[upperLine, middleLine, lowerLine].forEach(line => { switch(parseInt(style)) {
line.attr('stroke-width', width); case 0: // 实线
line.attr('stroke-dasharray', 'none');
// 设置线条样式 break;
switch(parseInt(style)) { case 1: // 点线
case 0: // 实线 line.attr('stroke-dasharray', '2,3');
line.attr('stroke-dasharray', 'none'); break;
break; case 2: // 虚线
case 1: // 点线 line.attr('stroke-dasharray', '5,5');
line.attr('stroke-dasharray', '2,3'); break;
break; case 3: // 大虚线
case 2: // 虚线 line.attr('stroke-dasharray', '10,5');
line.attr('stroke-dasharray', '5,5'); break;
break; }
case 3: // 大虚线 });
line.attr('stroke-dasharray', '10,5'); }
break;
}
});
}
// 监听配置变化以更新预览 // 监听配置变化以更新预览
$(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview); $(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) { $(document).on('click', '#bbConfigModal', function(e) {
if (e.target === this) { if (e.target === this) {
@@ -9128,8 +9175,6 @@
const now = new Date(); const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
// 添加页面滚动事件监听器,清除十字线延长线 // 添加页面滚动事件监听器,清除十字线延长线
$(window).on('scroll', function() { $(window).on('scroll', function() {
try { try {
@@ -9146,18 +9191,10 @@
console.debug('清除滚动中的十字线时出错:', e); console.debug('清除滚动中的十字线时出错:', e);
} }
}); });
}); });
// ====== ChanMACD图表相关函数 ====== // ====== ChanMACD图表相关函数 ======
// 清除ChanMACD标注 // 清除ChanMACD标注
function clearChanMacdMarkers() { function clearChanMacdMarkers() {
// 清除所有系列的标记 // 清除所有系列的标记
@@ -9219,7 +9256,7 @@
color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50', color: seg.seg_dir === 'ABOVE' ? '#e91e63' : '#4caf50',
shape: 'square', shape: 'square',
text: `S${index}E`, text: `S${index}E`,
size: 0.5 size: 0.5
}); });
} }
}); });
@@ -9369,7 +9406,6 @@
addStateMarkers(stateMarkers); addStateMarkers(stateMarkers);
} }
} }
// 添加状态标记 // 添加状态标记
function addStateMarkers(stateMarkers) { function addStateMarkers(stateMarkers) {
const stateMarkersList = []; const stateMarkersList = [];