diff --git a/.DS_Store b/.DS_Store index d30ef56..536b273 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ChanKLC.py b/ChanKLC.py index 4601a08..7172096 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -84,6 +84,9 @@ class ChanKLC(): if self.fx == Chan_FX_TYPE.BOTTOM: if self.macd < 0: self.bb_out = True + def cal_macd_state(self, dir): + + return macd_state def cal_indicators(self): for index in range(1, len(self.klus)): self.volume += self.klus[index].volume diff --git a/ChanKLU.py b/ChanKLU.py index 9eac3d5..8144a64 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -600,6 +600,14 @@ class ChanKLU: # 设置指标后更新实时分析 self.update_realtime_analysis() + def cal_macd_state(self): + if self.pre: + pre_macd_slop = self.macd - self.pre.macd + pre_signal_slop = self.signal - self.pre.signal + pre_hist_slop = self.macdhist - self.pre.macdhist + + return self.macd_state + def get_feature_data(self): features = dict() features['klu_close'] = self.close diff --git a/ChanLun.py b/ChanLun.py index 1528e38..c32b657 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -1130,9 +1130,9 @@ class ChanLun(): def get_decimal(self, value): return Decimal("{:.2f}".format(value)) def add_indicators(self, df): - fast = 8 - slow = 16 - period = 6 + fast = 12 + slow = 26 + period = 9 macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) bbp365 = ta.BBP(df, timeperiod=365) diff --git a/config/ChanLun_BTC_K_protect.json b/config/ChanLun_BTC_K_protect.json new file mode 100644 index 0000000..1158b5a --- /dev/null +++ b/config/ChanLun_BTC_K_protect.json @@ -0,0 +1,87 @@ +{ + "$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.chanlun_btc_k.sqlite", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "can_short": true, + "timeframe": "1m", + "process_only_new_candles": false, + "unfilledtimeout": { + "entry": 1, + "exit": 1, + "exit_timeout_count": 5, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8", + "secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT" + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList", + "number_assets": 1, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + + "telegram": { + "enabled": false, + "token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y", + "chat_id": "580807463" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8815, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d", + "ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg", + "CORS_origins": [], + "username": "freqtrader", + "password": "FreqTrade007" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 2 + } +} + + + diff --git a/config/ChanLun_ETH_60.json b/config/ChanLun_ETH_60.json new file mode 100644 index 0000000..dd40b10 --- /dev/null +++ b/config/ChanLun_ETH_60.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.chanlun_eth_60.sqlite", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "can_short" : true, + "timeframe" : "1m", + "process_only_new_candles" : false, + "unfilledtimeout": { + "entry": 1, + "exit": 1, + "exit_timeout_count": 5, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing":{ + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8", + "secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "ETH/USDT:USDT" + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList", + "number_assets": 1, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + "telegram": { + "enabled": true, + "token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y", + "chat_id": "580807463" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8813, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d", + "ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg", + "CORS_origins": [], + "username": "freqtrader", + "password": "FreqTrade007" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 2 + } +} \ No newline at end of file diff --git a/strategies/BB9033.py b/strategies/BB9033.py index 87e922f..f3a89f7 100644 --- a/strategies/BB9033.py +++ b/strategies/BB9033.py @@ -62,7 +62,7 @@ class BB9033(IStrategy): use_custom_stoploss = True # Optimal timeframe for the strategy - time = 5 + time = 60 # Trailing stop loss trailing_stop = False lev = 1.0 diff --git a/strategies/BB90331.py b/strategies/BB90331.py index c947f88..9c29dcd 100644 --- a/strategies/BB90331.py +++ b/strategies/BB90331.py @@ -61,7 +61,7 @@ class BB90331(IStrategy): use_custom_stoploss = True # Optimal timeframe for the strategy - time =1 + time = 1 # Trailing stop loss trailing_stop = False lev = 1.0 diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py index 43fade9..7671bf1 100644 --- a/strategies/ChanLun_BTC_30.py +++ b/strategies/ChanLun_BTC_30.py @@ -26,9 +26,9 @@ logger = logging.getLogger(__name__) # freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m --pairs BTC/USDT:USDT --timerange=20250405- # freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 200 --timerange=20250201-20250401 -# sudo docker compose run --rm chan_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --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 ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies +# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies --timerange=20250721- +# sudo docker compose run --rm chanlun_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 chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies class ChanLun_BTC_30(IStrategy): INTERFACE_VERSION: int = 3 @@ -66,7 +66,7 @@ class ChanLun_BTC_30(IStrategy): } can_short = True lev = 1.0 - stoploss = -0.2 # 设置为很大的负值,让custom_stoploss来控制 + stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制 use_custom_stoploss = False # 启用自定义止损 trailing_stop = False @@ -158,9 +158,9 @@ class ChanLun_BTC_30(IStrategy): bi2 = bi_list[-2] print(bi1.start_time, bi1.end_time, bi1.dir, bi2.start_time, bi2.end_time, bi2.dir) def add_indicators(self, df): - fast = 8 - slow = 16 - period = 6 + fast = 12 + slow = 26 + period = 9 macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0) diff --git a/strategies/ChanLun_BTC_K.py b/strategies/ChanLun_BTC_K.py index e294254..ba514ef 100644 --- a/strategies/ChanLun_BTC_K.py +++ b/strategies/ChanLun_BTC_K.py @@ -63,9 +63,9 @@ class ChanLun_BTC_K(IStrategy): timeframe = '5m' # 指标参数 - macd_fast = 12 - macd_slow = 26 - macd_signal = 9 + macd_fast = 24 + macd_slow = 52 + macd_signal = 18 ema_short = 24 ema_long = 52 @@ -120,7 +120,7 @@ class ChanLun_BTC_K(IStrategy): # 金叉/死叉 df_resampled[f'macd_cross_up_{suffix}'] = (df_resampled[f'macd_{suffix}'] > df_resampled[f'macdsignal_{suffix}']) & (df_resampled[f'macd_{suffix}'].shift(1) <= df_resampled[f'macdsignal_{suffix}'].shift(1)) df_resampled[f'macd_cross_down_{suffix}'] = (df_resampled[f'macd_{suffix}'] < df_resampled[f'macdsignal_{suffix}']) & (df_resampled[f'macd_{suffix}'].shift(1) >= df_resampled[f'macdsignal_{suffix}'].shift(1)) - return df_resampled[[ + cols = [ 'date', 'close', f'macd_{suffix}', f'macdsignal_{suffix}', f'macdhist_{suffix}', @@ -128,7 +128,9 @@ class ChanLun_BTC_K(IStrategy): f'atr_{suffix}', f'atr_pct_{suffix}', f'above_zero_{suffix}', f'below_zero_{suffix}', f'near_zero_{suffix}', f'hist_increasing_{suffix}', f'hist_decreasing_{suffix}', f'high_position_{suffix}', f'macd_cross_up_{suffix}', f'macd_cross_down_{suffix}' - ]] + ] + # 确保返回独立副本,避免下游在 resampled_merge 内部触发 SettingWithCopyWarning + return df_resampled.loc[:, cols].copy() for suf, minutes in intervals.items(): df_res = resample_to_interval(dataframe, minutes) diff --git a/strategies/ChanLun_ETH_60.py b/strategies/ChanLun_ETH_60.py new file mode 100644 index 0000000..fa63b89 --- /dev/null +++ b/strategies/ChanLun_ETH_60.py @@ -0,0 +1,472 @@ +# --- Do not remove these libs --- +from statistics import median +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 ChanLun_Classifier import ChanLunClassifier +from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +from ChanPY import ChanPY +# -------------------------------- +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__) +### Now you can use logger.info('asfd') to log +# freqtrade plot-dataframe --strategy ChanLun_ETH_60 --datadir user_data/data/binance -c ./user_data/ChanLun_ETH_60.json --timerange=20250309- + +# freqtrade trade -c ./user_data/Chan/config/ChanLun_ETH_60.json --strategy ChanLun_ETH_60 --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_ETH_60.json --strategy ChanLun_ETH_60 --strategy-path ./user_data/Chan/strategies --timerange=20250712- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_ETH_60.json -t 1m --pairs ETH/USDT:USDT --timerange=20240101- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss --strategy ChanLun_ETH_60 --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_ETH_60.json -e 200 --timerange=20250201-20250401 + +# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies --timerange=20250721- +# sudo docker compose run --rm chanlun_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 chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC_30 --strategy-path ./user_data/Chan/strategies + +class ChanLun_ETH_60(IStrategy): + INTERFACE_VERSION: int = 3 + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + # 30m and 1h + + minimal_roi = { + "0": 0.15, + "360": 0.2, + "640": 0.1, + "1200": 0 + } + # 5m and 15m + minimal_roi_1 = { + "0": 0.1, + "60": 0.05, + "120": 0.02, + "240": 0 + } + # 15m and 30m + minimal_roi_1 = { + "0": 0.1, + "240": 0.05, + "480": 0.03, + "600": 0 + } + minimal_roi_1 = { + "0": 1.50, + "120": 0.05, + "240": 0.025, + "360": 0 + } + minimal_roi = { + } + can_short = True + lev = 1.0 + stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制 + use_custom_stoploss = False # 启用自定义止损 + + trailing_stop = False + trailing_stop_positive = 0.025 + trailing_stop_positive_offset = 0.045 + trailing_only_offset_is_reached = False + + # 启用仓位调整功能以支持分批止盈 + position_adjustment_enable = True + startup_candle_count = 2880 + + time5 = 5 + time15 = 15 + time30 = 30 + time60 = 60 + time4h = 240 + time30 = 60 + last_time = datetime.now() + chan = ChanLun() + chanpy = ChanPY() + classifier = ChanLunClassifier(None) + last_order = None + last_trade = None + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + # resample our dataframes + dataframe_3 = resample_to_interval(dataframe, self.get_ticker_indicator() * 3) + dataframe_5 = resample_to_interval(dataframe, self.get_ticker_indicator() * 5) + dataframe_15 = resample_to_interval(dataframe, self.get_ticker_indicator() * 15) + dataframe_30 = resample_to_interval(dataframe, self.get_ticker_indicator() * 30) + dataframe_60 = resample_to_interval(dataframe, self.get_ticker_indicator() * 60) + dataframe_4h = resample_to_interval(dataframe, self.get_ticker_indicator() * 240) + + #dataframe_1d = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d') + #dataframe_1w = resample_to_interval(dataframe_1d, self.get_ticker_indicator() * 10080) + #dataframe_1m = resample_to_interval(dataframe_1d, self.get_ticker_indicator() * 43200) + + dataframe_1d = resample_to_interval(dataframe, self.get_ticker_indicator() * 1440) + #dataframe_1w = resample_to_interval(dataframe, self.get_ticker_indicator() * 10080) + #dataframe_1m = resample_to_interval(dataframe, self.get_ticker_indicator() * 43200) + dataframe = self.add_indicators(dataframe) + dataframe_3 = self.add_indicators(dataframe_3) + dataframe_5 = self.add_indicators(dataframe_5) + dataframe_15 = self.add_indicators(dataframe_15) + dataframe_30 = self.add_indicators(dataframe_30) + dataframe_60 = self.add_indicators(dataframe_60) + dataframe_4h = self.add_indicators(dataframe_4h) + dataframe_1d = self.add_indicators(dataframe_1d) + #self.chan.plot_dual(dataframe_5, dataframe_30) + #chanpy_state = self.chanpy.get_bsp_state(dataframe_5) + #dataframe_5['chanpy_state'] = chanpy_state + state_list = self.chan.get_klc_state_list(dataframe_60) + dataframe_60['state'] = state_list + dataframe_60['fx'] = state_list + #bi_list_1 = self.chan.get_bi_list(dataframe) + #bi_list_5 = self.chan.get_bi_list(dataframe_5) + #bi_list_15 = self.chan.get_bi_list(dataframe_15) + #bi_list_30 = self.chan.get_bi_list(dataframe_30) + #bi_list_60 = self.chan.get_bi_list(dataframe_60) + if self.last_time + timedelta(minutes=1) < datetime.now(): + #self.print_bi(bi_list_1) + #self.print_bi(bi_list_5) + #self.print_bi(bi_list_15) + #self.print_bi(bi_list_30) + #self.print_bi(bi_list_60) + self.print_seg(dataframe_5) + print("-------------------------------------------------------------------------------") + self.last_time = datetime.now() + dataframe = resampled_merge(dataframe, dataframe_3) + dataframe = resampled_merge(dataframe, dataframe_5) + #dataframe = resampled_merge(dataframe, dataframe_15) + #dataframe = resampled_merge(dataframe, dataframe_30) + dataframe = resampled_merge(dataframe, dataframe_60) + #dataframe = resampled_merge(dataframe, dataframe_4h) + return dataframe + def print_seg(self, dataframe): + klc_list = self.chan.get_klc_list(dataframe) + bi_list = self.chan.cal_bi_list(klc_list) + seg_list = self.chan.get_seg_list(bi_list) + zs_list = self.chan.get_zs_list(bi_list, seg_list) + seg = seg_list[-1] + bi = bi_list[-1] + zs = zs_list[-1] + print(zs.start_time, zs.zg, zs.zd, zs.dir) + def print_bi(self, bi_list): + if bi_list and len(bi_list) > 2: + bi1 = bi_list[-1] + bi2 = bi_list[-2] + print(bi1.start_time, bi1.end_time, bi1.dir, bi2.start_time, bi2.end_time, bi2.dir) + def add_indicators(self, df): + fast = 8 + slow = 16 + period = 6 + macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) + bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) + bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0) + bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0) + bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0) + + # 计算布林带中轨(移动平均线) + bb30_middle = ta.SMA(df, timeperiod=90) + + # 手动计算布林带 %B 指标 (BBP) + # %B = (Price - Lower Band) / (Upper Band - Lower Band) + bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband']) + bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband']) + bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband']) + bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband']) + df['atr'] = ta.ATR(df, timeperiod=14) + df['bbup365'] = bb365['upperband'] + df['bblow365'] = bb365['lowerband'] + df['bbp365'] = bbp365 + df['bbup120'] = bb120['upperband'] + df['bblow120'] = bb120['lowerband'] + df['bbp120'] = bbp120 + df['bbup30'] = bb30['upperband'] + df['bblow30'] = bb30['lowerband'] + df['bbmiddle30'] = bb30_middle # 添加bb30中轨 + df['bbp30'] = bbp30 + df['bbup302'] = bb302['upperband'] + df['bblow302'] = bb302['lowerband'] + df['bbp302'] = bbp302 + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + 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) + df['rsi'] = ta.RSI(df, timeperiod=14) + df['volume_ratio'] = self.cal_volume_ratio(df) + return df + def cal_volume_ratio(self, dataframe, window=10): + df = dataframe.copy() + # 计算过去N根K线的平均成交量 + df['avg_volume'] = df['volume'].rolling(window=window).mean() + # 计算量比 + df['volume_ratio'] = df['volume'] / df['avg_volume'] + # 填充缺失值(前N根K线) + df['volume_ratio'] = df['volume_ratio'].fillna(1.0) + return df['volume_ratio'] + def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, + entry_tag: str | None, side: str, **kwargs) -> float: + new_entryprice = proposed_rate + if trade: + if trade.is_short: + new_entryprice = proposed_rate - 50 + else: + new_entryprice = proposed_rate + 50 + return new_entryprice + + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_profit: float, exit_tag: str | None, **kwargs) -> float: + new_exitprice = proposed_rate + if trade: + if trade.is_short: + new_exitprice = proposed_rate + 50 + else: + new_exitprice = proposed_rate - 50 + return new_exitprice + + def adjust_trade_position1(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: + """ + 基于布林带的分批止盈逻辑 + """ + # 获取当前数据 + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + if dataframe is None or len(dataframe) == 0: + return None + + last_candle = dataframe.iloc[-1] + + # 获取布林带数据 + bb30_middle = last_candle['bbmiddle30'] + bb30_upper = last_candle['bbup30'] + bb30_lower = last_candle['bblow30'] + bb302_upper = last_candle['bbup302'] + bb302_lower = last_candle['bblow302'] + + # 获取交易的状态标记 + first_tp_triggered = trade.get_custom_data(key="first_tp_triggered", default=False) + second_tp_triggered = trade.get_custom_data(key="second_tp_triggered", default=False) + + if trade.is_short: + # 做空逻辑 + if not first_tp_triggered and current_rate <= bb30_middle: + # 第一次止盈:价格跌到bb30中轨,止盈50% + logger.info(f"做空第一次止盈触发:价格{current_rate} <= BB30中轨{bb30_middle}") + trade.set_custom_data(key="first_tp_triggered", value=True) + trade.set_custom_data(key="new_stoploss", value=trade.open_rate) # 设置止损为开仓价 + return -(trade.amount * 0.5) # 减少50%仓位 + + elif first_tp_triggered and not second_tp_triggered and current_rate <= bb302_lower: + # 第二次止盈:继续跌到bb302下轨,止盈剩余仓位的60% + logger.info(f"做空第二次止盈触发:价格{current_rate} <= BB302下轨{bb302_lower}") + trade.set_custom_data(key="second_tp_triggered", value=True) + trade.set_custom_data(key="new_stoploss", value=bb30_middle) # 移动止损到bb30中轨 + remaining_amount = trade.amount * 0.5 # 剩余50% + return -(remaining_amount * 0.6) # 减少剩余仓位的60% + + else: + # 做多逻辑 + if not first_tp_triggered and current_rate >= bb30_middle: + # 第一次止盈:价格涨到bb30中轨,止盈50% + logger.info(f"做多第一次止盈触发:价格{current_rate} >= BB30中轨{bb30_middle}") + trade.set_custom_data(key="first_tp_triggered", value=True) + trade.set_custom_data(key="new_stoploss", value=trade.open_rate) # 设置止损为开仓价 + return -(trade.amount * 0.5) # 减少50%仓位 + + elif first_tp_triggered and not second_tp_triggered and current_rate >= bb302_upper: + # 第二次止盈:继续涨到bb302上轨,止盈剩余仓位的60% + logger.info(f"做多第二次止盈触发:价格{current_rate} >= BB302上轨{bb302_upper}") + trade.set_custom_data(key="second_tp_triggered", value=True) + trade.set_custom_data(key="new_stoploss", value=bb30_middle) # 移动止损到bb30中轨 + remaining_amount = trade.amount * 0.5 # 剩余50% + return -(remaining_amount * 0.6) # 减少剩余仓位的60% + + return None + + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float | None: + """ + 动态止损逻辑 + """ + # 检查是否有自定义的新止损价格(分批止盈后的动态止损) + new_stoploss_price = trade.get_custom_data(key="new_stoploss") + if new_stoploss_price: + logger.info(f"使用动态止损价格: {new_stoploss_price}") + return stoploss_from_absolute(new_stoploss_price, current_rate, is_short=trade.is_short) + + + # 如果没有ATR数据,使用固定的5%止损作为备用 + logger.warning(f"未找到开仓时ATR数据,使用默认5%止损") + return -0.05 + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs): + """ + 自定义退出逻辑 - 处理最终止盈条件 + """ + # 获取当前数据 + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if dataframe is None or len(dataframe) == 0: + return None + + last_candle = dataframe.iloc[-1] + + # 获取布林带数据 + bb30_upper = last_candle['bbup30'] + bb30_lower = last_candle['bblow30'] + + # 检查是否已经触发过前两次止盈 + first_tp_triggered = trade.get_custom_data(key="first_tp_triggered", default=False) + second_tp_triggered = trade.get_custom_data(key="second_tp_triggered", default=False) + + if trade.is_short: + # 做空:如果价格跌到bb30下轨,全部止盈 + if first_tp_triggered and second_tp_triggered and current_rate <= bb30_lower: + logger.info(f"做空最终止盈触发:价格{current_rate} <= BB30下轨{bb30_lower}") + return "short_final_tp_bb30_lower" + else: + # 做多:如果价格涨到bb30上轨,全部止盈 + if first_tp_triggered and second_tp_triggered and current_rate >= bb30_upper: + logger.info(f"做多最终止盈触发:价格{current_rate} >= BB30上轨{bb30_upper}") + return "long_final_tp_bb30_upper" + + # 原有退出逻辑 + if trade.is_short: + last_high = trade.get_custom_data(key="entry_candle_high") + if last_high and current_rate > last_high: + return "Relay Top FX exit" + else: + last_low = trade.get_custom_data(key="entry_candle_low") + if last_low and current_rate < last_low: + return "Relay Bottom FX exit" + + return None + + def confirm_trade_entry1(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: + if self.last_trade: + if self.last_trade.is_short: + if side == 'short': + if self.last_trade.open_date + timedelta(minutes=30) > current_time: + return False + else: + return True + else: + if side == 'long': + if self.last_trade.open_date + timedelta(minutes=30) > current_time: + return True + else: + return False + #if self.last_trade: + #print(self.last_trade.open_date, current_time, self.last_trade.open_date + timedelta(minutes=self.time5)) + return True + def custom_stoploss1(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> float | None: + + last_high = trade.get_custom_data(key="entry_candle_high") + last_low = trade.get_custom_data(key="entry_candle_low") + + # Convert absolute price to percentage relative to current_rate + if last_high: + return stoploss_from_absolute(last_high, current_rate, is_short=trade.is_short) + if last_low: + return stoploss_from_absolute(last_low, current_rate, is_short=trade.is_short) + # return maximum stoploss value, keeping current stoploss price unchanged + return None + + def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None: + """ + Called right after an order fills. + Will be called for all order types (entry, exit, stoploss, position adjustment). + :param pair: Pair for trade + :param trade: trade object. + :param order: Order object. + :param current_time: datetime object, containing the current datetime + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + # Obtain pair dataframe (just to show how to access it) + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + # 保存开仓时的ATR值用于止损计算 + if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): + entry_atr = last_candle['atr'] + trade.set_custom_data(key="entry_atr", value=entry_atr) + logger.info(f"保存开仓时ATR值: {entry_atr}") + return None + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + state_str = 'resample_{}_state'.format(self.get_ticker_indicator()*self.time30) + fx_str = 'resample_{}_fx'.format(self.get_ticker_indicator()*self.time30) + #chanpy_state_str = 'resample_{}_chanpy_state'.format(self.get_ticker_indicator()*self.time5) + shift_time = self.time30 + dataframe.loc[ + ( + (dataframe[state_str].shift(shift_time) == "-10") + #(dataframe['state'] == "-30") + #(dataframe[state_str].shift(shift_time) == "-10") + #(dataframe[fx_str].shift(shift_time) == -1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == 1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") + #(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal'])) + ), + ['enter_long', 'enter_tag']] = (1, 'long_signal_chan') + dataframe.loc[ + ( + (dataframe[state_str].shift(shift_time) == "10") + #(dataframe[fx_str].shift(shift_time) == 1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == -1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "-10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time5)].shift(self.time5) == "-10") + #(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal'])) + ), + ['enter_short', 'enter_tag']] = (1, 'short_signal_chan') + return dataframe + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + state_str = 'resample_{}_state'.format(self.get_ticker_indicator()*self.time30) + fx_str = 'resample_{}_fx'.format(self.get_ticker_indicator()*self.time30) + #chanpy_state_str = 'resample_{}_chanpy_state'.format(self.get_ticker_indicator()*self.time5) + shift_time = self.time30 + dataframe.loc[ + ( + #(dataframe['state']== "30") + (dataframe[state_str].shift(shift_time) == "10") + #(dataframe[fx_str].shift(shift_time) == 1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == -1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") + ), + ['exit_long', 'exit_tag']] = (1, 'long_close_signal_chan') + dataframe.loc[ + ( + #(dataframe['state']== "30") + (dataframe[state_str].shift(shift_time) == "-10") + #(dataframe[fx_str].shift(shift_time) == -1) + #(dataframe[chanpy_state_str].shift(shift_time+30) == 1) + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time30)] == "10") & + #(dataframe['resample_{}_state'.format(self.get_ticker_indicator()*self.time60)] == "10") + ), + ['exit_short', 'exit_tag']] = (1, 'short_close_signal_chan') + 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 self.lev + + def get_ticker_indicator(self): + return int(self.timeframe[:-1]) \ No newline at end of file diff --git a/strategies/ChanMomentumBayes.json b/strategies/ChanMomentumBayes.json new file mode 100644 index 0000000..2dd0814 --- /dev/null +++ b/strategies/ChanMomentumBayes.json @@ -0,0 +1,28 @@ +{ + "strategy_name": "ChanMomentumBayes", + "params": { + "max_open_trades": { + "max_open_trades": 1 + }, + "buy": {}, + "sell": {}, + "protection": {}, + "roi": { + "0": 0.395, + "41": 0.128, + "206": 0.034, + "555": 0 + }, + "stoploss": { + "stoploss": -0.345 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.219, + "trailing_stop_positive_offset": 0.229, + "trailing_only_offset_is_reached": false + } + }, + "ft_stratparam_v": 1, + "export_time": "2025-08-09 09:13:05.692223+00:00" +} \ No newline at end of file diff --git a/strategies/ChanMomentumBayes.py b/strategies/ChanMomentumBayes.py new file mode 100644 index 0000000..eb17f10 --- /dev/null +++ b/strategies/ChanMomentumBayes.py @@ -0,0 +1,566 @@ +from __future__ import annotations + +from freqtrade.strategy import IStrategy +from technical.util import resample_to_interval, resampled_merge +from pandas import DataFrame +import pandas as pd +from functools import reduce +from typing import Dict, Tuple +import talib.abstract as ta +import numpy as np + + +class MomentumConcepts: + """ + 将《K线动能理论》的关键概念实现为可复用的判定函数。 + 输出以列的形式添加到 DataFrame,列名统一为 concept_*。 + """ + + def __init__(self, macd_fast: int = 12, macd_slow: int = 26, macd_signal: int = 9, + ema_short: int = 24, ema_long: int = 52) -> None: + self.macd_fast = macd_fast + self.macd_slow = macd_slow + self.macd_signal = macd_signal + self.ema_short = ema_short + self.ema_long = ema_long + + def _ensure_indicators(self, df: DataFrame) -> DataFrame: + macd = ta.MACD(df, fastperiod=self.macd_fast, slowperiod=self.macd_slow, signalperiod=self.macd_signal) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + df['ema_24'] = ta.EMA(df, timeperiod=self.ema_short) + df['ema_52'] = ta.EMA(df, timeperiod=self.ema_long) + df['atr_14'] = ta.ATR(df, timeperiod=14) + # 基础量化 + df['macd_abs'] = np.abs(df['macd']) + df['macdsignal_abs'] = np.abs(df['macdsignal']) + df['zero_dist'] = np.sqrt(np.square(df['macd']) + np.square(df['macdsignal'])) + df['zero_dist_ema'] = df['zero_dist'].ewm(span=50, adjust=False).mean() + df['zero_eps'] = (df['zero_dist_ema'] * 0.2).clip(lower=1e-8) + df['histogram_decreasing'] = df['macdhist'] < df['macdhist'].shift(1) + df['histogram_increasing'] = df['macdhist'] > df['macdhist'].shift(1) + df['above_zero'] = (df['macd'] > 0) & (df['macdsignal'] > 0) + df['below_zero'] = (df['macd'] < 0) & (df['macdsignal'] < 0) + return df + + def compute_all_features(self, df: DataFrame) -> DataFrame: + df = self._ensure_indicators(df.copy()) + + # 归零轴 | 高位 | 离开/穿越零轴 + # “归零轴”的两种情形:价格触碰EMA52;或快线DIF无限接近零轴(文档1-6,21-27) + df['concept_price_touch_ema52'] = (np.abs(df['close'] - df['ema_52']) / df['ema_52'] < 0.005) + df['concept_fastline_near_zero'] = (df['macd_abs'] < df['zero_eps']) + df['concept_near_zero'] = (df['macd_abs'] < df['zero_eps']) & (df['macdsignal_abs'] < df['zero_eps']) + df['concept_zero_axis_return'] = df['concept_price_touch_ema52'] | df['concept_fastline_near_zero'] + # 价格先触EMA52而黄白线尚未归零轴 + df['concept_zero_touch_price_first'] = df['concept_price_touch_ema52'] & (~df['concept_near_zero']) + df['concept_high_position'] = df['zero_dist'] > (df['zero_dist_ema'] * 1.5) + df['concept_cross_zero_up'] = ( + ((df['macd'].shift(1) <= 0) & (df['macd'] > 0)) | + ((df['macdsignal'].shift(1) <= 0) & (df['macdsignal'] > 0)) + ) + df['concept_cross_zero_down'] = ( + ((df['macd'].shift(1) >= 0) & (df['macd'] < 0)) | + ((df['macdsignal'].shift(1) >= 0) & (df['macdsignal'] < 0)) + ) + df['concept_leave_zero'] = (df['concept_cross_zero_up'] | df['concept_cross_zero_down']) & df['histogram_increasing'] + + # “有效穿越零轴/EMA52”(文档10-13,23-27):当前穿越,且下一根仍保持在同侧 + def _effective_break(price: DataFrame, ma: DataFrame, direction: str) -> DataFrame: + if direction == 'down': + cross_now = (price < ma) & (price.shift(1) >= ma.shift(1)) + hold_next = price.shift(-1) < ma.shift(-1) + else: + cross_now = (price > ma) & (price.shift(1) <= ma.shift(1)) + hold_next = price.shift(-1) > ma.shift(-1) + return (cross_now & hold_next).fillna(False) + + df['concept_effective_break_ema52_down'] = _effective_break(df['close'], df['ema_52'], 'down') + df['concept_effective_break_ema52_up'] = _effective_break(df['close'], df['ema_52'], 'up') + + def _effective_cross_zero(series: DataFrame, direction: str) -> DataFrame: + if direction == 'down': + cross_now = (series < 0) & (series.shift(1) >= 0) + hold_next = series.shift(-1) < 0 + else: + cross_now = (series > 0) & (series.shift(1) <= 0) + hold_next = series.shift(-1) > 0 + return (cross_now & hold_next).fillna(False) + + df['concept_effective_dea_cross_down'] = _effective_cross_zero(df['macdsignal'], 'down') + df['concept_effective_dea_cross_up'] = _effective_cross_zero(df['macdsignal'], 'up') + df['concept_cross_zero_effective_down'] = df['concept_effective_break_ema52_down'] & df['concept_effective_dea_cross_down'] + df['concept_cross_zero_effective_up'] = df['concept_effective_break_ema52_up'] & df['concept_effective_dea_cross_up'] + + # 高位空(高位 + 能量柱衰减 + 黄白线横盘 + 价格横盘) + df['macd_flat_3'] = (np.abs(df['macd'] - df['macd'].shift(3)) < df['zero_eps']) + df['macdsignal_flat_3'] = (np.abs(df['macdsignal'] - df['macdsignal'].shift(3)) < df['zero_eps']) + df['concept_high_position_empty'] = ( + df['concept_high_position'] & df['histogram_decreasing'] & + (np.abs(df['close'] - df['close'].shift(3)) / df['close'].shift(3) < 0.02) & + df['macd_flat_3'] & df['macdsignal_flat_3'] + ) + + # 隐形形态(无能量配合,文档32-51) + # 高位隐形:远离零轴的高位价格延续而能量未同向释放 → 归零轴/穿零轴需求 + df['concept_hidden_high'] = ( + df['concept_high_position'] & ( + ((df['close'] > df['close'].shift(1)) & (df['macdhist'] <= 0)) | + ((df['close'] < df['close'].shift(1)) & (df['macdhist'] >= 0)) + ) + ) + # 归零轴隐形:零轴支撑期应有正向能量却无 → 后续易穿零 + df['concept_hidden_zero'] = ( + df['concept_zero_axis_return'] & (np.abs(df['close'] - df['ema_52']) / df['ema_52'] < 0.01) & (df['macdhist'] <= 0) + ) + # 兼容旧列名(用于卖出特征列表) + df['concept_hidden_high_bull'] = df['concept_hidden_high'] + df['concept_hidden_zero_bull_fail'] = df['concept_hidden_zero'] + + # 顶/底分型 + df['concept_fractal_top'] = (df['high'] > df['high'].shift(1)) & (df['high'] > df['high'].shift(-1)) + df['concept_fractal_bottom'] = (df['low'] < df['low'].shift(1)) & (df['low'] < df['low'].shift(-1)) + + # 强势/超强势/弱势结构(修正:强调“趋于归零轴”,而非“已在零轴附近”) + # 零轴距离连续收敛,仍在高位,尚未到近零 + df['zero_dist_decr_3'] = ( + (df['zero_dist'] < df['zero_dist'].shift(1)) & + (df['zero_dist'].shift(1) < df['zero_dist'].shift(2)) & + (df['zero_dist'].shift(2) < df['zero_dist'].shift(3)) + ) + highpos_recent = df['concept_high_position'].rolling(8, min_periods=1).max() > 0 + price_sideways_3 = (np.abs(df['close'] - df['close'].shift(3)) / df['close'].shift(3) < 0.02) + ema24_slope_up = df['ema_24'] > df['ema_24'].shift(3) + near_ema24 = (np.abs(df['close'] - df['ema_24']) / df['ema_24'] < 0.01) + + # 强势结构:高位横盘 + MACD黄白线趋于零轴(距离收敛)+ 仍在零轴上方,且未到近零 + df['concept_trend_strong'] = ( + (df['close'] >= df['ema_24']) & price_sideways_3 & highpos_recent & df['zero_dist_decr_3'] & df['above_zero'] & (~df['concept_near_zero']) + ) + # 超强势结构:贴近EMA24缓慢上行 + 趋于零轴(距离收敛)+ 仍未到近零,倾向于破前高 + df['concept_trend_super_strong'] = ( + (df['close'] > df['ema_24']) & near_ema24 & ema24_slope_up & df['zero_dist_decr_3'] & (~df['concept_near_zero']) + ) + # 弱势结构:价格快速靠近/跌至EMA52附近,同时MACD距离快速收敛(可接近近零) + zero_dist_fast_drop = (df['zero_dist'] < df['zero_dist'].shift(1)) & (df['zero_dist'].shift(1) < df['zero_dist'].shift(2)) + df['concept_trend_weak'] = ( + (df['close'] <= df['ema_52']) & zero_dist_fast_drop + ) + + # 强势反弹触发:强势结构之后,黄白线真正“归零轴”触发有效反弹(文档53-56) + strong_ctx = df['concept_trend_strong'].rolling(5, min_periods=1).max() > 0 + df['concept_strong_rebound_trigger'] = strong_ctx & df['concept_near_zero'] & (df['close'] >= df['ema_24']) + # 超强势破高触发:超强势结构后,归零轴并突破近期高点(速度快、力度强且破前高) + prev_high_10 = df['close'].rolling(10).max().shift(1) + df['concept_super_strong_break_trigger'] = (df['concept_trend_super_strong'].rolling(8, min_periods=1).max() > 0) & df['concept_near_zero'] & (df['close'] > prev_high_10) + + # 线段(以慢线穿零轴划分)与单位调整周期(near_zero触发 id) + seg_change = ( + ((df['macdsignal'] <= 0) & (df['macdsignal'].shift(1) > 0)) | + ((df['macdsignal'] >= 0) & (df['macdsignal'].shift(1) < 0)) + ) + df['concept_segment_id'] = seg_change.cumsum().fillna(0).astype(int) + df['concept_unit_cycle_id'] = (df['concept_near_zero'].astype(int).diff().fillna(0) > 0).cumsum().astype(int) + # 当前线段内的第一个单位周期(用于限制“最佳买卖点”发生在首次离零后的周期) + seg_first_cycle = df.groupby('concept_segment_id')['concept_unit_cycle_id'].transform('min') + df['first_cycle_in_segment'] = (df['concept_unit_cycle_id'] == seg_first_cycle).fillna(False) + # 当前线段内的第二个单位周期 + def _second_cycle_id(s): + uniq = np.sort(s.unique()) + return uniq[1] if len(uniq) > 1 else np.nan + seg_second_cycle_id = df.groupby('concept_segment_id')['concept_unit_cycle_id'].transform(_second_cycle_id) + df['second_cycle_in_segment'] = (df['concept_unit_cycle_id'] == seg_second_cycle_id).fillna(False) + # 当前线段内的第三个单位周期 + def _third_cycle_id(s): + uniq = np.sort(s.unique()) + return uniq[2] if len(uniq) > 2 else np.nan + seg_third_cycle_id = df.groupby('concept_segment_id')['concept_unit_cycle_id'].transform(_third_cycle_id) + df['third_cycle_in_segment'] = (df['concept_unit_cycle_id'] == seg_third_cycle_id).fillna(False) + + # 零轴粘合 / 倒挂(文档67-75,77-79) + small_lines = (df['macd_abs'] < df['zero_eps'] * 1.2) & (df['macdsignal_abs'] < df['zero_eps'] * 1.2) + same_side_hist = ( + ((df['macdhist'] >= 0) & df['concept_cross_zero_up']) | + ((df['macdhist'] <= 0) & df['concept_cross_zero_down']) + ) + df['concept_zero_adhesion'] = small_lines & same_side_hist + macd_cross_event = ( + ((df['macd'] > df['macdsignal']) & (df['macd'].shift(1) <= df['macdsignal'].shift(1))) | + ((df['macd'] < df['macdsignal']) & (df['macd'].shift(1) >= df['macdsignal'].shift(1))) + ) + hist_flip = np.sign(df['macdhist']) != np.sign(df['macdhist'].shift(1)) + df['concept_zero_inverted'] = df['concept_near_zero'] & df['histogram_decreasing'] & macd_cross_event & hist_flip + # 零轴缠绕/纠缠:近零或小幅上下缠绕,代表本级别调整结束倾向(文档28-31) + small_hist = df['macdhist'].abs() < df['zero_eps'] + near_or_flip_small = (df['concept_near_zero'] | (small_hist & (df['macd_abs'] < df['zero_eps'] * 1.5) & (df['macdsignal_abs'] < df['zero_eps'] * 1.5))) + df['concept_zero_entanglement'] = (near_or_flip_small.rolling(5, min_periods=1).max() > 0) + + # 顶/底背离(线段内比较) + df['concept_top_div'] = False + df['concept_bottom_div'] = False + g = df.groupby('concept_segment_id', group_keys=False) + seg_close_max_prev = g['close'].apply(lambda s: s.cummax().shift(1)) + seg_macd_max_prev = g['macd'].apply(lambda s: s.cummax().shift(1)) + seg_close_min_prev = g['close'].apply(lambda s: s.cummin().shift(1)) + seg_macd_min_prev = g['macd'].apply(lambda s: s.cummin().shift(1)) + cond_top = (df['close'] > seg_close_max_prev) & (df['macd'] < seg_macd_max_prev) & df['above_zero'] + cond_bottom = (df['close'] < seg_close_min_prev) & (df['macd'] > seg_macd_min_prev) & df['below_zero'] + df.loc[cond_top.fillna(False), 'concept_top_div'] = True + df.loc[cond_bottom.fillna(False), 'concept_bottom_div'] = True + + # 跳空/分立跳空(文档101-139,近似实现) + df['concept_continuous_gap'] = False + df['concept_separate_gap'] = False + hist_within = df['macdhist'].abs() <= np.maximum(df['macd_abs'], df['macdsignal_abs']) + for i in range(6, len(df)): + # 连续跳空:单位周期内,能量衰减→转增,能量柱包含在黄白线之内,且未放出反向能量 + decr_then_incr = (df['macdhist'].iloc[i-4:i-1].diff().dropna() < 0).all() and (df['macdhist'].iloc[i-1:i+1].diff().dropna() > 0).all() + within_lines = hist_within.iloc[i-3:i+1].all() + no_opposite = not ((df['macdhist'].iloc[i-6:i] < 0).any() and (df['macdhist'].iloc[i] > 0)) and not ((df['macdhist'].iloc[i-6:i] > 0).any() and (df['macdhist'].iloc[i] < 0)) + same_cycle = df['concept_unit_cycle_id'].iloc[i] == df['concept_unit_cycle_id'].iloc[i-3] + if decr_then_incr and within_lines and no_opposite and same_cycle: + df.iloc[i, df.columns.get_loc('concept_continuous_gap')] = True + # 分立跳空:同向能量堆之间被反向能量隔开,再次出现同向能量堆且处高位 + recent = df['macdhist'].iloc[i-12:i+1] + if len(recent) >= 8: + pos = (recent > 0).astype(int) + neg = (recent < 0).astype(int) + has_pos_sep = (pos.diff().abs().sum() >= 2) and (recent.iloc[-1] > 0) + has_neg_sep = (neg.diff().abs().sum() >= 2) and (recent.iloc[-1] < 0) + if (has_pos_sep or has_neg_sep) and df['concept_high_position'].iloc[i]: + df.iloc[i, df.columns.get_loc('concept_separate_gap')] = True + + # 最佳买卖点(原文:单位周期之内 + 隐形 + 分立跳空 + 背离 + 黄白线高位空) + # 在分立跳空计算之后再做周期聚合,避免列不存在 + cycle_id = df['concept_unit_cycle_id'] + cycle_hidden_high = df['concept_hidden_high'].groupby(cycle_id).cummax().fillna(False).astype(bool) + cycle_hidden_zero = df['concept_hidden_zero'].groupby(cycle_id).cummax().fillna(False).astype(bool) + cycle_sep_gap = df['concept_separate_gap'].groupby(cycle_id).cummax().fillna(False).astype(bool) + cycle_top_div = df['concept_top_div'].groupby(cycle_id).cummax().fillna(False).astype(bool) + cycle_bottom_div = df['concept_bottom_div'].groupby(cycle_id).cummax().fillna(False).astype(bool) + hpe_above_series = (df['concept_high_position_empty'] & df['above_zero']).astype(bool) + hpe_below_series = (df['concept_high_position_empty'] & df['below_zero']).astype(bool) + cycle_hpe_above = hpe_above_series.groupby(cycle_id).cummax().fillna(False).astype(bool) + cycle_hpe_below = hpe_below_series.groupby(cycle_id).cummax().fillna(False).astype(bool) + + df['concept_best_sell_by_doc'] = (cycle_hidden_high & cycle_sep_gap & cycle_top_div & cycle_hpe_above) + df['concept_best_buy_by_doc'] = (cycle_hidden_zero & cycle_sep_gap & cycle_bottom_div & cycle_hpe_below) + + # 连续背离:单位周期内,价格与能量柱方向反复背离(粗略识别:近零后两次及以上背离触发) + g2 = df.groupby('concept_unit_cycle_id') + df['concept_divergence_series'] = (g2['concept_top_div'].transform('sum') + g2['concept_bottom_div'].transform('sum')) >= 2 + + # 单位周期之间的背离 / 线段背离(粗略量化) + df['concept_cycle_between_div'] = False + df['concept_segment_between_div'] = False + # 周期间离开零轴距离比较 + cycle_peak = g2['zero_dist'].transform('max') + df['concept_cycle_between_div'] = (cycle_peak < cycle_peak.shift(1)) & (df['concept_unit_cycle_id'] == df['concept_unit_cycle_id']) + # 线段之间:比较两个线段 DIF 峰值 + seg_peak = g['macd'].transform('max') + df['concept_segment_between_div'] = (seg_peak < seg_peak.shift(1)) + + # 动能不足(价格不破新高/新低 + 能量衰减) + df['concept_momentum_lack_up'] = (df['close'] <= df['close'].shift(1)) & df['histogram_decreasing'] & df['above_zero'] + df['concept_momentum_lack_down'] = (df['close'] >= df['close'].shift(1)) & df['histogram_decreasing'] & df['below_zero'] + + # 底部形态四阶段 & V字反转(近似) + df['concept_bottom_phase1'] = df['below_zero'] & (df['close'] < df['close'].shift(3)) + df['concept_bottom_phase2'] = df['concept_near_zero'] & (df['close'] > df['close'].shift(1)) + df['concept_bottom_phase3'] = df['concept_bottom_div'] | df['concept_momentum_lack_down'] + df['concept_bottom_phase4'] = df['concept_zero_adhesion'] | (df['concept_near_zero'] & (np.sign(df['macd']) != np.sign(df['macd'].shift(1)))) + price_break = df['close'] > df['close'].rolling(10).max().shift(1) + macd_converge = df['histogram_decreasing'].rolling(4).sum() >= 3 + df['concept_v_reversal'] = df['concept_near_zero'] & (df['close'] >= df['ema_52']) & macd_converge & price_break + + # 最佳买/卖点(组合特征) + df['concept_best_buy'] = ( + df['concept_bottom_div'] | + df['concept_v_reversal'] | + (df['concept_zero_adhesion'] & (df['close'] >= df['ema_52'])) | + (df['concept_zero_axis_return'] & df['concept_trend_strong']) + ) + df['concept_best_sell'] = ( + df['concept_top_div'] | df['concept_high_position_empty'] | df['concept_zero_inverted'] | df['concept_separate_gap'] + ) + + return df + + +class ChanMomentumBayes(IStrategy): + INTERFACE_VERSION: int = 3 + + # 以15分钟为基础K线,仅合并上级周期30m/60m + timeframe = '15m' + minimal_roi = {"0": 0.004, "30": 0.006, "120": 0.01} + stoploss = -0.015 + trailing_stop = True + trailing_stop_positive = 0.003 + trailing_stop_positive_offset = 0.006 + trailing_only_offset_is_reached = True + position_adjustment_enable = True + + macd_fast = 24 + macd_slow = 52 + macd_signal = 18 + ema_short = 24 + ema_long = 52 + + # 贝叶斯阈值(放宽买入阈值) + buy_threshold = 0.65 + sell_threshold = 0.55 + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + concepts = MomentumConcepts(self.macd_fast, self.macd_slow, self.macd_signal, self.ema_short, self.ema_long) + df = concepts.compute_all_features(dataframe) + + # 多时间周期重采样:次周期 3m/5m,高周期 30m/60m(基于15m交易) + def build_htf(src: DataFrame, minutes: int, suffix: str) -> DataFrame: + dfr = resample_to_interval(src, minutes) + macd_htf = ta.MACD(dfr, fastperiod=self.macd_fast, slowperiod=self.macd_slow, signalperiod=self.macd_signal) + dfr[f'macd_{suffix}'] = macd_htf['macd'] + dfr[f'macdsignal_{suffix}'] = macd_htf['macdsignal'] + dfr[f'macdhist_{suffix}'] = macd_htf['macdhist'] + dfr[f'ema_24_{suffix}'] = ta.EMA(dfr, timeperiod=self.ema_short) + dfr[f'ema_52_{suffix}'] = ta.EMA(dfr, timeperiod=self.ema_long) + # 动态零轴度量 + zero_dist = np.sqrt(np.square(dfr[f'macd_{suffix}']) + np.square(dfr[f'macdsignal_{suffix}'])) + zero_ema = zero_dist.ewm(span=50, adjust=False).mean() + zero_eps = (zero_ema * 0.2).clip(lower=1e-8) + dfr[f'above_zero_{suffix}'] = (dfr[f'macd_{suffix}'] > 0) & (dfr[f'macdsignal_{suffix}'] > 0) + dfr[f'below_zero_{suffix}'] = (dfr[f'macd_{suffix}'] < 0) & (dfr[f'macdsignal_{suffix}'] < 0) + dfr[f'near_zero_{suffix}'] = (np.abs(dfr[f'macd_{suffix}']) < zero_eps) & (np.abs(dfr[f'macdsignal_{suffix}']) < zero_eps) + dfr[f'hist_decr_{suffix}'] = dfr[f'macdhist_{suffix}'] < dfr[f'macdhist_{suffix}'].shift(1) + dfr[f'high_position_{suffix}'] = zero_dist > (zero_ema * 1.5) + return dfr[['date', f'macd_{suffix}', f'macdsignal_{suffix}', f'macdhist_{suffix}', f'ema_24_{suffix}', f'ema_52_{suffix}', + f'above_zero_{suffix}', f'below_zero_{suffix}', f'near_zero_{suffix}', f'hist_decr_{suffix}', f'high_position_{suffix}']].copy() + + # 合并上级周期特征(resampled_merge会添加列前缀 resample_{minutes}_) + for minutes, suf in [(30, 'x30'), (60, 'x60')]: + htf = build_htf(df, minutes, suf) + df = resampled_merge(df, htf) + + # 60m均线金叉(EMA24由下穿上EMA52) + ema24_60 = df.get('resample_60_ema_24_x60', None) + ema52_60 = df.get('resample_60_ema_52_x60', None) + if ema24_60 is not None and ema52_60 is not None: + df['resample_60_ema_bull_cross'] = ( + (ema24_60 >= ema52_60) & (ema24_60.shift(1) < ema52_60.shift(1)) + ).fillna(False) + else: + df['resample_60_ema_bull_cross'] = False + + # 30m对齐窗口:仅在新30m的前15分钟(即第一根15mK线)允许入场 + d30 = df.get('resample_30_date') + df['is_new_30m'] = d30.ne(d30.shift(1)).fillna(False) + # 新30m的前30分钟窗口(覆盖第一根与第二根15m) + if 'date' in df.columns and d30 is not None: + dt_delta_30 = (df['date'] - d30) + df['within_30m_first30m'] = dt_delta_30.dt.total_seconds().div(60).between(0, 30).fillna(False) + else: + df['within_30m_first30m'] = df['is_new_30m'] + # 60m窗口:新60m的前30分钟 + d60 = df.get('resample_60_date') + df['is_new_60m'] = d60.ne(d60.shift(1)).fillna(False) if d60 is not None else False + if 'date' in df.columns and d60 is not None: + dt_delta_60 = (df['date'] - d60) + df['within_60m_first30m'] = dt_delta_60.dt.total_seconds().div(60).between(0, 30).fillna(False) + else: + df['within_60m_first30m'] = df['is_new_60m'] + + # 高位空多周期确认(加强:要求60m能量柱走弱) + # 15m本级出现高位空 + 30m高位且能量衰减 + 60m多头或高位 + 次级别(5m/3m)出现近零或能量衰减 + df['concept_high_position_empty_mtf'] = ( + df.get('concept_high_position_empty', False) & + df.get('resample_30_high_position_x30', False) & df.get('resample_30_hist_decr_x30', False) & + (df.get('resample_60_above_zero_x60', False) | df.get('resample_60_high_position_x60', False)) & + df.get('resample_60_hist_decr_x60', False) & + (df.get('resample_5_near_zero_x5', False) | df.get('resample_5_hist_decr_x5', False)) + ).fillna(False) + + # 简化版:按零轴侧别区分“高位空”方向性(上方看空、下方看多) + df['concept_hpe_above'] = df['concept_high_position_empty'] & df['above_zero'] + df['concept_hpe_below'] = df['concept_high_position_empty'] & df['below_zero'] + # 上级周期近似HPE(高位 + 直方图衰减),并区分上下零轴 + for m, s in [(30, 'x30'), (60, 'x60')]: + df[f'resample_{m}_hpe_{s}'] = df.get(f'resample_{m}_high_position_{s}', False) & df.get(f'resample_{m}_hist_decr_{s}', False) + df[f'resample_{m}_hpe_above_{s}'] = df.get(f'resample_{m}_hpe_{s}', False) & df.get(f'resample_{m}_above_zero_{s}', False) + df[f'resample_{m}_hpe_below_{s}'] = df.get(f'resample_{m}_hpe_{s}', False) & df.get(f'resample_{m}_below_zero_{s}', False) + + # 1小时门控:买入弱势背景(<=0, ema24<=ema52, 30m收敛);卖出强势背景(>=0, ema24>=ema52, 30m衰减) + def as_bool_series(val): + if val is None or isinstance(val, (bool, int, float)): + return pd.Series(False, index=df.index) + return val.fillna(False).astype(bool) + + s60_hist_decr = as_bool_series(df.get('resample_60_hist_decr_x60')) + s60_near_zero = as_bool_series(df.get('resample_60_near_zero_x60')) + s30_hist_decr = as_bool_series(df.get('resample_30_hist_decr_x30')) + s30_near_zero = as_bool_series(df.get('resample_30_near_zero_x30')) + # 30m 直方图是否增加,用于定义“走平/不增” + s30_hist = df.get('resample_30_macdhist_x30', pd.Series(np.nan, index=df.index)) + s30_hist_incr = (s30_hist > s30_hist.shift(1)).fillna(False) + # 买入门控(弱势背景) + df['htf_buy_gate'] = ( + (df.get('resample_60_ema_24_x60', np.nan) <= df.get('resample_60_ema_52_x60', np.nan)) & + (df.get('resample_60_macdsignal_x60', np.nan) <= 0) & + (s30_near_zero | (~s30_hist_incr)) + ).fillna(False) + # 卖出门控(强势背景) + df['htf_sell_gate'] = ( + (df.get('resample_60_ema_24_x60', np.nan) >= df.get('resample_60_ema_52_x60', np.nan)) & + ((df.get('resample_60_macdsignal_x60', np.nan) >= 0) | s60_near_zero) & + (s30_hist_decr | (~s30_hist_incr)) + ).fillna(False) + + # 波动过滤(ATR百分比)与均线邻近(放宽入场) + df['atr_pct'] = (df['atr_14'] / df['close']).fillna(0) + df['vol_ok'] = (df['atr_pct'] > 0.0005) + df['near_ema24'] = (np.abs(df['close'] - df['ema_24']) / df['ema_24'] < 0.002).fillna(False) + df['near_ema52'] = (np.abs(df['close'] - df['ema_52']) / df['ema_52'] < 0.002).fillna(False) + # 简化:贝叶斯/事件链关闭 + self._buy_features = [] + self._sell_features = [] + + return df + + def _naive_bayes(self, row: Dict[str, float], feature_list: Tuple[str, ...], prior: float = 0.5, + p_true_given_class: float = 0.7, p_true_given_not: float = 0.3) -> float: + """ + 朴素贝叶斯:假设各概念在类条件下相互独立。 + - feature=True 时,使用 P(feature|Class)=p_true_given_class;False 时用 1-p_true_given_class。 + - 非该类时对应为 p_true_given_not。 + 返回后验 P(Class|features)。 + """ + # 使用对数似然避免下溢 + log_p_class = np.log(prior) + log_p_not = np.log(1 - prior) + for f in feature_list: + v = bool(row.get(f, False)) + if v: + log_p_class += np.log(p_true_given_class) + log_p_not += np.log(p_true_given_not) + else: + log_p_class += np.log(1 - p_true_given_class) + log_p_not += np.log(1 - p_true_given_not) + # 归一化 + max_log = max(log_p_class, log_p_not) + p_class = np.exp(log_p_class - max_log) + p_not = np.exp(log_p_not - max_log) + return float(p_class / (p_class + p_not)) + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + pair = metadata.get('pair', '') + is_eth = pair.startswith('ETH/') or pair.startswith('ETH:') + # 使用原文“最佳买点”简化:单位周期之内 + 隐形(零轴)+ 分立跳空 + 底背离 + 黄白线高位空(零轴下) + # 门控:30/60m非上方HPE强压制 + no_htf_bear = ~(dataframe.get('resample_60_hpe_above_x60', False) | dataframe.get('resample_30_hpe_above_x30', False)) + price_dev_ema52 = (np.abs(dataframe['close'] - dataframe['ema_52']) / dataframe['ema_52']) + price_ok = (price_dev_ema52 < 0.04) + # 允许发生在当前线段的第1或第2个单位周期,且买入时要求本级零轴下方(同向一致) + first_cycle = ( + dataframe.get('first_cycle_in_segment', False) | + dataframe.get('second_cycle_in_segment', False) + ) + below_zero_now = dataframe['below_zero'] + # 兜底分支:底背离 + 60m near_zero + ATR/EMA过滤 + atr_pct = (dataframe['atr_14'] / dataframe['close']).fillna(0) + ema24_up = dataframe['ema_24'] > dataframe['ema_24'].shift(1) + fallback_buy = dataframe.get('concept_bottom_div', False) & dataframe.get('resample_60_near_zero_x60', False) & (atr_pct > (0.01 if is_eth else 0.007)) & ema24_up + # 穿越确认:本级 macd 与 macdsignal 金叉(或从负转平)后第1根 + golden_cross = ((dataframe['macd'] > dataframe['macdsignal']) & (dataframe['macd'].shift(1) <= dataframe['macdsignal'].shift(1))).fillna(False) + macdsignal_flat_up = ((dataframe['macdsignal'] >= 0) & (dataframe['macdsignal'].shift(1) < 0)).fillna(False) + cross_now = golden_cross | macdsignal_flat_up + cross_ok = cross_now | cross_now.shift(1).fillna(False) + # 价格贴均线约束 + near_ema24 = (np.abs(dataframe['close'] - dataframe['ema_24']) / dataframe['ema_24'] < 0.03).fillna(False) + near_ema52 = (np.abs(dataframe['close'] - dataframe['ema_52']) / dataframe['ema_52'] < 0.03).fillna(False) + # 价格贴近或站上EMA24(保持EMA52约束不变) + near_ma = near_ema24 | near_ema52 | (dataframe['close'] >= dataframe['ema_24']) + # 入场窗口:新30m或新60m前30分钟 + in_window = dataframe.get('within_30m_first30m', False) | dataframe.get('within_60m_first30m', False) + # ETH额外门控 + 更严格的价格偏离 + price_ok_eff = price_ok & (~is_eth | (price_dev_ema52 < 0.02)) + eth_gate = (~is_eth) | ( + dataframe.get('resample_60_near_zero_x60', False) & + dataframe.get('resample_30_near_zero_x30', False) & + (dataframe.get('resample_60_ema_bull_cross', False) | dataframe.get('resample_60_ema_24_x60', 0) >= dataframe.get('resample_60_ema_52_x60', 0)) + ) + buy_cond = (dataframe.get('concept_best_buy_by_doc', False) | fallback_buy) & eth_gate & no_htf_bear & price_ok_eff & first_cycle & below_zero_now & cross_ok & near_ma & in_window + dataframe.loc[buy_cond & dataframe['htf_buy_gate'], 'enter_long'] = 1 + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # 使用原文“最佳卖点”简化:单位周期之内 + 隐形(高位)+ 分立跳空 + 顶背离 + 黄白线高位空(零轴上) + # 或风险触发(跌破EMA52/零轴倒挂/HTF强压制) + htf_bear = (dataframe.get('resample_60_hpe_above_x60', False) | dataframe.get('resample_30_hpe_above_x30', False)) + # 卖出同理:发生在首个单位周期,且处于零轴上方(同向一致) + first_cycle = dataframe.get('first_cycle_in_segment', False) | dataframe.get('second_cycle_in_segment', False) + above_zero_now = dataframe['above_zero'] + # 穿越确认:死叉或从正转平 + dead_cross = ((dataframe['macd'] < dataframe['macdsignal']) & (dataframe['macd'].shift(1) >= dataframe['macdsignal'].shift(1))).fillna(False) + macdsignal_flat_down = ((dataframe['macdsignal'] <= 0) & (dataframe['macdsignal'].shift(1) > 0)).fillna(False) + cross_now_s = dead_cross | macdsignal_flat_down + cross_ok_s = cross_now_s | cross_now_s.shift(1).fillna(False) + # 退出窗口:新30m或新60m前30分钟 + in_window = dataframe.get('within_30m_first30m', False) | dataframe.get('within_60m_first30m', False) + # 卖出形态“二选一” + 跌破EMA24 + pattern_two = (dataframe.get('concept_top_div', False) | dataframe.get('concept_high_position_empty', False)) + price_break_ema24 = (dataframe['close'] < dataframe['ema_24']) + sell_doc = dataframe.get('concept_best_sell_by_doc', False) & first_cycle & above_zero_now & cross_ok_s & in_window & pattern_two & price_break_ema24 + risk_exit = (dataframe['close'] < dataframe['ema_52']) | dataframe.get('concept_zero_inverted', False) | htf_bear + dataframe.loc[(sell_doc & dataframe['htf_sell_gate']) | risk_exit, 'exit_long'] = 1 + return dataframe + + @property + def protections(self): + return [ + { + "method": "CooldownPeriod", + "stop_duration_candles": 8, + }, + { + "method": "MaxDrawdown", + "lookback_period_candles": 96, + "trade_limit": 20, + "stop_duration_candles": 24, + "max_allowed_drawdown": 0.2, + "only_per_pair": False, + }, + { + "method": "StoplossGuard", + "lookback_period_candles": 24, + "trade_limit": 1, + "stop_duration_candles": 24, + "only_per_pair": False, + "only_per_side": False, + }, + ] + + def custom_stoploss(self, pair: str, trade, current_time, current_rate: float, current_profit: float, **kwargs) -> float: + # 动态ATR止损,避免大额亏损:不低于-0.5%,不高于-2% + df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last = df.iloc[-1] + atr_pct = float((last['atr_14'] / last['close']).clip(lower=0.0005, upper=0.02)) + dyn_sl = -max(0.005, min(0.02, 1.5 * atr_pct)) + # 若价格跌破EMA52则收紧 + if last['close'] < last['ema_52']: + dyn_sl = min(dyn_sl, -0.008) + return float(dyn_sl) + + def adjust_trade_position(self, trade, current_time, current_rate: float, current_profit: float, **kwargs): + try: + df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last = df.iloc[-1] + prev = df.iloc[-2] if len(df) > 1 else last + hist_turn_weak = (last.get('macdhist', 0) < prev.get('macdhist', 0)) + price_above_ema24 = last['close'] > (1.01 * last['ema_24']) + # 条件:已盈利且动能转弱且价格偏离EMA24>1% → 减仓50% + if current_profit is not None and current_profit > 0.004 and hist_turn_weak and price_above_ema24 and trade.amount is not None and trade.amount > 0: + return -float(trade.amount) * 0.5 + except Exception: + return None + return None + + diff --git a/web/app.py b/web/app.py index a602177..9261a3d 100644 --- a/web/app.py +++ b/web/app.py @@ -255,8 +255,8 @@ def get_a_stock_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time return None def add_indicators(df): - fast = 24 - slow = 52 + fast = 12 + slow = 26 period = 9 macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) diff --git a/web/templates/index.html b/web/templates/index.html index 24aeb72..765797b 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1255,7 +1255,6 @@ -