MACD使用标准参数
This commit is contained in:
@@ -84,6 +84,9 @@ class ChanKLC():
|
|||||||
if self.fx == Chan_FX_TYPE.BOTTOM:
|
if self.fx == Chan_FX_TYPE.BOTTOM:
|
||||||
if self.macd < 0:
|
if self.macd < 0:
|
||||||
self.bb_out = True
|
self.bb_out = True
|
||||||
|
def cal_macd_state(self, dir):
|
||||||
|
|
||||||
|
return macd_state
|
||||||
def cal_indicators(self):
|
def cal_indicators(self):
|
||||||
for index in range(1, len(self.klus)):
|
for index in range(1, len(self.klus)):
|
||||||
self.volume += self.klus[index].volume
|
self.volume += self.klus[index].volume
|
||||||
|
|||||||
@@ -600,6 +600,14 @@ class ChanKLU:
|
|||||||
# 设置指标后更新实时分析
|
# 设置指标后更新实时分析
|
||||||
self.update_realtime_analysis()
|
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):
|
def get_feature_data(self):
|
||||||
features = dict()
|
features = dict()
|
||||||
features['klu_close'] = self.close
|
features['klu_close'] = self.close
|
||||||
|
|||||||
+3
-3
@@ -1130,9 +1130,9 @@ class ChanLun():
|
|||||||
def get_decimal(self, value):
|
def get_decimal(self, value):
|
||||||
return Decimal("{:.2f}".format(value))
|
return Decimal("{:.2f}".format(value))
|
||||||
def add_indicators(self, df):
|
def add_indicators(self, df):
|
||||||
fast = 8
|
fast = 12
|
||||||
slow = 16
|
slow = 26
|
||||||
period = 6
|
period = 9
|
||||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||||
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||||
bbp365 = ta.BBP(df, timeperiod=365)
|
bbp365 = ta.BBP(df, timeperiod=365)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ class BB9033(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
# Optimal timeframe for the strategy
|
# Optimal timeframe for the strategy
|
||||||
time = 5
|
time = 60
|
||||||
# Trailing stop loss
|
# Trailing stop loss
|
||||||
trailing_stop = False
|
trailing_stop = False
|
||||||
lev = 1.0
|
lev = 1.0
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class BB90331(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
# Optimal timeframe for the strategy
|
# Optimal timeframe for the strategy
|
||||||
time =1
|
time = 1
|
||||||
# Trailing stop loss
|
# Trailing stop loss
|
||||||
trailing_stop = False
|
trailing_stop = False
|
||||||
lev = 1.0
|
lev = 1.0
|
||||||
|
|||||||
@@ -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 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
|
# 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 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 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 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 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 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):
|
class ChanLun_BTC_30(IStrategy):
|
||||||
INTERFACE_VERSION: int = 3
|
INTERFACE_VERSION: int = 3
|
||||||
@@ -66,7 +66,7 @@ class ChanLun_BTC_30(IStrategy):
|
|||||||
}
|
}
|
||||||
can_short = True
|
can_short = True
|
||||||
lev = 1.0
|
lev = 1.0
|
||||||
stoploss = -0.2 # 设置为很大的负值,让custom_stoploss来控制
|
stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制
|
||||||
use_custom_stoploss = False # 启用自定义止损
|
use_custom_stoploss = False # 启用自定义止损
|
||||||
|
|
||||||
trailing_stop = False
|
trailing_stop = False
|
||||||
@@ -158,9 +158,9 @@ class ChanLun_BTC_30(IStrategy):
|
|||||||
bi2 = bi_list[-2]
|
bi2 = bi_list[-2]
|
||||||
print(bi1.start_time, bi1.end_time, bi1.dir, bi2.start_time, bi2.end_time, bi2.dir)
|
print(bi1.start_time, bi1.end_time, bi1.dir, bi2.start_time, bi2.end_time, bi2.dir)
|
||||||
def add_indicators(self, df):
|
def add_indicators(self, df):
|
||||||
fast = 8
|
fast = 12
|
||||||
slow = 16
|
slow = 26
|
||||||
period = 6
|
period = 9
|
||||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||||
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
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)
|
bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ class ChanLun_BTC_K(IStrategy):
|
|||||||
timeframe = '5m'
|
timeframe = '5m'
|
||||||
|
|
||||||
# 指标参数
|
# 指标参数
|
||||||
macd_fast = 12
|
macd_fast = 24
|
||||||
macd_slow = 26
|
macd_slow = 52
|
||||||
macd_signal = 9
|
macd_signal = 18
|
||||||
ema_short = 24
|
ema_short = 24
|
||||||
ema_long = 52
|
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_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))
|
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',
|
'date',
|
||||||
'close',
|
'close',
|
||||||
f'macd_{suffix}', f'macdsignal_{suffix}', f'macdhist_{suffix}',
|
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'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'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}'
|
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():
|
for suf, minutes in intervals.items():
|
||||||
df_res = resample_to_interval(dataframe, minutes)
|
df_res = resample_to_interval(dataframe, minutes)
|
||||||
|
|||||||
@@ -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])
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
+2
-2
@@ -255,8 +255,8 @@ def get_a_stock_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def add_indicators(df):
|
def add_indicators(df):
|
||||||
fast = 24
|
fast = 12
|
||||||
slow = 52
|
slow = 26
|
||||||
period = 9
|
period = 9
|
||||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||||
|
|
||||||
|
|||||||
+13
-20
@@ -1255,7 +1255,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentData = null;
|
let currentData = null;
|
||||||
@@ -1808,7 +1807,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化图表
|
// 初始化图表
|
||||||
function initTradingView(symbol, timeframe) {
|
function initTradingView(symbol, timeframe) {
|
||||||
try {
|
try {
|
||||||
@@ -1955,28 +1953,32 @@
|
|||||||
// 如果需要显示MACD,创建MACD容器
|
// 如果需要显示MACD,创建MACD容器
|
||||||
let macdChartContainer = null;
|
let macdChartContainer = null;
|
||||||
if (showMacd) {
|
if (showMacd) {
|
||||||
// 设置各图表高度 - 为四个图表分配合理比例
|
// 显示MACD时:主图(45%) → MACD(17.5%) → 成交量(20%) → ATR(17.5%)
|
||||||
mainChartContainer.style.height = '45%'; // 主图占45%
|
mainChartContainer.style.height = '45%'; // 主图占45%
|
||||||
volumeChartContainer.style.top = '45%';
|
|
||||||
volumeChartContainer.style.height = '20%'; // 成交量图占20%
|
// MACD 放到成交量上方
|
||||||
|
|
||||||
atrChartContainer.style.top = '65%'; // ATR图从65%位置开始
|
|
||||||
atrChartContainer.style.height = '17.5%'; // ATR图占17.5%
|
|
||||||
|
|
||||||
macdChartContainer = document.createElement('div');
|
macdChartContainer = document.createElement('div');
|
||||||
macdChartContainer.style.width = '100%';
|
macdChartContainer.style.width = '100%';
|
||||||
macdChartContainer.style.height = '17.5%'; // MACD图占17.5%
|
macdChartContainer.style.height = '17.5%'; // MACD图占17.5%
|
||||||
macdChartContainer.style.position = 'absolute';
|
macdChartContainer.style.position = 'absolute';
|
||||||
macdChartContainer.style.top = '82.5%'; // 从82.5%位置开始
|
macdChartContainer.style.top = '45%'; // 紧贴主图下方
|
||||||
macdChartContainer.style.left = '0';
|
macdChartContainer.style.left = '0';
|
||||||
macdChartContainer.style.right = '0';
|
macdChartContainer.style.right = '0';
|
||||||
macdChartContainer.style.borderTop = '1px solid #e0e0e0';
|
macdChartContainer.style.borderTop = '1px solid #e0e0e0';
|
||||||
|
|
||||||
|
// 成交量位于 MACD 之下
|
||||||
|
volumeChartContainer.style.top = '62.5%';
|
||||||
|
volumeChartContainer.style.height = '20%'; // 成交量图占20%
|
||||||
|
|
||||||
|
// ATR 位于最底部
|
||||||
|
atrChartContainer.style.top = '82.5%'; // ATR图从82.5%位置开始
|
||||||
|
atrChartContainer.style.height = '17.5%'; // ATR图占17.5%
|
||||||
} else {
|
} else {
|
||||||
// 不显示MACD时的高度 - 主图、成交量图和ATR图分配
|
// 不显示MACD时的高度 - 主图、成交量图和ATR图分配
|
||||||
mainChartContainer.style.height = '55%'; // 主图占55%
|
mainChartContainer.style.height = '55%'; // 主图占55%
|
||||||
volumeChartContainer.style.top = '55%';
|
volumeChartContainer.style.top = '55%';
|
||||||
volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%
|
volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%
|
||||||
|
|
||||||
atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始
|
atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始
|
||||||
atrChartContainer.style.height = '22.5%'; // ATR图占22.5%
|
atrChartContainer.style.height = '22.5%'; // ATR图占22.5%
|
||||||
}
|
}
|
||||||
@@ -2567,7 +2569,6 @@
|
|||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示笔的绘制 - 分别处理主周期和次周期
|
// 显示笔的绘制 - 分别处理主周期和次周期
|
||||||
if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked')) {
|
if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked')) {
|
||||||
console.log('绘制笔 - 已启用');
|
console.log('绘制笔 - 已启用');
|
||||||
@@ -3152,7 +3153,6 @@
|
|||||||
} else {
|
} else {
|
||||||
console.log('绘制线段 - 已禁用');
|
console.log('绘制线段 - 已禁用');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示中枢的绘制 - 分别处理主周期和次周期
|
// 显示中枢的绘制 - 分别处理主周期和次周期
|
||||||
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) {
|
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) {
|
||||||
console.log('绘制中枢 - 已启用');
|
console.log('绘制中枢 - 已启用');
|
||||||
@@ -3728,7 +3728,6 @@
|
|||||||
} else {
|
} else {
|
||||||
console.log('绘制未完成中枢 - 已禁用');
|
console.log('绘制未完成中枢 - 已禁用');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加买卖点标记
|
// 添加买卖点标记
|
||||||
if ($('#showTradePoints').is(':checked')) {
|
if ($('#showTradePoints').is(':checked')) {
|
||||||
console.log('绘制买卖点 - 已启用');
|
console.log('绘制买卖点 - 已启用');
|
||||||
@@ -4433,7 +4432,6 @@
|
|||||||
window.mainFxMarkers = [];
|
window.mainFxMarkers = [];
|
||||||
window.fxMarkers = [];
|
window.fxMarkers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制小周期分型标记
|
// 绘制小周期分型标记
|
||||||
if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) ||
|
if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) ||
|
||||||
($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0)) {
|
($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0)) {
|
||||||
@@ -5169,7 +5167,6 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, volumeChart, atrChart, macdChart, showMacd) {
|
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, volumeChart, atrChart, macdChart, showMacd) {
|
||||||
// 调试变量
|
// 调试变量
|
||||||
window.debugMode = true;
|
window.debugMode = true;
|
||||||
@@ -5961,7 +5958,6 @@
|
|||||||
|
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面加载时初始化
|
// 页面加载时初始化
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// 从本地存储中恢复时区设置
|
// 从本地存储中恢复时区设置
|
||||||
@@ -6723,7 +6719,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化回放
|
// 初始化回放
|
||||||
function initReplay() {
|
function initReplay() {
|
||||||
// 检查是否有新的回放数据结构
|
// 检查是否有新的回放数据结构
|
||||||
@@ -7511,7 +7506,6 @@
|
|||||||
alertDiv.alert('close');
|
alertDiv.alert('close');
|
||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示成功提示
|
// 显示成功提示
|
||||||
function showSuccessAlert(message) {
|
function showSuccessAlert(message) {
|
||||||
const alertDiv = $(`
|
const alertDiv = $(`
|
||||||
@@ -8259,7 +8253,6 @@
|
|||||||
|
|
||||||
console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加布林带"按钮');
|
console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加布林带"按钮');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加布林带
|
// 添加布林带
|
||||||
function addBollingerBand() {
|
function addBollingerBand() {
|
||||||
console.log('🚨🚨🚨 ➕ 警告:addBollingerBand函数被调用了!!!');
|
console.log('🚨🚨🚨 ➕ 警告:addBollingerBand函数被调用了!!!');
|
||||||
|
|||||||
Reference in New Issue
Block a user