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 @@
-