diff --git a/.DS_Store b/.DS_Store index dfc60c1..5f2e7b2 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index a94d92f..49dbb41 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ feature_meta .DS_Store .DS_Store .DS_Store +.DS_Store diff --git a/ChanLun.py b/ChanLun.py index 18b184c..0ff5ee8 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -24,7 +24,7 @@ class ChanLun(): self.time15m = 15 self.time30m = 30 self.time_m_intervals = [3, 5, 10, 15, 30] - self.time_m_symbols = ['3m', '5m', '10m', '15m', '30m'] + self.time_m_symbols = ['2m', '3m', '5m', '10m', '15m', '20m','30m'] self.time2h = 2*60 self.time4h = 4*60 self.time6h = 6*60 @@ -45,9 +45,9 @@ class ChanLun(): self.time1y = 12*30*24*60 self.time_M_intervals = [2*30*24*60, 3*30*24*60, 6*30*24*60, 12*30*24*60] self.time_M_symbols = ['2M', '3M', '6M', '1y'] - self.time_symbols = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d', '1w', '2w', '1M', '3M', '6M', '1y'] + self.time_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m','30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d', '1w', '2w', '1M', '3M', '6M', '1y'] self.tf_df_dict = {} - self.ema_symbols = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d'] + self.ema_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m','30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d'] self.tf_df = TF_DF() def init_data(self, dataframe, intervals, timeframes): for index in range(0, len(intervals)): @@ -79,7 +79,17 @@ class ChanLun(): if len(self.tf_df_dict) > 0: return {key: self.tf_df_dict[key].get_current_klc() for key in self.ema_symbols} return None - + def check_price_ema52(self, price): + key_list = [] + if len(self.tf_df_dict) > 0: + ema52_dict = self.get_ema52_dict() + for key in self.ema_symbols: + if abs(price - ema52_dict[key]) < 100: + key_list.append(key) + return key_list + + + diff --git a/K线动能理论.txt b/K线动能理论.txt index bc96f52..6fe7412 100644 --- a/K线动能理论.txt +++ b/K线动能理论.txt @@ -1,3 +1,18 @@ +均线 +5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 16h, 1d, 2d, 3d, 1w, 2w, 1M + +参考时间周期 +大周期:4h, 1d +小周期:1h, 15m + +顺大逆小 +大周期看多,小周期跌完做多,跌完:顶分型和EMA52归零轴反弹 +大周期看空,小周期涨完做空,涨完:顶分型和EMA52归零轴反抽 + +多周期EMA52 + + + EMA52线的反弹比零轴的反弹弱 EMA52线和MACD白线同时归零轴同时满足的话是完美形态,最佳买卖点 diff --git a/TF_DF.py b/TF_DF.py index 2746706..fafb920 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -34,7 +34,7 @@ class TF_DF(): self.klc_list = self.get_klc_list(self.klu_list) self.bi_list = self.cal_bi_list(self.klc_list) self.seg_list = self.get_seg_list(self.bi_list) - self.zs_list = self.cal_zs_list(self.bi_list, self.seg_list) + self.zs_list = self.get_zs_list(self.bi_list, self.seg_list) self.chanmacd = ChanMACD(self.klu_list) self.klu_list = self.chanmacd.cal_macd_state() def get_ema52(self): @@ -123,12 +123,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: + 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.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: + 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: #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") @@ -838,11 +838,11 @@ class TF_DF(): last_bottom = None for klc in klc_list: fx = self.check_fx(klc) - if fx == Chan_FX_TYPE.TOP: + if fx == Chan_FX_TYPE.TOP and False: if last_bottom: if self.check_top_fx(last_bottom, klc) == False: fx = Chan_FX_TYPE.UNKNOWN - if fx == Chan_FX_TYPE.BOTTOM: + if fx == Chan_FX_TYPE.BOTTOM and False: if last_top: if self.check_bottom_fx(last_top, klc) == False: fx = Chan_FX_TYPE.UNKNOWN diff --git a/config/ChanLun_EMA52.json b/config/ChanLun_EMA52.json new file mode 100644 index 0000000..bf50e85 --- /dev/null +++ b/config/ChanLun_EMA52.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_btc_15.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": 8811, + "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/config/ThreeFilterEMA_BTC_15.json b/config/ThreeFilterEMA_BTC_15.json new file mode 100644 index 0000000..661f402 --- /dev/null +++ b/config/ThreeFilterEMA_BTC_15.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_btc_15.sqlite", + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "can_short" : true, + "timeframe" : "15m", + "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": [ + "WIF/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": 8811, + "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/ChanLun_EMA52.py b/strategies/ChanLun_EMA52.py new file mode 100644 index 0000000..e375cfd --- /dev/null +++ b/strategies/ChanLun_EMA52.py @@ -0,0 +1,333 @@ +# --- 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 ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX +# -------------------------------- +from technical.util import resample_to_interval, resampled_merge +import talib.abstract as ta +from pandas import DataFrame +import pandas as pd +from datetime import datetime, timedelta +from freqtrade.persistence import Trade, Order +from typing import Optional +import logging +logger = logging.getLogger(__name__) +""" +使用EMA周期52 +1. 检查当前price是否穿越,如果穿越时,MACD也是归零轴反转,则开仓 +2. 接近某个EMA周期后反转,此时MACD归零轴反转,则开仓 + +1. 从大周期开始找到价格接近ema52,MACD也接近零轴的周期,需要看这个周期的长级别是否高位空,大趋势方向 +2. 然后去小于这个时间周期的周期找买卖点,小级趋势方向和大趋势相反并且开始反向,小级别需要检查MACD是否归零轴反转,同时价格是否接近EMA52 +""" + +### Now you can use logger.info('asfd') to log +# freqtrade plot-dataframe --strategy ChanLun_BTC --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309- + +# freqtrade trade -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies +# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange=20260101- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_EMA52.json -t 1m 1m 1h 1d 1M --pairs BTC/USDT:USDT --timerange=20250405- +# freqtrade download-data -c ./user_data/Chan/config/ChanLun_EMA52.json -t 1m 1h 1d 1M --pairs BTC/USDT --timerange=20170101- +# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_EMA52.json -e 200 --timerange=20250201-20250901 +# freqtrade edge -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 +# freqtrade plot-dataframe -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901 + +# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange=20250721- +# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_EMA52.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101- +# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies + +class ChanLun_EMA52(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 + } + + can_short = True + lev = 1.0 + stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制 + use_custom_stoploss = True # 启用自定义止损 + + trailing_stop = False + trailing_stop_positive = 0.03 + trailing_stop_positive_offset = 0.06 + trailing_only_offset_is_reached = False + + # 关闭分批止盈/仓位调整 + position_adjustment_enable = False + startup_candle_count = 1600 + + last_time = datetime.now() + chan = ChanLun() + last_order = None + last_trade = None + pair = 'BTC/USDT:USDT' + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + self.init_dataframes(dataframe) + return dataframe + def init_dataframes(self, dataframe_1m): + dataframe_1h = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1h') + dataframe_1d = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1d') + dataframe_1M = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1M') + self.chan.init_dataframes(dataframe_1m, dataframe_1h, dataframe_1d, dataframe_1M) + current_price = dataframe_1m.iloc[-1]['close'] + print("Current Price: ", current_price) + self.print_all_current_klc() + def print_all_ema52(self): + for key, value in self.chan.get_ema52_dict().items(): + print(key, value) + def print_all_ema24(self): + for key, value in self.chan.get_ema24_dict().items(): + print(key, value) + def print_all_current_klc(self): + for key, value in self.chan.get_current_klc_dict().items(): + print(key, value.to_string()) + def add_indicators(self, df): + 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) + 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 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + bb302 = ta.BBANDS(df, timeperiod=20, 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['ema24'] = ta.EMA(df, timeperiod=24) + 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_position(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]: + # 关闭分批止盈,始终不调整仓位 + 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: + """ + 止损 = 开仓价 ± 1 * ATR(开仓时的ATR)。 + 多单: 开仓价 - ATR;空单: 开仓价 + ATR。 + """ + # 保本止损:当浮盈达到或超过 1% 时,将止损提至开仓价 + #if current_profit is not None and current_profit >= 0.14: + #return stoploss_from_absolute(trade.open_rate, current_rate, is_short=trade.is_short) + + entry_atr = trade.get_custom_data(key="entry_atr") + if entry_atr is None: + # 回退:取当前数据的 ATR 估算 + 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: + # 最保守的回退:5% + return -0.05 + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + ema52_str = 'resample_{}_ema52'.format(self.time15m) + ema52_val = float(last_candle.get(ema52_str, 0) or 0) + close_str = 'resample_{}_close'.format(self.time15m) + close_val = float(last_candle.get(close_str, 0) or 0) + if close_val < ema52_val: + return -0.01 + if trade.is_short: + stop_price = trade.open_rate + float(entry_atr) + else: + stop_price = trade.open_rate - float(entry_atr) + return stoploss_from_absolute(stop_price, current_rate, is_short=trade.is_short) + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs): + # 不做分批止盈/最终止盈处理,退出由策略信号/ROI/止损决定 + 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 过滤:atr < 100 不开单。 + """ + 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.time1h) + atr_val = float(last.get(atr_str, 0) or 0) + if atr_val < 0.001: + #logger.info(f"ATR过滤:atr={atr_val:.2f} < 100, 拒绝进场 {pair}") + return False + return True + except Exception as e: + logger.warning(f"confirm_trade_entry 异常: {e}") + return True + + 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_str = 'resample_{}_atr'.format(elf.time15) + # 保存开仓时的ATR值用于止损计算 + if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): + entry_atr = last_candle[atr_str] * 4 + 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: + shift15 = self.time15m + shift60 = self.time1h + bsp_col = 'resample_{}_bsp_mtf'.format(shift15) + score_col = 'resample_{}_mtf_score'.format(shift15) + macdh_col = 'resample_{}_macdhist'.format(shift15) + c60_col = 'resample_{}_close'.format(shift60) + e60_col = 'resample_{}_ema52'.format(shift60) + # 强化过滤:15m BSP + 分数阈值 + 60m 趋势同向 + 15m MACD柱同向 + if all(col in dataframe.columns for col in [bsp_col, score_col, macdh_col, c60_col, e60_col]): + dataframe.loc[ + ( + (dataframe[bsp_col].shift(shift15) == 1) & + (dataframe[score_col].shift(shift15) >= 1.2) & + (dataframe[c60_col].shift(shift60) >= dataframe[e60_col].shift(shift60)) & + (dataframe[macdh_col].shift(shift15) > 0) + ), + ['enter_long', 'enter_tag']] = (1, 'long_bsp15_v2') + dataframe.loc[ + ( + (dataframe[bsp_col].shift(shift15) == -1) & + (dataframe[score_col].shift(shift15) <= -1.2) & + (dataframe[c60_col].shift(shift60) <= dataframe[e60_col].shift(shift60)) & + (dataframe[macdh_col].shift(shift15) < 0) + ), + ['enter_short', 'enter_tag']] = (1, 'short_bsp15_v2') + return dataframe + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + shift15 = self.time15m + shift60 = self.time1h + bsp_col = 'resample_{}_bsp_mtf'.format(shift15) + score_col = 'resample_{}_mtf_score'.format(shift15) + c60_col = 'resample_{}_close'.format(shift60) + e60_col = 'resample_{}_ema52'.format(shift60) + # 反向强信号或60m趋势反向时平仓 + if all(col in dataframe.columns for col in [bsp_col, score_col, c60_col, e60_col]): + dataframe.loc[ + ( + ((dataframe[bsp_col].shift(shift15) == -1) & (dataframe[score_col].shift(shift15) <= -0.8)) | + (dataframe[c60_col].shift(shift60) < dataframe[e60_col].shift(shift60)) + ), + ['exit_long', 'exit_tag']] = (1, 'long_close_bsp15') + dataframe.loc[ + ( + ((dataframe[bsp_col].shift(shift15) == 1) & (dataframe[score_col].shift(shift15) >= 0.8)) | + (dataframe[c60_col].shift(shift60) > dataframe[e60_col].shift(shift60)) + ), + ['exit_short', 'exit_tag']] = (1, 'short_close_bsp15') + 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 + \ No newline at end of file diff --git a/strategies/ThreeFilterEMA_BTC_15.py b/strategies/ThreeFilterEMA_BTC_15.py new file mode 100644 index 0000000..8663c41 --- /dev/null +++ b/strategies/ThreeFilterEMA_BTC_15.py @@ -0,0 +1,485 @@ +# --- Do not remove these libs --- +""" +三重滤网均线交易策略 (Three Filter EMA Strategy) + +策略原理: +1. 大趋势判断:价格相对于EMA156的位置 +2. 小趋势判断:价格相对于EMA52的位置 +3. MACD金叉/死叉确认:金叉后需confirm_bars根K线持续上涨确认 +4. 价格站稳EMA52:需breakout_bars根K线站稳EMA52上方/下方 +5. 止损止盈:使用最近lookback_period根K线的最低/最高点作为止损, + 止盈 = 入场价 + 风险 * 盈亏比 +使用缠论分型和新笔的组合来判断趋势和入场时机 +EMA52在EMA156上方,顶分型2出现后,金叉,确认向上笔,价格站稳EMA52上方,入场做多 +EMA52在EMA156下方,底分型2出现后,死叉,确认向下笔,价格站稳EMA52下方,入场做空 +入场条件: +- 多头:大趋势多头(>EMA156) + 小趋势多头(>EMA52) + MACD金叉确认 + 价格站稳EMA52 +- 空头:大趋势空头( DataFrame: + """计算所有技术指标""" + + # ==================== 计算均线 ==================== + dataframe['ema156'] = ta.EMA(dataframe, timeperiod=self.ema156_length.value) + dataframe['ema52'] = ta.EMA(dataframe, timeperiod=self.ema52_length.value) + + # ==================== 计算MACD ==================== + macd = ta.MACD(dataframe, + fastperiod=self.macd_fast.value, + slowperiod=self.macd_slow.value, + signalperiod=self.macd_signal.value) + dataframe['macd'] = macd['macd'] + dataframe['macd_signal'] = macd['macdsignal'] + dataframe['macd_hist'] = macd['macdhist'] + + # MACD金叉和死叉 + dataframe['macd_golden_cross'] = ( + (dataframe['macd'] > dataframe['macd_signal']) & + (dataframe['macd'].shift(1) <= dataframe['macd_signal'].shift(1)) + ).astype(int) + + dataframe['macd_death_cross'] = ( + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['macd'].shift(1) >= dataframe['macd_signal'].shift(1)) + ).astype(int) + + # ==================== 趋势判断 ==================== + # 大趋势:价格相对于EMA156的位置 + dataframe['big_trend_bullish'] = (dataframe['close'] > dataframe['ema156']).astype(int) + dataframe['big_trend_bearish'] = (dataframe['close'] < dataframe['ema156']).astype(int) + + # 小趋势:价格相对于EMA52的位置 + dataframe['small_trend_bullish'] = (dataframe['close'] > dataframe['ema52']).astype(int) + dataframe['small_trend_bearish'] = (dataframe['close'] < dataframe['ema52']).astype(int) + + # ==================== 金叉/死叉后趋势确认 ==================== + confirm_bars = self.confirm_bars.value + breakout_bars = self.breakout_bars.value + + # 计算距离最近金叉的K线数 + dataframe['bars_since_golden'] = self._calculate_bars_since(dataframe, 'macd_golden_cross') + # 计算距离最近死叉的K线数 + dataframe['bars_since_death'] = self._calculate_bars_since(dataframe, 'macd_death_cross') + + # 记录金叉/死叉时的价格 + dataframe['golden_cross_price'] = self._get_cross_price(dataframe, 'macd_golden_cross') + dataframe['death_cross_price'] = self._get_cross_price(dataframe, 'macd_death_cross') + + # 检查金叉后confirm_bars根K线是否持续上涨 + dataframe['golden_cross_confirmed'] = self._check_golden_cross_confirmed( + dataframe, confirm_bars) + + # 检查死叉后confirm_bars根K线是否持续下跌 + dataframe['death_cross_confirmed'] = self._check_death_cross_confirmed( + dataframe, confirm_bars) + + # ==================== 价格站稳EMA52确认 ==================== + # 检查价格是否在近breakout_bars根K线内站稳EMA52上方 + dataframe['price_above_ema52_stable'] = self._check_price_above_ema52_stable( + dataframe, breakout_bars) + + # 检查价格是否在近breakout_bars根K线内站稳EMA52下方 + dataframe['price_below_ema52_stable'] = self._check_price_below_ema52_stable( + dataframe, breakout_bars) + + # 检测EMA52突破 + dataframe['ema52_breakout_up'] = ( + (dataframe['close'] > dataframe['ema52']) & + (dataframe['close'].shift(1) <= dataframe['ema52'].shift(1)) + ).astype(int) + + dataframe['ema52_breakout_down'] = ( + (dataframe['close'] < dataframe['ema52']) & + (dataframe['close'].shift(1) >= dataframe['ema52'].shift(1)) + ).astype(int) + + # 近期是否有EMA52突破 + dataframe['recent_ema52_breakout_up'] = dataframe['ema52_breakout_up'].rolling( + window=breakout_bars).sum().fillna(0) > 0 + dataframe['recent_ema52_breakout_down'] = dataframe['ema52_breakout_down'].rolling( + window=breakout_bars).sum().fillna(0) > 0 + + # ==================== 计算回调低点/高点作为止损 ==================== + lookback = self.lookback_period.value + dataframe['swing_low'] = dataframe['low'].rolling(window=lookback).min() + dataframe['swing_high'] = dataframe['high'].rolling(window=lookback).max() + + return dataframe + + def _calculate_bars_since(self, dataframe: DataFrame, column: str) -> np.ndarray: + """计算距离最近信号的K线数""" + result = np.zeros(len(dataframe)) + bars_count = np.nan + + for i in range(len(dataframe)): + if dataframe[column].iloc[i] == 1: + bars_count = 0 + elif not np.isnan(bars_count): + bars_count += 1 + result[i] = bars_count + + return result + + def _get_cross_price(self, dataframe: DataFrame, column: str) -> np.ndarray: + """获取金叉/死叉时的价格""" + result = np.full(len(dataframe), np.nan) + cross_price = np.nan + + for i in range(len(dataframe)): + if dataframe[column].iloc[i] == 1: + cross_price = dataframe['close'].iloc[i] + result[i] = cross_price + + return result + + def _check_golden_cross_confirmed(self, dataframe: DataFrame, confirm_bars: int) -> np.ndarray: + """检查金叉后confirm_bars根K线是否持续上涨""" + result = np.zeros(len(dataframe), dtype=bool) + + for i in range(confirm_bars + 5, len(dataframe)): + bars_since = dataframe['bars_since_golden'].iloc[i] + if np.isnan(bars_since): + continue + + bars_since = int(bars_since) + if confirm_bars <= bars_since <= confirm_bars + 5: + golden_price = dataframe['golden_cross_price'].iloc[i] + if np.isnan(golden_price): + continue + + # 检查金叉后的K线是否按上涨趋势运行 + trend_up = True + for j in range(1, min(confirm_bars + 1, bars_since + 1)): + idx = i - (bars_since - j) + if 0 <= idx < len(dataframe): + if dataframe['close'].iloc[idx] < golden_price: + trend_up = False + break + + result[i] = trend_up + + return result + + def _check_death_cross_confirmed(self, dataframe: DataFrame, confirm_bars: int) -> np.ndarray: + """检查死叉后confirm_bars根K线是否持续下跌""" + result = np.zeros(len(dataframe), dtype=bool) + + for i in range(confirm_bars + 5, len(dataframe)): + bars_since = dataframe['bars_since_death'].iloc[i] + if np.isnan(bars_since): + continue + + bars_since = int(bars_since) + if confirm_bars <= bars_since <= confirm_bars + 5: + death_price = dataframe['death_cross_price'].iloc[i] + if np.isnan(death_price): + continue + + # 检查死叉后的K线是否按下跌趋势运行 + trend_down = True + for j in range(1, min(confirm_bars + 1, bars_since + 1)): + idx = i - (bars_since - j) + if 0 <= idx < len(dataframe): + if dataframe['close'].iloc[idx] > death_price: + trend_down = False + break + + result[i] = trend_down + + return result + + def _check_price_above_ema52_stable(self, dataframe: DataFrame, breakout_bars: int) -> np.ndarray: + """检查价格是否在近breakout_bars根K线内站稳EMA52上方""" + result = np.ones(len(dataframe), dtype=bool) + + for i in range(breakout_bars, len(dataframe)): + for j in range(breakout_bars): + idx = i - j + if dataframe['close'].iloc[idx] < dataframe['ema52'].iloc[idx]: + result[i] = False + break + + return result + + def _check_price_below_ema52_stable(self, dataframe: DataFrame, breakout_bars: int) -> np.ndarray: + """检查价格是否在近breakout_bars根K线内站稳EMA52下方""" + result = np.ones(len(dataframe), dtype=bool) + + for i in range(breakout_bars, len(dataframe)): + for j in range(breakout_bars): + idx = i - j + if dataframe['close'].iloc[idx] > dataframe['ema52'].iloc[idx]: + result[i] = False + break + + return result + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """定义入场条件""" + + breakout_bars = self.breakout_bars.value + + # ==================== 多头入场条件 ==================== + # 1. 大级别为多头趋势(价格在EMA156上方) + # 2. 小级别为多头趋势(价格在EMA52上方) + # 3. MACD金叉已确认 或 最近breakout_bars根K线内有金叉 + # 4. 近期有EMA52突破 或 价格站稳EMA52上方 + # 5. MACD在信号线上方 + + long_condition = ( + (dataframe['big_trend_bullish'] == 1) & + (dataframe['small_trend_bullish'] == 1) & + ( + (dataframe['golden_cross_confirmed'] == True) | + (dataframe['bars_since_golden'] <= breakout_bars) + ) & + ( + (dataframe['recent_ema52_breakout_up'] == True) | + (dataframe['price_above_ema52_stable'] == True) + ) & + (dataframe['macd'] > dataframe['macd_signal']) + ) + + dataframe.loc[long_condition, ['enter_long', 'enter_tag']] = (1, 'three_filter_long') + + # ==================== 空头入场条件 ==================== + # 1. 大级别为空头趋势(价格在EMA156下方) + # 2. 小级别为空头趋势(价格在EMA52下方) + # 3. MACD死叉已确认 或 最近breakout_bars根K线内有死叉 + # 4. 近期有EMA52跌破 或 价格站稳EMA52下方 + # 5. MACD在信号线下方 + + short_condition = ( + (dataframe['big_trend_bearish'] == 1) & + (dataframe['small_trend_bearish'] == 1) & + ( + (dataframe['death_cross_confirmed'] == True) | + (dataframe['bars_since_death'] <= breakout_bars) + ) & + ( + (dataframe['recent_ema52_breakout_down'] == True) | + (dataframe['price_below_ema52_stable'] == True) + ) & + (dataframe['macd'] < dataframe['macd_signal']) + ) + + dataframe.loc[short_condition, ['enter_short', 'enter_tag']] = (1, 'three_filter_short') + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """定义出场条件 - 基于趋势反转""" + + # 多头出场:小趋势转空或MACD死叉 + long_exit_condition = ( + (dataframe['small_trend_bearish'] == 1) | + (dataframe['macd_death_cross'] == 1) + ) + + dataframe.loc[long_exit_condition, ['exit_long', 'exit_tag']] = (1, 'trend_reversal_exit') + + # 空头出场:小趋势转多或MACD金叉 + short_exit_condition = ( + (dataframe['small_trend_bullish'] == 1) | + (dataframe['macd_golden_cross'] == 1) + ) + + dataframe.loc[short_exit_condition, ['exit_short', 'exit_tag']] = (1, 'trend_reversal_exit') + + return dataframe + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + """确认交易入场时,计算并存储止损止盈价格""" + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if dataframe.empty: + return True + + last_candle = dataframe.iloc[-1] + risk_reward = self.risk_reward_ratio.value + + if side == 'long': + stop_loss = last_candle['swing_low'] + risk = rate - stop_loss + if risk > 0: + take_profit = rate + risk * risk_reward + self.custom_trade_info[pair] = { + 'stop_loss': stop_loss, + 'take_profit': take_profit, + 'entry_price': rate + } + #logger.info(f"Long entry: {pair} @ {rate}, SL: {stop_loss}, TP: {take_profit}") + else: + # 如果风险为0或负数,不进入交易 + #logger.warning(f"Invalid risk for long entry: {pair}, risk={risk}") + return False + else: # short + stop_loss = last_candle['swing_high'] + risk = stop_loss - rate + if risk > 0: + take_profit = rate - risk * risk_reward + self.custom_trade_info[pair] = { + 'stop_loss': stop_loss, + 'take_profit': take_profit, + 'entry_price': rate + } + #logger.info(f"Short entry: {pair} @ {rate}, SL: {stop_loss}, TP: {take_profit}") + else: + # 如果风险为0或负数,不进入交易 + #logger.warning(f"Invalid risk for short entry: {pair}, risk={risk}") + return False + + return True + + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, after_fill: bool, + **kwargs) -> Optional[float]: + """自定义止损逻辑""" + + if pair not in self.custom_trade_info: + return None + + trade_info = self.custom_trade_info[pair] + + stop_loss = trade_info.get('stop_loss') + entry_price = trade_info.get('entry_price') + + if stop_loss is None or entry_price is None: + return None + + if trade.is_short: + # 空头止损:当前价格 >= 止损价格 + if current_rate >= stop_loss: + return -0.0001 # 触发止损 + # 计算止损百分比 + sl_pct = (stop_loss - entry_price) / entry_price + return sl_pct + else: + # 多头止损:当前价格 <= 止损价格 + if current_rate <= stop_loss: + return -0.0001 # 触发止损 + # 计算止损百分比 + sl_pct = (entry_price - stop_loss) / entry_price + return -sl_pct + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> Optional[str]: + """自定义出场逻辑 - 止盈""" + + if pair not in self.custom_trade_info: + return None + + trade_info = self.custom_trade_info[pair] + take_profit = trade_info.get('take_profit') + + if take_profit is None: + return None + + if trade.is_short: + # 空头止盈:当前价格 <= 止盈价格 + if current_rate <= take_profit: + return 'take_profit' + else: + # 多头止盈:当前价格 >= 止盈价格 + if current_rate >= take_profit: + return 'take_profit' + + return None + + 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.leverage_value + + def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, + proposed_rate: float, current_profit: float, + exit_tag: Optional[str], **kwargs) -> float: + """自定义出场价格,减少滑点""" + # 使用提议价格,可根据需要调整 + return proposed_rate + + def trade_exit_confirm(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, exit_reason: str, + current_time: datetime, **kwargs) -> bool: + """交易退出确认,清理自定义交易信息""" + if pair in self.custom_trade_info: + del self.custom_trade_info[pair] + return True diff --git a/web/app.py b/web/app.py index f1b525e..fd1f92a 100644 --- a/web/app.py +++ b/web/app.py @@ -65,7 +65,7 @@ DEFAULT_TIMEFRAME_LABELS = OrderedDict([ ]) DEFAULT_SYMBOLS = [ - 'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT', + 'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT', 'WIF/USDT:USDT', 'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT' ] diff --git a/web/templates/index.html b/web/templates/index.html index 22c1186..3a56c7c 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -5885,8 +5885,9 @@ const defaultMAs = [ { type: 'EMA', length: 52, color: '#800080', name: 'EMA52' }, // 紫色 { type: 'EMA', length: 24, color: '#008000', name: 'EMA24' }, // 深绿色 - { type: 'SMA', length: 30, color: '#FF8C00', name: 'SMA30' }, // 橙色 - { type: 'SMA', length: 250, color: '#1E90FF', name: 'SMA250' } // 蓝色 + { type: 'EMA', length: 104, color: '#FF8C00', name: 'EMA104' }, // 橙色 + { type: 'EMA', length: 156, color: '#1E90FF', name: 'EMA156' }, // 蓝色 + { type: 'EMA', length: 208, color: '#1F004F', name: 'EMA312' } // 蓝色 ]; defaultMAs.forEach(ma => {