diff --git a/ChanEnum.py b/ChanEnum.py index 3754d21..9ee4f06 100644 --- a/ChanEnum.py +++ b/ChanEnum.py @@ -268,7 +268,15 @@ Chan_BSP_MAIN_TYPE = Literal['1', '2', '3'] class Chan_BSP_DIR(Enum): BUY = auto() SELL = auto() - +class Chan_BSP_TYPE(Enum): + B1 = auto() + B2 = auto() + B3 = auto() + S1 = auto() + S2 = auto() + S3 = auto() + NONE = auto() +""" class Chan_BSP_TYPE(Enum): T1 = '1' T1P = '1p' @@ -285,7 +293,7 @@ class Chan_BSP_TYPE(Enum): def main_type(self) -> Chan_BSP_MAIN_TYPE: return self.value[0] # type: ignore - +""" class Chan_AUTYPE(Enum): QFQ = auto() HFQ = auto() diff --git a/ChanKLC.py b/ChanKLC.py index a0d6fc2..7634009 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -1,7 +1,7 @@ import copy from typing import Dict, Optional -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX, Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS, Chan_EMA_SEMANTIC +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX, Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS, Chan_EMA_SEMANTIC, Chan_BSP_TYPE import ChanKLU import ChanCTime @@ -55,6 +55,7 @@ class ChanKLC(): self.klc_dir = Chan_KLINE_DIR.UP if klu.close > klu.open else Chan_KLINE_DIR.DOWN self.ema_dir = klu.ema_dir self.bsp = False + self.bsp_type = Chan_BSP_TYPE.NONE # EMA状态字典:key为EMA名称,value为 {'pos': Chan_EMA_POS, 'semantic': Chan_EMA_SEMANTIC} self.ema_status = {} # 向后兼容:保留 ema52_status 和 ema52_pos @@ -233,7 +234,10 @@ class ChanKLC(): # threshold_pct: 阈值百分比,用于自动计算绝对阈值 # 例如 0.001 表示 EMA 值的 0.1%,BTC $100,000 时 threshold = $100 threshold_pct = 0.001 - + def set_bsp_type(self, bsp_type): + if bsp_type and bsp_type != Chan_BSP_TYPE.NONE: + self.bsp_type = bsp_type + self.bsp = True def cal_all_ema_status(self): """ 统一计算所有EMA与K线的位置关系和语义状态 diff --git a/ChanLun.py b/ChanLun.py index 20aff1d..ffd7f52 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -157,8 +157,8 @@ class ChanLun(): return self.tf_df.find_first_bsp(bi_list, bi_zs_list) def find_second_bsp(self, bi_list, first_bsp_list): return self.tf_df.find_second_bsp(bi_list, first_bsp_list) - def find_third_bsp(self, bi_list, bi_zs_list): - return self.tf_df.find_third_bsp(bi_list, bi_zs_list) + def find_all_bsp(self, bi_list, bi_zs_list): + return self.tf_df.find_all_bsp(bi_list, bi_zs_list) def get_zs_list(self, bi_list, seg_list): return self.tf_df.get_zs_list(bi_list, seg_list) def cal_bi_zs(self, seg_list): diff --git a/TF_DF.py b/TF_DF.py index 0e5efd2..d2cdbb7 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -156,12 +156,12 @@ class TF_DF(): return klu_state_list def check_fx(self, klc): if klc.pre and klc.next: - if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low and klc.macd> 0: + if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: klc.set_fx(Chan_FX_TYPE.TOP) #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") return Chan_FX_TYPE.TOP - elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high and klc.macd < 0: + elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high: #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: klc.set_fx(Chan_FX_TYPE.BOTTOM) #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") @@ -1399,7 +1399,7 @@ class TF_DF(): if len(zs_list) > 0: bi_zs_list.append(zs_list) return bi_zs_list - def find_third_bsp(self, bi_list, bi_zs_list): + def find_all_bsp(self, bi_list, bi_zs_list): """ 笔中枢的三类买卖点识别 @@ -1444,6 +1444,18 @@ class TF_DF(): # 三类买点:向上离开中枢后回拉不破 zg #print("Leave bi:", leave_bi.start_time, leave_bi.end_time, leave_bi.dir, leave_bi.is_sure, leave_bi.low, leave_bi.high) if leave_bi.dir == Chan_BI_DIR.UP: + first_bsp_bi_div = self.check_bi_div(zs, leave_bi) + # 确认一类卖点:离开断能量小于进入段能量 + if first_bsp_bi_div: + bsp = ChanBSP( + leave_bi, len(bsp_list), + Chan_BSP_TYPE.S1, + Chan_BSP_DIR.SELL, + leave_bi.sure_time, + 1, zs, None + ) + leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S1) + bsp_list.append(bsp) # 回拉笔 pullback_bi = leave_bi.next #print(pullback_bi.start_klc.start_time, pullback_bi.dir, pullback_bi.is_sure, pullback_bi.low, pullback_bi.high) @@ -1452,15 +1464,41 @@ class TF_DF(): # 确认三类买点:回拉笔的低点不跌回中枢 bsp = ChanBSP( pullback_bi, len(bsp_list), - Chan_BSP_TYPE.T3, + Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY, pullback_bi.sure_time, 1, zs, None ) + pullback_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B3) + bsp_list.append(bsp) + # 二类卖点 + if first_bsp_bi_div: + second_bsp_bi = pullback_bi.next + if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.high < leave_bi.end_klc.high: + # 确认二类卖点:一类卖点后回拉不超过一类卖点高点 + bsp = ChanBSP( + second_bsp_bi, len(bsp_list), + Chan_BSP_TYPE.S2, + Chan_BSP_DIR.SELL, + second_bsp_bi.sure_time, + 1, zs, None + ) + second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2) bsp_list.append(bsp) - # 三类卖点:向下离开中枢后反弹不破 zd elif leave_bi.dir == Chan_BI_DIR.DOWN: + first_bsp_bi_div = self.check_bi_div(zs, leave_bi) + # 确认一类买点:离开段能量小于进入段 + if first_bsp_bi_div: + bsp = ChanBSP( + leave_bi, len(bsp_list), + Chan_BSP_TYPE.B1, + Chan_BSP_DIR.BUY, + leave_bi.sure_time, + 1, zs, None + ) + leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B1) + bsp_list.append(bsp) # 反弹笔 bounce_bi = leave_bi.next #print(bounce_bi.start_klc.start_time, bounce_bi.dir, bounce_bi.is_sure, bounce_bi.low, bounce_bi.high) @@ -1469,14 +1507,35 @@ class TF_DF(): # 确认三类卖点:反弹笔的高点不回到中枢 bsp = ChanBSP( bounce_bi, len(bsp_list), - Chan_BSP_TYPE.T3, + Chan_BSP_TYPE.S3, Chan_BSP_DIR.SELL, bounce_bi.sure_time, 1, zs, None ) + bounce_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S3) + bsp_list.append(bsp) + # 二类卖点 + if first_bsp_bi_div: + second_bsp_bi = bounce_bi.next + if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.low > leave_bi.end_klc.low: + # 确认二类买点:一类买点后回拉不超过一类卖点高点 + bsp = ChanBSP( + second_bsp_bi, len(bsp_list), + Chan_BSP_TYPE.B2, + Chan_BSP_DIR.BUY, + second_bsp_bi.sure_time, + 1, zs, None + ) + second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2) bsp_list.append(bsp) return bsp_list - + def check_bi_div(self, zs, leave_bi): + enter_bi = zs.bi_list[0].pre + macdhist_div = 0 + if enter_bi and enter_bi.dir == leave_bi.dir: + macdhist_div = abs(leave_bi.macd_hist) - abs(enter_bi.macd_hist) + #print(enter_bi.end_time, leave_bi.end_time, macdhist_div < 0) + return macdhist_div < 0 def find_first_bsp(self, bi_list, bi_zs_list): """ 笔中枢的一类买卖点识别 @@ -1776,7 +1835,7 @@ class TF_DF(): bi_out_count += 1 first_bi_out = bi if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): - bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) + bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) bsp_list.append(bsp) #print("First bi out 3", first_bi_out.start_klc.start_time) in_again = False @@ -1812,7 +1871,7 @@ class TF_DF(): bi_out_count += 1 first_bi_out = bi if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): - bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.T3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) + bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) bsp_list.append(bsp) #print("First bi out 4", first_bi_out.start_klc.start_time) in_again = False @@ -1820,12 +1879,12 @@ class TF_DF(): if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: #print(bi.start_klc.start_time, bi.high, seg.high) if bi.high == seg.high: - bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg) + bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.S3, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg) bsp_list.append(bsp) else: if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN: if bi.low == seg.low: - bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg) + bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg) bsp_list.append(bsp) #self.print_zs(zs_list) return zs_list diff --git a/config/BTC_Perpetual_Futures.json b/config/BTC_Perpetual_Futures.json new file mode 100644 index 0000000..2bcb666 --- /dev/null +++ b/config/BTC_Perpetual_Futures.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "max_open_trades": 1, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "dry_run": true, + "db_url": "sqlite:///tradesv3.btc_perpetual.sqlite", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "can_short": true, + "timeframe": "1m", + "process_only_new_candles": false, + "unfilledtimeout": { + "entry": 1, + "exit": 1, + "exit_timeout_count": 5, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "YOUR_BINANCE_API_KEY", + "secret": "YOUR_BINANCE_API_SECRET", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT" + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList", + "number_assets": 1, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + "telegram": { + "enabled": true, + "token": "YOUR_TELEGRAM_BOT_TOKEN", + "chat_id": "YOUR_TELEGRAM_CHAT_ID" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8820, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "change_me_to_a_random_secret_key", + "ws_token": "change_me_to_a_random_ws_token", + "CORS_origins": [], + "username": "freqtrader", + "password": "FreqTrade007" + }, + "bot_name": "BTC_Perpetual_Bot", + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 2 + } +} diff --git a/strategies/BTC_Perpetual_Futures.py b/strategies/BTC_Perpetual_Futures.py new file mode 100644 index 0000000..b4dfbdc --- /dev/null +++ b/strategies/BTC_Perpetual_Futures.py @@ -0,0 +1,259 @@ +# --- Do not remove these libs --- +from freqtrade.strategy import IStrategy, stoploss_from_absolute +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from ChanLun import ChanLun +from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR +# -------------------------------- +from technical.util import resample_to_interval, resampled_merge +import talib.abstract as ta +from pandas import DataFrame +from datetime import datetime, timedelta +from freqtrade.persistence import Trade, Order +from typing import Optional +import logging + +logger = logging.getLogger(__name__) + +# freqtrade trade -c ./user_data/Chan/config/BTC_Perpetual_Futures.json --strategy BTC_Perpetual_Futures --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/BTC_Perpetual_Futures.json --strategy BTC_Perpetual_Futures --strategy-path ./user_data/Chan/strategies --timerange=20260101- +# freqtrade download-data -c ./user_data/Chan/config/BTC_Perpetual_Futures.json -t 1m --pairs BTC/USDT:USDT --timerange=20260101- + + +class BTC_Perpetual_Futures(IStrategy): + """ + BTC永续合约交易策略 - 优化版 + - 基于缠论(ChanLun)技术分析 + RSI/MACD/布林带/ATR 多指标共振 + - 支持做多和做空 + - 基于ATR的动态止损 + - 成交量确认过滤 + """ + INTERFACE_VERSION: int = 3 + + # ROI配��� - 分阶段止盈 + minimal_roi = { + "0": 0.08, # 立即: 8%止盈 + "60": 0.05, # 1小时后: 5%止盈 + "180": 0.02, # 3小时后: 2%止盈 + "360": 0 # 6小时后: 保本出场 + } + + can_short = True + lev = 1.0 # 杠杆倍数,建议新手用1-3倍 + + stoploss = -0.04 # 默认4%止损(custom_stoploss会覆盖) + use_custom_stoploss = True + + trailing_stop = False + position_adjustment_enable = False + startup_candle_count = 1440 # 需要1440根1分钟K线预热 + + # 时间框架常量 + time5 = 5 + time15 = 15 + time30 = 30 + time60 = 60 + + chan = ChanLun() + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """添加多时间框架技术指标""" + # 重采样到5分钟和30分钟 + dataframe_5m = resample_to_interval(dataframe, self.get_ticker_indicator() * self.time5) + dataframe_30m = resample_to_interval(dataframe, self.get_ticker_indicator() * self.time30) + + # 添加技术指标 + dataframe = self._add_indicators(dataframe) + dataframe_5m = self._add_indicators(dataframe_5m) + dataframe_30m = self._add_indicators(dataframe_30m) + + # 缠论状态分析 + dataframe_5m['state'] = self.chan.get_klu_state(dataframe_5m) + dataframe_30m['state'] = self.chan.get_klu_state(dataframe_30m) + + # 合并多时间框架数据 + dataframe = resampled_merge(dataframe, dataframe_5m) + dataframe = resampled_merge(dataframe, dataframe_30m) + + return dataframe + + def _add_indicators(self, df: DataFrame) -> DataFrame: + """添加技术指标""" + # MACD + macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + + # 布林带 (20周期, 2倍标准差) + bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) + df['bb_upper'] = bb['upperband'] + df['bb_middle'] = bb['middleband'] + df['bb_lower'] = bb['lowerband'] + + # ATR - 用于动态止损 + df['atr'] = ta.ATR(df, timeperiod=14) + + # EMA均线系统 + df['ema5'] = ta.EMA(df, timeperiod=5) + df['ema10'] = ta.EMA(df, timeperiod=10) + df['ema26'] = ta.EMA(df, timeperiod=26) + df['ema52'] = ta.EMA(df, timeperiod=52) + + # RSI + df['rsi'] = ta.RSI(df, timeperiod=14) + + # 成交量比(当前成交量 / 10周期均量) + avg_vol = df['volume'].rolling(window=10).mean() + df['volume_ratio'] = (df['volume'] / avg_vol).fillna(1.0) + + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 进场信号定义 - 优化版 + - 做多: 缠论底部信号(-10/-20) + RSI<65 + 放量 + EMA确认 + - 做空: 缠论顶部信号(10/20) + RSI>35 + 放量 + EMA确认 + """ + state_30m = 'resample_{}_state'.format(self.get_ticker_indicator() * self.time30) + ema52_30m = 'resample_{}_ema52'.format(self.get_ticker_indicator() * self.time30) + close_30m = 'resample_{}_close'.format(self.get_ticker_indicator() * self.time30) + shift = self.time30 + + # 做多信号:只在缠论-10信号 + 强势过滤 + dataframe.loc[ + (dataframe[state_30m].shift(shift) == "-10") & + (dataframe['rsi'] < 65) & + (dataframe['rsi'] > 20) & # RSI不过冷 + (dataframe['volume_ratio'] > 1.2) & # 放量确认 + (dataframe['close'] > dataframe['ema52']) & # 价格在EMA52上方 + (dataframe['macd'] > dataframe['macdsignal']), # MACD金叉 + ['enter_long', 'enter_tag']] = (1, 'chan_long') + + # 做空信号:只在缠论10信号 + 强势过滤 + dataframe.loc[ + (dataframe[state_30m].shift(shift) == "10") & + (dataframe['rsi'] > 35) & + (dataframe['rsi'] < 80) & # RSI不过热 + (dataframe['volume_ratio'] > 1.2) & # 放量确认 + (dataframe['close'] < dataframe['ema52']) & # 价格在EMA52下方 + (dataframe['macd'] < dataframe['macdsignal']), # MACD死叉 + ['enter_short', 'enter_tag']] = (1, 'chan_short') + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 出场信号定义 + - 做多出场: 缠论顶部反转信号 + - 做空出场: 缠论底部反转信号 + """ + state_30m = 'resample_{}_state'.format(self.get_ticker_indicator() * self.time30) + shift = self.time30 + + dataframe.loc[ + (dataframe[state_30m].shift(shift) == "10"), + ['exit_long', 'exit_tag']] = (1, 'chan_exit_long') + + dataframe.loc[ + (dataframe[state_30m].shift(shift) == "-10"), + ['exit_short', 'exit_tag']] = (1, 'chan_exit_short') + + return dataframe + + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float | None: + """ + 基于ATR的动态止损 + 止损距离 = 开仓价 ± 1.5*ATR + """ + try: + entry_atr = trade.get_custom_data(key="entry_atr") + if entry_atr is None: + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + if dataframe is not None and len(dataframe) > 0 and 'atr' in dataframe.columns: + entry_atr = float(dataframe.iloc[-1]['atr']) + else: + return -0.04 + + if trade.is_short: + stop_price = trade.open_rate + (float(entry_atr) * 1.5) + else: + stop_price = trade.open_rate - (float(entry_atr) * 1.5) + + return stoploss_from_absolute(stop_price, current_rate, is_short=trade.is_short) + + except Exception as e: + logger.warning(f"custom_stoploss error: {e}") + return None + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs): + """快速止盈:浮盈超过0.5%直接出场""" + if current_profit > 0.005: + return "quick_profit" + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: str | None, + side: str, **kwargs) -> bool: + """进场前最终过滤:ATR太小或RSI极端时拒绝""" + try: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if dataframe is None or len(dataframe) == 0: + return False + + last = dataframe.iloc[-1] + atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator() * self.time30) + atr_val = float(last.get(atr_str, 0) or 0) + if atr_val < 0.001: + return False + + return True + except Exception as e: + logger.warning(f"confirm_trade_entry error: {e}") + return True + + def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None: + """订单成交时保存ATR用于止损计算""" + try: + if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + if dataframe is not None and len(dataframe) > 0: + atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator() * self.time30) + last = dataframe.iloc[-1] + entry_atr = float(last.get(atr_str, 0) or 0) * 3 + trade.set_custom_data(key="entry_atr", value=entry_atr) + except Exception as e: + logger.warning(f"order_filled error: {e}") + + def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, + entry_tag: str | None, side: str, **kwargs) -> float: + """入场价微调,减少滑点""" + if trade: + if trade.is_short: + return proposed_rate - 50 + else: + return proposed_rate + 50 + return proposed_rate + + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_profit: float, exit_tag: str | None, **kwargs) -> float: + """出场价微调,减少滑点""" + if trade: + if trade.is_short: + return proposed_rate + 50 + else: + return proposed_rate - 50 + return proposed_rate + + 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 self.lev + + def get_ticker_indicator(self) -> int: + return int(self.timeframe[:-1]) diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py index 932a2c6..f4ba615 100644 --- a/strategies/ChanLun_BTC_30.py +++ b/strategies/ChanLun_BTC_30.py @@ -349,12 +349,16 @@ class ChanLun_BTC_30(IStrategy): state30 = 'resample_{}_state'.format(self.get_ticker_indicator()*shift30) dataframe.loc[ ( - (dataframe[state30].shift(shift30) == "10") + (dataframe[state30].shift(shift30) == "10") | + (dataframe[state30].shift(shift30) == "20") | + (dataframe[state30].shift(shift30) == "30") ), ['exit_long', 'exit_tag']] = (1, 'long_close_30') dataframe.loc[ ( - (dataframe[state30].shift(shift30) == "-10") + (dataframe[state30].shift(shift30) == "-10") | + (dataframe[state30].shift(shift30) == "-20") | + (dataframe[state30].shift(shift30) == "-30") ), ['exit_short', 'exit_tag']] = (1, 'short_close_30') return dataframe diff --git a/web/app.py b/web/app.py index 5fe85b6..3b506ca 100644 --- a/web/app.py +++ b/web/app.py @@ -532,7 +532,9 @@ def analyze_chan(df, symbol=None, timeframe=None): bi_zs_list = [] bsp_list = [] if len(bi_zs_list) > 0: - bsp_list = chan.find_third_bsp(bi_list, bi_zs_list) + bsp_list = chan.find_all_bsp(bi_list, bi_zs_list) + for bsp in bsp_list: + print(bsp.end_time, bsp.type, bsp.dir) # 添加买卖点识别 for bi in bi_list: bi.cal_macdhist() @@ -646,6 +648,7 @@ def analyze_chan(df, symbol=None, timeframe=None): 'seg_list': seg_list, 'zs_list': zs_list, 'bi_zs_list': bi_zs_list, # 添加BI中枢列表 + 'bsp_list': bsp_list, # 添加买卖点列表 'klc_fx_info': klc_fx_info, # KLC分型信息 'chan_macd': chan_macd_data, # 添加ChanMACD分析数据 'ema52_dict': ema52_dict # 添加多时间周期EMA52数据 @@ -1379,7 +1382,36 @@ def analyze(): # 添加多时间周期EMA52数据 'ema52_dict': analysis_result.get('ema52_dict', {}), # 直接输出KLC趋势标记(使用已有trend字段) - 'klc_trend': klc_trend + 'klc_trend': klc_trend, + # 添加主周期买卖点列表 + # 注意:部分枚举在转为字符串时可能形如 "Chan_BSP_TYPE.BSP1(1)", + # 这里进行健壮的解析,确保前端拿到的始终是 "BSP1" / "BUY" 这种简洁形式, + # 以便与前端的 BSP_STYLE 键(如 "BSP1_BUY")正确匹配。 + 'bsp_list': [{ + 'time': format_time_safely(bsp.end_time, client_tz), + 'price': float(bsp.klc.low if 'BUY' in str(bsp.dir) else bsp.klc.high), + # -- 规范化 type 名称,例如: + # "Chan_BSP_TYPE.BSP1" -> "BSP1" + # "Chan_BSP_TYPE.BSP1(1)" -> "BSP1" + # "BSP1" -> "BSP1" + 'type': ( + lambda raw: ( + (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] + ) + )(str(bsp.type)), + # -- 规范化 dir 名称,例如: + # "Chan_BSP_DIR.BUY" -> "BUY" + # "Chan_BSP_DIR.BUY(1)" -> "BUY" + # "BUY" -> "BUY" + 'dir': ( + lambda raw: ( + (raw.split('.')[-1] if '.' in raw else raw).split('(')[0] + ) + )(str(bsp.dir)), + 'is_sure': bool(bsp.is_sure), + 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, + 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 + } for bsp in analysis_result.get('bsp_list', [])] }) @@ -1547,6 +1579,17 @@ def analyze(): # 添加小周期 KLC 趋势标记 result['element_klc_trend'] = element_klc_trend + + # 添加次周期买卖点列表 + result['element_bsp_list'] = [{ + 'time': format_time_safely(bsp.end_time, client_tz), + 'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high), + 'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''), + 'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''), + 'is_sure': bool(bsp.is_sure), + 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, + 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 + } for bsp in element_analysis.get('bsp_list', [])] pass diff --git a/web/templates/index.html b/web/templates/index.html index 3a56c7c..d3f5bc0 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -912,6 +912,10 @@ +