添加新的策略

This commit is contained in:
jackyu66git
2026-01-27 16:02:55 +08:00
parent 94f7c022c0
commit 2af660b1dc
11 changed files with 1023 additions and 12 deletions
Vendored
BIN
View File
Binary file not shown.
+1
View File
@@ -36,3 +36,4 @@ feature_meta
.DS_Store
.DS_Store
.DS_Store
.DS_Store
+14 -4
View File
@@ -24,7 +24,7 @@ class ChanLun():
self.time15m = 15
self.time30m = 30
self.time_m_intervals = [3, 5, 10, 15, 30]
self.time_m_symbols = ['3m', '5m', '10m', '15m', '30m']
self.time_m_symbols = ['2m', '3m', '5m', '10m', '15m', '20m','30m']
self.time2h = 2*60
self.time4h = 4*60
self.time6h = 6*60
@@ -45,9 +45,9 @@ class ChanLun():
self.time1y = 12*30*24*60
self.time_M_intervals = [2*30*24*60, 3*30*24*60, 6*30*24*60, 12*30*24*60]
self.time_M_symbols = ['2M', '3M', '6M', '1y']
self.time_symbols = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d', '1w', '2w', '1M', '3M', '6M', '1y']
self.time_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m','30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d', '1w', '2w', '1M', '3M', '6M', '1y']
self.tf_df_dict = {}
self.ema_symbols = ['1m', '3m', '5m', '10m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d']
self.ema_symbols = ['1m', '2m', '3m', '5m', '10m', '15m', '20m','30m', '1h', '2h', '4h', '6h', '8h', '12h', '16h', '1d', '2d', '3d']
self.tf_df = TF_DF()
def init_data(self, dataframe, intervals, timeframes):
for index in range(0, len(intervals)):
@@ -79,7 +79,17 @@ class ChanLun():
if len(self.tf_df_dict) > 0:
return {key: self.tf_df_dict[key].get_current_klc() for key in self.ema_symbols}
return None
def check_price_ema52(self, price):
key_list = []
if len(self.tf_df_dict) > 0:
ema52_dict = self.get_ema52_dict()
for key in self.ema_symbols:
if abs(price - ema52_dict[key]) < 100:
key_list.append(key)
return key_list
+15
View File
@@ -1,3 +1,18 @@
均线
5m, 15m, 30m, 1h, 2h, 4h, 8h, 12h, 16h, 1d, 2d, 3d, 1w, 2w, 1M
参考时间周期
大周期:4h, 1d
小周期:1h, 15m
顺大逆小
大周期看多,小周期跌完做多,跌完:顶分型和EMA52归零轴反弹
大周期看空,小周期涨完做空,涨完:顶分型和EMA52归零轴反抽
多周期EMA52
EMA52线的反弹比零轴的反弹弱
EMA52线和MACD白线同时归零轴同时满足的话是完美形态,最佳买卖点
+5 -5
View File
@@ -34,7 +34,7 @@ class TF_DF():
self.klc_list = self.get_klc_list(self.klu_list)
self.bi_list = self.cal_bi_list(self.klc_list)
self.seg_list = self.get_seg_list(self.bi_list)
self.zs_list = self.cal_zs_list(self.bi_list, self.seg_list)
self.zs_list = self.get_zs_list(self.bi_list, self.seg_list)
self.chanmacd = ChanMACD(self.klu_list)
self.klu_list = self.chanmacd.cal_macd_state()
def get_ema52(self):
@@ -123,12 +123,12 @@ class TF_DF():
return klu_state_list
def check_fx(self, klc):
if klc.pre and klc.next:
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low:
if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low and klc.macd > 0:
#if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0:
klc.set_fx(Chan_FX_TYPE.TOP)
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP")
return Chan_FX_TYPE.TOP
elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high:
elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high and klc.macd < 0:
#if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0:
klc.set_fx(Chan_FX_TYPE.BOTTOM)
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM")
@@ -838,11 +838,11 @@ class TF_DF():
last_bottom = None
for klc in klc_list:
fx = self.check_fx(klc)
if fx == Chan_FX_TYPE.TOP:
if fx == Chan_FX_TYPE.TOP and False:
if last_bottom:
if self.check_top_fx(last_bottom, klc) == False:
fx = Chan_FX_TYPE.UNKNOWN
if fx == Chan_FX_TYPE.BOTTOM:
if fx == Chan_FX_TYPE.BOTTOM and False:
if last_top:
if self.check_bottom_fx(last_top, klc) == False:
fx = Chan_FX_TYPE.UNKNOWN
+83
View File
@@ -0,0 +1,83 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": 1,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD",
"dry_run": true,
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short" : true,
"timeframe" : "1m",
"process_only_new_candles" : false,
"unfilledtimeout": {
"entry": 1,
"exit": 1,
"exit_timeout_count": 5,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USDT:USDT"
],
"pair_blacklist": [
"BNB/.*"
]
},
"pairlists": [
{
"method": "StaticPairList",
"number_assets": 1,
"sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 1800
}
],
"telegram": {
"enabled": false,
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
"chat_id": "580807463"
},
"api_server": {
"enabled": true,
"listen_ip_address": "127.0.0.1",
"listen_port": 8811,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "freqtrade",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 2
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"$schema": "https://schema.freqtrade.io/schema.json",
"max_open_trades": 1,
"stake_currency": "USDT",
"stake_amount": "unlimited",
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD",
"dry_run": true,
"db_url": "sqlite:///tradesv3.chanlun_btc_15.sqlite",
"dry_run_wallet": 1000,
"cancel_open_orders_on_exit": true,
"trading_mode": "futures",
"margin_mode": "isolated",
"can_short" : true,
"timeframe" : "15m",
"process_only_new_candles" : false,
"unfilledtimeout": {
"entry": 1,
"exit": 1,
"exit_timeout_count": 5,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "binance",
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"WIF/USDT:USDT"
],
"pair_blacklist": [
"BNB/.*"
]
},
"pairlists": [
{
"method": "StaticPairList",
"number_assets": 1,
"sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 1800
}
],
"telegram": {
"enabled": false,
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
"chat_id": "580807463"
},
"api_server": {
"enabled": true,
"listen_ip_address": "127.0.0.1",
"listen_port": 8811,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
"CORS_origins": [],
"username": "freqtrader",
"password": "FreqTrade007"
},
"bot_name": "freqtrade",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 2
}
}
+333
View File
@@ -0,0 +1,333 @@
# --- Do not remove these libs ---
from statistics import median
from freqtrade.strategy import IStrategy, stoploss_from_absolute
import sys
import os
# 添加父目录到系统路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ChanLun import ChanLun
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
# --------------------------------
from technical.util import resample_to_interval, resampled_merge
import talib.abstract as ta
from pandas import DataFrame
import pandas as pd
from datetime import datetime, timedelta
from freqtrade.persistence import Trade, Order
from typing import Optional
import logging
logger = logging.getLogger(__name__)
"""
使用EMA周期52
1. 检查当前price是否穿越,如果穿越时,MACD也是归零轴反转,则开仓
2. 接近某个EMA周期后反转,此时MACD归零轴反转,则开仓
1. 从大周期开始找到价格接近ema52,MACD也接近零轴的周期,需要看这个周期的长级别是否高位空,大趋势方向
2. 然后去小于这个时间周期的周期找买卖点,小级趋势方向和大趋势相反并且开始反向,小级别需要检查MACD是否归零轴反转,同时价格是否接近EMA52
"""
### Now you can use logger.info('asfd') to log
# freqtrade plot-dataframe --strategy ChanLun_BTC --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309-
# freqtrade trade -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies
# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange=20260101-
# freqtrade download-data -c ./user_data/Chan/config/ChanLun_EMA52.json -t 1m 1m 1h 1d 1M --pairs BTC/USDT:USDT --timerange=20250405-
# freqtrade download-data -c ./user_data/Chan/config/ChanLun_EMA52.json -t 1m 1h 1d 1M --pairs BTC/USDT --timerange=20170101-
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_EMA52.json -e 200 --timerange=20250201-20250901
# freqtrade edge -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901
# freqtrade plot-dataframe -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901
# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies --timerange=20250721-
# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_EMA52.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101-
# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_EMA52.json --strategy ChanLun_EMA52 --strategy-path ./user_data/Chan/strategies
class ChanLun_EMA52(IStrategy):
INTERFACE_VERSION: int = 3
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
# 30m and 1h
minimal_roi = {
"0": 0.15,
"360": 0.2,
"640": 0.1,
"1200": 0
}
# 5m and 15m
minimal_roi_1 = {
"0": 0.1,
"60": 0.05,
"120": 0.02,
"240": 0
}
# 15m and 30m
minimal_roi_1 = {
"0": 0.1,
"240": 0.05,
"480": 0.03,
"600": 0
}
minimal_roi_1 = {
"0": 1.50,
"120": 0.05,
"240": 0.025,
"360": 0
}
can_short = True
lev = 1.0
stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制
use_custom_stoploss = True # 启用自定义止损
trailing_stop = False
trailing_stop_positive = 0.03
trailing_stop_positive_offset = 0.06
trailing_only_offset_is_reached = False
# 关闭分批止盈/仓位调整
position_adjustment_enable = False
startup_candle_count = 1600
last_time = datetime.now()
chan = ChanLun()
last_order = None
last_trade = None
pair = 'BTC/USDT:USDT'
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
self.init_dataframes(dataframe)
return dataframe
def init_dataframes(self, dataframe_1m):
dataframe_1h = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1h')
dataframe_1d = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1d')
dataframe_1M = self.dp.get_pair_dataframe(pair=self.pair, timeframe='1M')
self.chan.init_dataframes(dataframe_1m, dataframe_1h, dataframe_1d, dataframe_1M)
current_price = dataframe_1m.iloc[-1]['close']
print("Current Price: ", current_price)
self.print_all_current_klc()
def print_all_ema52(self):
for key, value in self.chan.get_ema52_dict().items():
print(key, value)
def print_all_ema24(self):
for key, value in self.chan.get_ema24_dict().items():
print(key, value)
def print_all_current_klc(self):
for key, value in self.chan.get_current_klc_dict().items():
print(key, value.to_string())
def add_indicators(self, df):
fast = 12
slow = 26
period = 9
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0)
bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0)
bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0)
bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
# 计算布林带中轨(移动平均线)
bb30_middle = ta.SMA(df, timeperiod=90)
# 手动计算布林带 %B 指标 (BBP)
# %B = (Price - Lower Band) / (Upper Band - Lower Band)
bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband'])
bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband'])
bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband'])
bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband'])
df['atr'] = ta.ATR(df, timeperiod=14)
df['bbup365'] = bb365['upperband']
df['bblow365'] = bb365['lowerband']
df['bbp365'] = bbp365
df['bbup120'] = bb120['upperband']
df['bblow120'] = bb120['lowerband']
df['bbp120'] = bbp120
df['bbup30'] = bb30['upperband']
df['bblow30'] = bb30['lowerband']
df['bbmiddle30'] = bb30_middle # 添加bb30中轨
df['bbp30'] = bbp30
df['bbup302'] = bb302['upperband']
df['bblow302'] = bb302['lowerband']
df['bbp302'] = bbp302
df['macd'] = macd['macd']
df['macdsignal'] = macd['macdsignal']
df['macdhist'] = macd['macdhist']
df['ema5'] = ta.EMA(df, timeperiod=5)
df['ema10'] = ta.EMA(df, timeperiod=10)
df['ema24'] = ta.EMA(df, timeperiod=24)
df['ema26'] = ta.EMA(df, timeperiod=26)
df['ema52'] = ta.EMA(df, timeperiod=52)
df['rsi'] = ta.RSI(df, timeperiod=14)
df['volume_ratio'] = self.cal_volume_ratio(df)
return df
def cal_volume_ratio(self, dataframe, window=10):
df = dataframe.copy()
# 计算过去N根K线的平均成交量
df['avg_volume'] = df['volume'].rolling(window=window).mean()
# 计算量比
df['volume_ratio'] = df['volume'] / df['avg_volume']
# 填充缺失值(前N根K线)
df['volume_ratio'] = df['volume_ratio'].fillna(1.0)
return df['volume_ratio']
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
entry_tag: str | None, side: str, **kwargs) -> float:
new_entryprice = proposed_rate
if trade:
if trade.is_short:
new_entryprice = proposed_rate - 50
else:
new_entryprice = proposed_rate + 50
return new_entryprice
def custom_exit_price(self, pair: str, trade: Trade,
current_time: datetime, proposed_rate: float,
current_profit: float, exit_tag: str | None, **kwargs) -> float:
new_exitprice = proposed_rate
if trade:
if trade.is_short:
new_exitprice = proposed_rate + 50
else:
new_exitprice = proposed_rate - 50
return new_exitprice
def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float,
min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs) -> Optional[float]:
# 关闭分批止盈,始终不调整仓位
return None
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> float | None:
"""
止损 = 开仓价 ± 1 * ATR(开仓时的ATR)。
多单: 开仓价 - ATR;空单: 开仓价 + ATR。
"""
# 保本止损:当浮盈达到或超过 1% 时,将止损提至开仓价
#if current_profit is not None and current_profit >= 0.14:
#return stoploss_from_absolute(trade.open_rate, current_rate, is_short=trade.is_short)
entry_atr = trade.get_custom_data(key="entry_atr")
if entry_atr is None:
# 回退:取当前数据的 ATR 估算
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
if dataframe is not None and len(dataframe) > 0 and 'atr' in dataframe.columns:
entry_atr = float(dataframe.iloc[-1]['atr'])
else:
# 最保守的回退:5%
return -0.05
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
ema52_str = 'resample_{}_ema52'.format(self.time15m)
ema52_val = float(last_candle.get(ema52_str, 0) or 0)
close_str = 'resample_{}_close'.format(self.time15m)
close_val = float(last_candle.get(close_str, 0) or 0)
if close_val < ema52_val:
return -0.01
if trade.is_short:
stop_price = trade.open_rate + float(entry_atr)
else:
stop_price = trade.open_rate - float(entry_atr)
return stoploss_from_absolute(stop_price, current_rate, is_short=trade.is_short)
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs):
# 不做分批止盈/最终止盈处理,退出由策略信号/ROI/止损决定
return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: str | None,
side: str, **kwargs) -> bool:
"""
ATR 过滤:atr < 100 不开单。
"""
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return False
last = dataframe.iloc[-1]
atr_str = 'resample_{}_atr'.format(self.time1h)
atr_val = float(last.get(atr_str, 0) or 0)
if atr_val < 0.001:
#logger.info(f"ATR过滤:atr={atr_val:.2f} < 100, 拒绝进场 {pair}")
return False
return True
except Exception as e:
logger.warning(f"confirm_trade_entry 异常: {e}")
return True
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
"""
Called right after an order fills.
Will be called for all order types (entry, exit, stoploss, position adjustment).
:param pair: Pair for trade
:param trade: trade object.
:param order: Order object.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
# Obtain pair dataframe (just to show how to access it)
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
atr_str = 'resample_{}_atr'.format(elf.time15)
# 保存开仓时的ATR值用于止损计算
if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
entry_atr = last_candle[atr_str] * 4
trade.set_custom_data(key="entry_atr", value=entry_atr)
#logger.info(f"保存开仓时ATR值: {entry_atr}")
return None
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
shift15 = self.time15m
shift60 = self.time1h
bsp_col = 'resample_{}_bsp_mtf'.format(shift15)
score_col = 'resample_{}_mtf_score'.format(shift15)
macdh_col = 'resample_{}_macdhist'.format(shift15)
c60_col = 'resample_{}_close'.format(shift60)
e60_col = 'resample_{}_ema52'.format(shift60)
# 强化过滤:15m BSP + 分数阈值 + 60m 趋势同向 + 15m MACD柱同向
if all(col in dataframe.columns for col in [bsp_col, score_col, macdh_col, c60_col, e60_col]):
dataframe.loc[
(
(dataframe[bsp_col].shift(shift15) == 1) &
(dataframe[score_col].shift(shift15) >= 1.2) &
(dataframe[c60_col].shift(shift60) >= dataframe[e60_col].shift(shift60)) &
(dataframe[macdh_col].shift(shift15) > 0)
),
['enter_long', 'enter_tag']] = (1, 'long_bsp15_v2')
dataframe.loc[
(
(dataframe[bsp_col].shift(shift15) == -1) &
(dataframe[score_col].shift(shift15) <= -1.2) &
(dataframe[c60_col].shift(shift60) <= dataframe[e60_col].shift(shift60)) &
(dataframe[macdh_col].shift(shift15) < 0)
),
['enter_short', 'enter_tag']] = (1, 'short_bsp15_v2')
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
shift15 = self.time15m
shift60 = self.time1h
bsp_col = 'resample_{}_bsp_mtf'.format(shift15)
score_col = 'resample_{}_mtf_score'.format(shift15)
c60_col = 'resample_{}_close'.format(shift60)
e60_col = 'resample_{}_ema52'.format(shift60)
# 反向强信号或60m趋势反向时平仓
if all(col in dataframe.columns for col in [bsp_col, score_col, c60_col, e60_col]):
dataframe.loc[
(
((dataframe[bsp_col].shift(shift15) == -1) & (dataframe[score_col].shift(shift15) <= -0.8)) |
(dataframe[c60_col].shift(shift60) < dataframe[e60_col].shift(shift60))
),
['exit_long', 'exit_tag']] = (1, 'long_close_bsp15')
dataframe.loc[
(
((dataframe[bsp_col].shift(shift15) == 1) & (dataframe[score_col].shift(shift15) >= 0.8)) |
(dataframe[c60_col].shift(shift60) > dataframe[e60_col].shift(shift60))
),
['exit_short', 'exit_tag']] = (1, 'short_close_bsp15')
return dataframe
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
return self.lev
+485
View File
@@ -0,0 +1,485 @@
# --- Do not remove these libs ---
"""
三重滤网均线交易策略 (Three Filter EMA Strategy)
策略原理:
1. 大趋势判断:价格相对于EMA156的位置
2. 小趋势判断:价格相对于EMA52的位置
3. MACD金叉/死叉确认:金叉后需confirm_bars根K线持续上涨确认
4. 价格站稳EMA52:需breakout_bars根K线站稳EMA52上方/下方
5. 止损止盈:使用最近lookback_period根K线的最低/最高点作为止损,
止盈 = 入场价 + 风险 * 盈亏比
使用缠论分型和新笔的组合来判断趋势和入场时机
EMA52在EMA156上方,顶分型2出现后,金叉,确认向上笔,价格站稳EMA52上方,入场做多
EMA52在EMA156下方,底分型2出现后,死叉,确认向下笔,价格站稳EMA52下方,入场做空
入场条件:
- 多头:大趋势多头(>EMA156) + 小趋势多头(>EMA52) + MACD金叉确认 + 价格站稳EMA52
- 空头:大趋势空头(<EMA156) + 小趋势空头(<EMA52) + MACD死叉确认 + 价格站稳EMA52下方
"""
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
from freqtrade.persistence import Trade
import talib.abstract as ta
from pandas import DataFrame
import numpy as np
from datetime import datetime
from typing import Optional
import logging
logger = logging.getLogger(__name__)
# freqtrade trade -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json --strategy ThreeFilterEMA_BTC_15 --strategy-path ./user_data/Chan/strategies
# freqtrade backtesting -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json --strategy ThreeFilterEMA_BTC_15 --strategy-path ./user_data/Chan/strategies --timerange=20260110-
# freqtrade lookahead-analysis --export none -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json --strategy ThreeFilterEMA_BTC_15 --strategy-path ./user_data/Chan/strategies --timerange=20250525-
# freqtrade download-data -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json -t 15m --pairs BTC/USDT:USDT --timerange=20250405-
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss buy sell --strategy ThreeFilterEMA_BTC_15 --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json -e 200 --timerange=20251001-20260101
# sudo docker compose run --rm chan_btc backtesting -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json --strategy ThreeFilterEMA_BTC_15 --strategy-path ./user_data/Chan/strategies --timerange=20250101-
# sudo docker compose run --rm chan_btc download-data -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json --pairs BTC/USDT:USDT -t 15m --timerange 20240101-
# sudo docker compose run --rm chan_btc trade -c ./user_data/Chan/config/ThreeFilterEMA_BTC_15.json --strategy ThreeFilterEMA_BTC_15 --strategy-path ./user_data/Chan/strategies
class ThreeFilterEMA_BTC_15(IStrategy):
INTERFACE_VERSION: int = 3
# ==================== 参数设置 ====================
# 均线参数
ema156_length = IntParameter(100, 200, default=156, space="buy", optimize=True)
ema52_length = IntParameter(30, 80, default=52, space="buy", optimize=True)
# MACD参数
macd_fast = IntParameter(8, 16, default=12, space="buy", optimize=True)
macd_slow = IntParameter(20, 32, default=26, space="buy", optimize=True)
macd_signal = IntParameter(6, 12, default=9, space="buy", optimize=True)
# 策略参数
confirm_bars = IntParameter(1, 10, default=3, space="buy", optimize=True) # 金叉/死叉后确认K线数
breakout_bars = IntParameter(5, 20, default=10, space="buy", optimize=True) # 突破EMA52确认K线数
risk_reward_ratio = DecimalParameter(1.0, 5.0, default=2.0, decimals=1, space="sell", optimize=True) # 盈亏比
lookback_period = IntParameter(10, 30, default=20, space="sell", optimize=True) # 止损回看周期
# ROI设置
minimal_roi = {
"0": 0.15,
"120": 0.08,
"240": 0.04,
"480": 0.02,
"720": 0
}
# 止损设置 (默认禁用,使用custom_stoploss)
stoploss = -0.15
use_custom_stoploss = True
# 是否支持做空
can_short = True
# 杠杆
leverage_value = 1.0
# 是否启用追踪止损
trailing_stop = False
trailing_stop_positive = 0.03
trailing_stop_positive_offset = 0.05
trailing_only_offset_is_reached = False
# 启动所需K线数量
startup_candle_count = 200
# 时间框架
timeframe = '15m'
# 用于存储止损止盈价格
custom_trade_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""计算所有技术指标"""
# ==================== 计算均线 ====================
dataframe['ema156'] = ta.EMA(dataframe, timeperiod=self.ema156_length.value)
dataframe['ema52'] = ta.EMA(dataframe, timeperiod=self.ema52_length.value)
# ==================== 计算MACD ====================
macd = ta.MACD(dataframe,
fastperiod=self.macd_fast.value,
slowperiod=self.macd_slow.value,
signalperiod=self.macd_signal.value)
dataframe['macd'] = macd['macd']
dataframe['macd_signal'] = macd['macdsignal']
dataframe['macd_hist'] = macd['macdhist']
# MACD金叉和死叉
dataframe['macd_golden_cross'] = (
(dataframe['macd'] > dataframe['macd_signal']) &
(dataframe['macd'].shift(1) <= dataframe['macd_signal'].shift(1))
).astype(int)
dataframe['macd_death_cross'] = (
(dataframe['macd'] < dataframe['macd_signal']) &
(dataframe['macd'].shift(1) >= dataframe['macd_signal'].shift(1))
).astype(int)
# ==================== 趋势判断 ====================
# 大趋势:价格相对于EMA156的位置
dataframe['big_trend_bullish'] = (dataframe['close'] > dataframe['ema156']).astype(int)
dataframe['big_trend_bearish'] = (dataframe['close'] < dataframe['ema156']).astype(int)
# 小趋势:价格相对于EMA52的位置
dataframe['small_trend_bullish'] = (dataframe['close'] > dataframe['ema52']).astype(int)
dataframe['small_trend_bearish'] = (dataframe['close'] < dataframe['ema52']).astype(int)
# ==================== 金叉/死叉后趋势确认 ====================
confirm_bars = self.confirm_bars.value
breakout_bars = self.breakout_bars.value
# 计算距离最近金叉的K线数
dataframe['bars_since_golden'] = self._calculate_bars_since(dataframe, 'macd_golden_cross')
# 计算距离最近死叉的K线数
dataframe['bars_since_death'] = self._calculate_bars_since(dataframe, 'macd_death_cross')
# 记录金叉/死叉时的价格
dataframe['golden_cross_price'] = self._get_cross_price(dataframe, 'macd_golden_cross')
dataframe['death_cross_price'] = self._get_cross_price(dataframe, 'macd_death_cross')
# 检查金叉后confirm_bars根K线是否持续上涨
dataframe['golden_cross_confirmed'] = self._check_golden_cross_confirmed(
dataframe, confirm_bars)
# 检查死叉后confirm_bars根K线是否持续下跌
dataframe['death_cross_confirmed'] = self._check_death_cross_confirmed(
dataframe, confirm_bars)
# ==================== 价格站稳EMA52确认 ====================
# 检查价格是否在近breakout_bars根K线内站稳EMA52上方
dataframe['price_above_ema52_stable'] = self._check_price_above_ema52_stable(
dataframe, breakout_bars)
# 检查价格是否在近breakout_bars根K线内站稳EMA52下方
dataframe['price_below_ema52_stable'] = self._check_price_below_ema52_stable(
dataframe, breakout_bars)
# 检测EMA52突破
dataframe['ema52_breakout_up'] = (
(dataframe['close'] > dataframe['ema52']) &
(dataframe['close'].shift(1) <= dataframe['ema52'].shift(1))
).astype(int)
dataframe['ema52_breakout_down'] = (
(dataframe['close'] < dataframe['ema52']) &
(dataframe['close'].shift(1) >= dataframe['ema52'].shift(1))
).astype(int)
# 近期是否有EMA52突破
dataframe['recent_ema52_breakout_up'] = dataframe['ema52_breakout_up'].rolling(
window=breakout_bars).sum().fillna(0) > 0
dataframe['recent_ema52_breakout_down'] = dataframe['ema52_breakout_down'].rolling(
window=breakout_bars).sum().fillna(0) > 0
# ==================== 计算回调低点/高点作为止损 ====================
lookback = self.lookback_period.value
dataframe['swing_low'] = dataframe['low'].rolling(window=lookback).min()
dataframe['swing_high'] = dataframe['high'].rolling(window=lookback).max()
return dataframe
def _calculate_bars_since(self, dataframe: DataFrame, column: str) -> np.ndarray:
"""计算距离最近信号的K线数"""
result = np.zeros(len(dataframe))
bars_count = np.nan
for i in range(len(dataframe)):
if dataframe[column].iloc[i] == 1:
bars_count = 0
elif not np.isnan(bars_count):
bars_count += 1
result[i] = bars_count
return result
def _get_cross_price(self, dataframe: DataFrame, column: str) -> np.ndarray:
"""获取金叉/死叉时的价格"""
result = np.full(len(dataframe), np.nan)
cross_price = np.nan
for i in range(len(dataframe)):
if dataframe[column].iloc[i] == 1:
cross_price = dataframe['close'].iloc[i]
result[i] = cross_price
return result
def _check_golden_cross_confirmed(self, dataframe: DataFrame, confirm_bars: int) -> np.ndarray:
"""检查金叉后confirm_bars根K线是否持续上涨"""
result = np.zeros(len(dataframe), dtype=bool)
for i in range(confirm_bars + 5, len(dataframe)):
bars_since = dataframe['bars_since_golden'].iloc[i]
if np.isnan(bars_since):
continue
bars_since = int(bars_since)
if confirm_bars <= bars_since <= confirm_bars + 5:
golden_price = dataframe['golden_cross_price'].iloc[i]
if np.isnan(golden_price):
continue
# 检查金叉后的K线是否按上涨趋势运行
trend_up = True
for j in range(1, min(confirm_bars + 1, bars_since + 1)):
idx = i - (bars_since - j)
if 0 <= idx < len(dataframe):
if dataframe['close'].iloc[idx] < golden_price:
trend_up = False
break
result[i] = trend_up
return result
def _check_death_cross_confirmed(self, dataframe: DataFrame, confirm_bars: int) -> np.ndarray:
"""检查死叉后confirm_bars根K线是否持续下跌"""
result = np.zeros(len(dataframe), dtype=bool)
for i in range(confirm_bars + 5, len(dataframe)):
bars_since = dataframe['bars_since_death'].iloc[i]
if np.isnan(bars_since):
continue
bars_since = int(bars_since)
if confirm_bars <= bars_since <= confirm_bars + 5:
death_price = dataframe['death_cross_price'].iloc[i]
if np.isnan(death_price):
continue
# 检查死叉后的K线是否按下跌趋势运行
trend_down = True
for j in range(1, min(confirm_bars + 1, bars_since + 1)):
idx = i - (bars_since - j)
if 0 <= idx < len(dataframe):
if dataframe['close'].iloc[idx] > death_price:
trend_down = False
break
result[i] = trend_down
return result
def _check_price_above_ema52_stable(self, dataframe: DataFrame, breakout_bars: int) -> np.ndarray:
"""检查价格是否在近breakout_bars根K线内站稳EMA52上方"""
result = np.ones(len(dataframe), dtype=bool)
for i in range(breakout_bars, len(dataframe)):
for j in range(breakout_bars):
idx = i - j
if dataframe['close'].iloc[idx] < dataframe['ema52'].iloc[idx]:
result[i] = False
break
return result
def _check_price_below_ema52_stable(self, dataframe: DataFrame, breakout_bars: int) -> np.ndarray:
"""检查价格是否在近breakout_bars根K线内站稳EMA52下方"""
result = np.ones(len(dataframe), dtype=bool)
for i in range(breakout_bars, len(dataframe)):
for j in range(breakout_bars):
idx = i - j
if dataframe['close'].iloc[idx] > dataframe['ema52'].iloc[idx]:
result[i] = False
break
return result
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""定义入场条件"""
breakout_bars = self.breakout_bars.value
# ==================== 多头入场条件 ====================
# 1. 大级别为多头趋势(价格在EMA156上方)
# 2. 小级别为多头趋势(价格在EMA52上方)
# 3. MACD金叉已确认 或 最近breakout_bars根K线内有金叉
# 4. 近期有EMA52突破 或 价格站稳EMA52上方
# 5. MACD在信号线上方
long_condition = (
(dataframe['big_trend_bullish'] == 1) &
(dataframe['small_trend_bullish'] == 1) &
(
(dataframe['golden_cross_confirmed'] == True) |
(dataframe['bars_since_golden'] <= breakout_bars)
) &
(
(dataframe['recent_ema52_breakout_up'] == True) |
(dataframe['price_above_ema52_stable'] == True)
) &
(dataframe['macd'] > dataframe['macd_signal'])
)
dataframe.loc[long_condition, ['enter_long', 'enter_tag']] = (1, 'three_filter_long')
# ==================== 空头入场条件 ====================
# 1. 大级别为空头趋势(价格在EMA156下方)
# 2. 小级别为空头趋势(价格在EMA52下方)
# 3. MACD死叉已确认 或 最近breakout_bars根K线内有死叉
# 4. 近期有EMA52跌破 或 价格站稳EMA52下方
# 5. MACD在信号线下方
short_condition = (
(dataframe['big_trend_bearish'] == 1) &
(dataframe['small_trend_bearish'] == 1) &
(
(dataframe['death_cross_confirmed'] == True) |
(dataframe['bars_since_death'] <= breakout_bars)
) &
(
(dataframe['recent_ema52_breakout_down'] == True) |
(dataframe['price_below_ema52_stable'] == True)
) &
(dataframe['macd'] < dataframe['macd_signal'])
)
dataframe.loc[short_condition, ['enter_short', 'enter_tag']] = (1, 'three_filter_short')
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""定义出场条件 - 基于趋势反转"""
# 多头出场:小趋势转空或MACD死叉
long_exit_condition = (
(dataframe['small_trend_bearish'] == 1) |
(dataframe['macd_death_cross'] == 1)
)
dataframe.loc[long_exit_condition, ['exit_long', 'exit_tag']] = (1, 'trend_reversal_exit')
# 空头出场:小趋势转多或MACD金叉
short_exit_condition = (
(dataframe['small_trend_bullish'] == 1) |
(dataframe['macd_golden_cross'] == 1)
)
dataframe.loc[short_exit_condition, ['exit_short', 'exit_tag']] = (1, 'trend_reversal_exit')
return dataframe
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
side: str, **kwargs) -> bool:
"""确认交易入场时,计算并存储止损止盈价格"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty:
return True
last_candle = dataframe.iloc[-1]
risk_reward = self.risk_reward_ratio.value
if side == 'long':
stop_loss = last_candle['swing_low']
risk = rate - stop_loss
if risk > 0:
take_profit = rate + risk * risk_reward
self.custom_trade_info[pair] = {
'stop_loss': stop_loss,
'take_profit': take_profit,
'entry_price': rate
}
#logger.info(f"Long entry: {pair} @ {rate}, SL: {stop_loss}, TP: {take_profit}")
else:
# 如果风险为0或负数,不进入交易
#logger.warning(f"Invalid risk for long entry: {pair}, risk={risk}")
return False
else: # short
stop_loss = last_candle['swing_high']
risk = stop_loss - rate
if risk > 0:
take_profit = rate - risk * risk_reward
self.custom_trade_info[pair] = {
'stop_loss': stop_loss,
'take_profit': take_profit,
'entry_price': rate
}
#logger.info(f"Short entry: {pair} @ {rate}, SL: {stop_loss}, TP: {take_profit}")
else:
# 如果风险为0或负数,不进入交易
#logger.warning(f"Invalid risk for short entry: {pair}, risk={risk}")
return False
return True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, after_fill: bool,
**kwargs) -> Optional[float]:
"""自定义止损逻辑"""
if pair not in self.custom_trade_info:
return None
trade_info = self.custom_trade_info[pair]
stop_loss = trade_info.get('stop_loss')
entry_price = trade_info.get('entry_price')
if stop_loss is None or entry_price is None:
return None
if trade.is_short:
# 空头止损:当前价格 >= 止损价格
if current_rate >= stop_loss:
return -0.0001 # 触发止损
# 计算止损百分比
sl_pct = (stop_loss - entry_price) / entry_price
return sl_pct
else:
# 多头止损:当前价格 <= 止损价格
if current_rate <= stop_loss:
return -0.0001 # 触发止损
# 计算止损百分比
sl_pct = (entry_price - stop_loss) / entry_price
return -sl_pct
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> Optional[str]:
"""自定义出场逻辑 - 止盈"""
if pair not in self.custom_trade_info:
return None
trade_info = self.custom_trade_info[pair]
take_profit = trade_info.get('take_profit')
if take_profit is None:
return None
if trade.is_short:
# 空头止盈:当前价格 <= 止盈价格
if current_rate <= take_profit:
return 'take_profit'
else:
# 多头止盈:当前价格 >= 止盈价格
if current_rate >= take_profit:
return 'take_profit'
return None
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
side: str, **kwargs) -> float:
"""返回杠杆倍数"""
return self.leverage_value
def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime,
proposed_rate: float, current_profit: float,
exit_tag: Optional[str], **kwargs) -> float:
"""自定义出场价格,减少滑点"""
# 使用提议价格,可根据需要调整
return proposed_rate
def trade_exit_confirm(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool:
"""交易退出确认,清理自定义交易信息"""
if pair in self.custom_trade_info:
del self.custom_trade_info[pair]
return True
+1 -1
View File
@@ -65,7 +65,7 @@ DEFAULT_TIMEFRAME_LABELS = OrderedDict([
])
DEFAULT_SYMBOLS = [
'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT',
'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT', 'WIF/USDT:USDT',
'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT'
]
+3 -2
View File
@@ -5885,8 +5885,9 @@
const defaultMAs = [
{ type: 'EMA', length: 52, color: '#800080', name: 'EMA52' }, // 紫色
{ type: 'EMA', length: 24, color: '#008000', name: 'EMA24' }, // 深绿色
{ type: 'SMA', length: 30, color: '#FF8C00', name: 'SMA30' }, // 橙色
{ type: 'SMA', length: 250, color: '#1E90FF', name: 'SMA250' } // 蓝色
{ type: 'EMA', length: 104, color: '#FF8C00', name: 'EMA104' }, // 橙色
{ type: 'EMA', length: 156, color: '#1E90FF', name: 'EMA156' }, // 蓝色
{ type: 'EMA', length: 208, color: '#1F004F', name: 'EMA312' } // 蓝色
];
defaultMAs.forEach(ma => {