Merge remote-tracking branch 'origin/main'
# Conflicts: # strategy/data_loader.py # strategy/indicators.py
This commit is contained in:
156
strategy/backtest_2023.py
Normal file
156
strategy/backtest_2023.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
2023 年回测入口 - 用训练出的最优参数在 2023 全年数据上回测
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from strategy.data_loader import load_klines
|
||||
from strategy.indicators import compute_all_indicators
|
||||
from strategy.strategy_signal import (
|
||||
generate_indicator_signals, compute_composite_score,
|
||||
apply_htf_filter,
|
||||
)
|
||||
from strategy.backtest_engine import BacktestEngine
|
||||
|
||||
|
||||
def main():
|
||||
# 加载最佳参数
|
||||
params_path = os.path.join(os.path.dirname(__file__), 'best_params_2020_2022.json')
|
||||
if not os.path.exists(params_path):
|
||||
print(f"错误: 找不到参数文件 {params_path}")
|
||||
print("请先运行 train.py 进行训练")
|
||||
return
|
||||
|
||||
with open(params_path, 'r') as f:
|
||||
params = json.load(f)
|
||||
|
||||
print("=" * 70)
|
||||
print("2023 年真实回测 (样本外)")
|
||||
print("=" * 70)
|
||||
|
||||
# 加载数据 (多加载一些前置数据用于指标预热)
|
||||
print("\n加载数据...")
|
||||
df_5m = load_klines('5m', '2022-11-01', '2024-01-01')
|
||||
df_1h = load_klines('1h', '2022-11-01', '2024-01-01')
|
||||
print(f" 5m: {len(df_5m)} 条, 1h: {len(df_1h)} 条")
|
||||
|
||||
# 计算指标
|
||||
print("计算指标...")
|
||||
df_5m = compute_all_indicators(df_5m, params)
|
||||
df_1h = compute_all_indicators(df_1h, params)
|
||||
|
||||
# 生成信号
|
||||
print("生成信号...")
|
||||
df_5m = generate_indicator_signals(df_5m, params)
|
||||
df_1h = generate_indicator_signals(df_1h, params)
|
||||
|
||||
# 综合得分
|
||||
score = compute_composite_score(df_5m, params)
|
||||
score = apply_htf_filter(score, df_1h, params)
|
||||
|
||||
# 截取 2023 年数据
|
||||
mask = (df_5m.index >= '2023-01-01') & (df_5m.index < '2024-01-01')
|
||||
df_2023 = df_5m.loc[mask]
|
||||
score_2023 = score.loc[mask]
|
||||
print(f" 2023年数据: {len(df_2023)} 条")
|
||||
|
||||
# 回测
|
||||
print("\n开始回测...")
|
||||
engine = BacktestEngine(
|
||||
initial_capital=1000.0,
|
||||
margin_per_trade=25.0,
|
||||
leverage=50,
|
||||
fee_rate=0.0005,
|
||||
rebate_ratio=0.70,
|
||||
max_daily_drawdown=50.0,
|
||||
min_hold_bars=1,
|
||||
stop_loss_pct=params['stop_loss_pct'],
|
||||
take_profit_pct=params['take_profit_pct'],
|
||||
max_positions=int(params.get('max_positions', 3)),
|
||||
)
|
||||
|
||||
result = engine.run(df_2023, score_2023, open_threshold=params['open_threshold'])
|
||||
|
||||
# ============================================================
|
||||
# 输出结果
|
||||
# ============================================================
|
||||
print("\n" + "=" * 70)
|
||||
print("2023 年回测结果")
|
||||
print("=" * 70)
|
||||
print(f" 初始资金: 1000.00 U")
|
||||
print(f" 最终资金: {result['final_capital']:.2f} U")
|
||||
print(f" 总收益: {result['total_pnl']:.2f} U")
|
||||
print(f" 总手续费: {result['total_fee']:.2f} U")
|
||||
print(f" 总返佣: {result['total_rebate']:.2f} U")
|
||||
print(f" 交易次数: {result['num_trades']}")
|
||||
print(f" 胜率: {result['win_rate']:.2%}")
|
||||
print(f" 盈亏比: {result['profit_factor']:.2f}")
|
||||
print(f" 日均收益: {result['avg_daily_pnl']:.2f} U")
|
||||
print(f" 最大日回撤: {result['max_daily_dd']:.2f} U")
|
||||
|
||||
# 月度统计
|
||||
daily_pnl = result['daily_pnl']
|
||||
if daily_pnl:
|
||||
df_daily = pd.DataFrame(list(daily_pnl.items()), columns=['date', 'pnl'])
|
||||
df_daily['date'] = pd.to_datetime(df_daily['date'])
|
||||
df_daily['month'] = df_daily['date'].dt.to_period('M')
|
||||
monthly = df_daily.groupby('month')['pnl'].agg(['sum', 'count', 'mean', 'min'])
|
||||
monthly.columns = ['月收益', '交易天数', '日均收益', '最大日亏损']
|
||||
|
||||
print("\n" + "-" * 70)
|
||||
print("月度统计:")
|
||||
print("-" * 70)
|
||||
for idx, row in monthly.iterrows():
|
||||
status = "✅" if row['月收益'] > 0 else "❌"
|
||||
dd_status = "✅" if row['最大日亏损'] > -50 else "⚠️"
|
||||
print(f" {idx} | 收益: {row['月收益']:>8.2f}U | "
|
||||
f"日均: {row['日均收益']:>7.2f}U | "
|
||||
f"最大日亏: {row['最大日亏损']:>7.2f}U {dd_status} | {status}")
|
||||
|
||||
# 日均收益是否达标
|
||||
avg_daily = df_daily['pnl'].mean()
|
||||
days_above_50 = (df_daily['pnl'] >= 50).sum()
|
||||
days_below_neg50 = (df_daily['pnl'] < -50).sum()
|
||||
print(f"\n 日均收益: {avg_daily:.2f}U {'✅ 达标' if avg_daily >= 50 else '❌ 未达标'}")
|
||||
print(f" 日收益>=50U的天数: {days_above_50} / {len(df_daily)}")
|
||||
print(f" 日回撤>50U的天数: {days_below_neg50} / {len(df_daily)}")
|
||||
|
||||
# 保存逐日 PnL
|
||||
output_dir = os.path.dirname(__file__)
|
||||
if daily_pnl:
|
||||
df_daily_out = pd.DataFrame(list(daily_pnl.items()), columns=['date', 'pnl'])
|
||||
df_daily_out['cumulative_pnl'] = df_daily_out['pnl'].cumsum()
|
||||
daily_csv = os.path.join(output_dir, 'backtest_2023_daily_pnl.csv')
|
||||
df_daily_out.to_csv(daily_csv, index=False)
|
||||
print(f"\n逐日PnL已保存: {daily_csv}")
|
||||
|
||||
# 保存交易记录
|
||||
if result['trades']:
|
||||
trades_data = []
|
||||
for t in result['trades']:
|
||||
trades_data.append({
|
||||
'entry_time': t.entry_time,
|
||||
'exit_time': t.exit_time,
|
||||
'direction': '多' if t.direction == 1 else '空',
|
||||
'entry_price': t.entry_price,
|
||||
'exit_price': t.exit_price,
|
||||
'pnl': round(t.pnl, 4),
|
||||
'fee': round(t.fee, 4),
|
||||
'rebate': round(t.rebate, 4),
|
||||
'holding_bars': t.holding_bars,
|
||||
})
|
||||
df_trades = pd.DataFrame(trades_data)
|
||||
trades_csv = os.path.join(output_dir, 'backtest_2023_trades.csv')
|
||||
df_trades.to_csv(trades_csv, index=False)
|
||||
print(f"交易记录已保存: {trades_csv}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
366
strategy/backtest_2023_daily_pnl.csv
Normal file
366
strategy/backtest_2023_daily_pnl.csv
Normal file
@@ -0,0 +1,366 @@
|
||||
date,pnl,cumulative_pnl
|
||||
2023-01-01,-13.24030780391167,-13.24030780391167
|
||||
2023-01-02,-6.446144625588336,-19.686452429500005
|
||||
2023-01-03,1.604645378778668,-18.081807050721338
|
||||
2023-01-04,56.24999999999943,38.168192949278094
|
||||
2023-01-05,0.33406192478092844,38.50225487405902
|
||||
2023-01-06,-4.28936549280675,34.21288938125227
|
||||
2023-01-07,14.786004985419915,48.99889436667218
|
||||
2023-01-08,74.57347500987392,123.5723693765461
|
||||
2023-01-09,100.43932814185042,224.01169751839655
|
||||
2023-01-10,-11.400950315324355,212.61074720307218
|
||||
2023-01-11,119.73808656927638,332.34883377234854
|
||||
2023-01-12,-46.87500000000008,285.4738337723485
|
||||
2023-01-13,24.95301706493165,310.42685083728014
|
||||
2023-01-14,36.87499999999923,347.3018508372794
|
||||
2023-01-15,-42.164085212923396,305.137765624356
|
||||
2023-01-16,-42.37550401931327,262.76226160504274
|
||||
2023-01-17,-39.23788027501831,223.52438133002443
|
||||
2023-01-18,10.968990345277774,234.4933716753022
|
||||
2023-01-19,-17.519099732290336,216.97427194301187
|
||||
2023-01-20,258.1249999999992,475.0992719430111
|
||||
2023-01-21,-26.63800028548694,448.46127165752415
|
||||
2023-01-22,-27.862031912052718,420.59923974547144
|
||||
2023-01-23,-40.570948096946864,380.0282916485246
|
||||
2023-01-24,153.52954075490175,533.5578324034263
|
||||
2023-01-25,76.9770888682049,610.5349212716312
|
||||
2023-01-26,-43.12500000000006,567.4099212716312
|
||||
2023-01-27,4.574004296634158,571.9839255682654
|
||||
2023-01-28,110.89323045990369,682.8771560281691
|
||||
2023-01-29,65.39337129909306,748.2705273272621
|
||||
2023-01-30,89.15573047359129,837.4262578008534
|
||||
2023-01-31,-33.65661196432259,803.7696458365308
|
||||
2023-02-01,-42.346269783529834,761.423376053001
|
||||
2023-02-02,5.6249999999996945,767.0483760530007
|
||||
2023-02-03,-38.015635992701625,729.032740060299
|
||||
2023-02-04,-6.061609860875804,722.9711301994232
|
||||
2023-02-05,91.32767729532108,814.2988074947443
|
||||
2023-02-06,12.340770370556534,826.6395778653009
|
||||
2023-02-07,-38.31565728321921,788.3239205820817
|
||||
2023-02-08,-10.754771409427375,777.5691491726543
|
||||
2023-02-09,180.00000000000034,957.5691491726546
|
||||
2023-02-10,90.62500000000017,1048.1941491726548
|
||||
2023-02-11,-40.23338038017373,1007.9607687924811
|
||||
2023-02-12,50.843005130216255,1058.8037739226972
|
||||
2023-02-13,-43.12499999999999,1015.6787739226972
|
||||
2023-02-14,-45.000000000000014,970.6787739226972
|
||||
2023-02-15,211.8749999999989,1182.553773922696
|
||||
2023-02-16,123.74999999999923,1306.3037739226954
|
||||
2023-02-17,-43.1250000000002,1263.1787739226952
|
||||
2023-02-18,-30.38874055427764,1232.7900333684177
|
||||
2023-02-19,2.5357914472640948,1235.3258248156817
|
||||
2023-02-20,10.13433097511785,1245.4601557907995
|
||||
2023-02-21,41.5960974135976,1287.056253204397
|
||||
2023-02-22,39.9999999999999,1327.056253204397
|
||||
2023-02-23,-13.478092269295054,1313.578160935102
|
||||
2023-02-24,-33.61127571949922,1279.9668852156028
|
||||
2023-02-25,56.87500000000026,1336.841885215603
|
||||
2023-02-26,52.96295237320025,1389.8048375888034
|
||||
2023-02-27,-21.012957685691507,1368.7918799031117
|
||||
2023-02-28,-17.510525954953838,1351.2813539481579
|
||||
2023-03-01,48.43861521106629,1399.719969159224
|
||||
2023-03-02,5.370115987703678,1405.0900851469278
|
||||
2023-03-03,114.81346298981391,1519.9035481367416
|
||||
2023-03-04,-9.975555145718014,1509.9279929910235
|
||||
2023-03-05,-40.070176144315376,1469.8578168467081
|
||||
2023-03-06,-11.368260314393366,1458.4895565323147
|
||||
2023-03-07,29.800169999101556,1488.2897265314164
|
||||
2023-03-08,-41.066516251938125,1447.2232102794783
|
||||
2023-03-09,178.12499999999966,1625.3482102794778
|
||||
2023-03-10,69.54272408495805,1694.890934364436
|
||||
2023-03-11,218.8441938252083,1913.7351281896442
|
||||
2023-03-12,-43.124999999999986,1870.6101281896442
|
||||
2023-03-13,31.874999999999282,1902.4851281896435
|
||||
2023-03-14,71.24999999999882,1973.7351281896424
|
||||
2023-03-15,-46.87499999999994,1926.8601281896424
|
||||
2023-03-16,-47.19007322239124,1879.6700549672512
|
||||
2023-03-17,249.37499999999858,2129.0450549672496
|
||||
2023-03-18,-39.375000000000455,2089.670054967249
|
||||
2023-03-19,-37.72054430463878,2051.9495106626105
|
||||
2023-03-20,-48.125000000000256,2003.8245106626102
|
||||
2023-03-21,-48.955919791756116,1954.8685908708542
|
||||
2023-03-22,-41.34851470243514,1913.520076168419
|
||||
2023-03-23,-40.53293708055611,1872.987139087863
|
||||
2023-03-24,-40.62177198705648,1832.3653671008065
|
||||
2023-03-25,-12.808008186129763,1819.5573589146768
|
||||
2023-03-26,44.60456842447671,1864.1619273391534
|
||||
2023-03-27,33.10077209532424,1897.2626994344776
|
||||
2023-03-28,-40.54590870106257,1856.7167907334149
|
||||
2023-03-29,32.68290319879213,1889.399693932207
|
||||
2023-03-30,-42.18093929678437,1847.2187546354226
|
||||
2023-03-31,-30.18435407486843,1817.034400560554
|
||||
2023-04-01,-18.629080327821725,1798.4053202327323
|
||||
2023-04-02,78.36987927566116,1876.7751995083934
|
||||
2023-04-03,-14.310148754581672,1862.4650507538117
|
||||
2023-04-04,67.49999999999953,1929.9650507538113
|
||||
2023-04-05,40.981623971671745,1970.946674725483
|
||||
2023-04-06,27.071471993660218,1998.0181467191433
|
||||
2023-04-07,69.95707277743023,2067.9752194965736
|
||||
2023-04-08,16.74780048539444,2084.723019981968
|
||||
2023-04-09,47.28635900754121,2132.009378989509
|
||||
2023-04-10,92.25687955666356,2224.2662585461726
|
||||
2023-04-11,13.118453933996253,2237.3847124801687
|
||||
2023-04-12,56.24999999999995,2293.6347124801687
|
||||
2023-04-13,179.99999999999943,2473.634712480168
|
||||
2023-04-14,-21.87500000000091,2451.7597124801673
|
||||
2023-04-15,0.6060490696602718,2452.3657615498278
|
||||
2023-04-16,69.62762541934998,2521.9933869691777
|
||||
2023-04-17,65.86212708679903,2587.8555140559765
|
||||
2023-04-18,11.498179506746062,2599.3536935627226
|
||||
2023-04-19,223.1815368848387,2822.5352304475614
|
||||
2023-04-20,-43.12499999999999,2779.4102304475614
|
||||
2023-04-21,31.875000000000142,2811.2852304475614
|
||||
2023-04-22,-29.84738767205142,2781.43784277551
|
||||
2023-04-23,13.154733757123939,2794.5925765326338
|
||||
2023-04-24,6.396324986843672,2800.9889015194776
|
||||
2023-04-25,123.74011330692568,2924.7290148264033
|
||||
2023-04-26,261.41402100210826,3186.1430358285115
|
||||
2023-04-27,24.10664119340748,3210.249677021919
|
||||
2023-04-28,-10.62612672957838,3199.6235502923405
|
||||
2023-04-29,23.597281085864836,3223.2208313782053
|
||||
2023-04-30,-44.289545693205795,3178.9312856849997
|
||||
2023-05-01,54.37500000000002,3233.3062856849997
|
||||
2023-05-02,-2.017144354733148,3231.2891413302664
|
||||
2023-05-03,81.77174013147513,3313.0608814617417
|
||||
2023-05-04,-25.04485318862535,3288.0160282731163
|
||||
2023-05-05,171.61321250444536,3459.629240777562
|
||||
2023-05-06,52.49999999999993,3512.129240777562
|
||||
2023-05-07,-33.168505868798924,3478.960734908763
|
||||
2023-05-08,8.124999999999917,3487.085734908763
|
||||
2023-05-09,-12.584917211039063,3474.5008176977235
|
||||
2023-05-10,52.019173246963206,3526.519990944687
|
||||
2023-05-11,90.00000000000003,3616.519990944687
|
||||
2023-05-12,126.17158084605737,3742.691571790744
|
||||
2023-05-13,8.545669463922014,3751.237241254666
|
||||
2023-05-14,1.8924095106008352,3753.129650765267
|
||||
2023-05-15,10.943496294234567,3764.0731470595015
|
||||
2023-05-16,-30.264218933238727,3733.808928126263
|
||||
2023-05-17,30.026157944387133,3763.83508607065
|
||||
2023-05-18,23.13394310654973,3786.9690291772
|
||||
2023-05-19,15.149354042131993,3802.118383219332
|
||||
2023-05-20,-16.41923661401725,3785.6991466053146
|
||||
2023-05-21,7.846127328452241,3793.545273933767
|
||||
2023-05-22,21.279945465818493,3814.8252193995854
|
||||
2023-05-23,70.02437426214179,3884.8495936617273
|
||||
2023-05-24,72.70217181804935,3957.551765479777
|
||||
2023-05-25,-16.75708025521019,3940.794685224567
|
||||
2023-05-26,18.74999999999976,3959.5446852245664
|
||||
2023-05-27,39.20117578898695,3998.745861013553
|
||||
2023-05-28,110.91041187532412,4109.6562728888775
|
||||
2023-05-29,-32.49999999999996,4077.1562728888775
|
||||
2023-05-30,10.449185839751497,4087.605458728629
|
||||
2023-05-31,-10.136360635569764,4077.469098093059
|
||||
2023-06-01,14.358833920817762,4091.8279320138768
|
||||
2023-06-02,65.90793376617691,4157.735865780053
|
||||
2023-06-03,2.796329514545665,4160.532195294599
|
||||
2023-06-04,-6.651917557240656,4153.8802777373585
|
||||
2023-06-05,168.7500000000002,4322.6302777373585
|
||||
2023-06-06,-43.12500000000005,4279.5052777373585
|
||||
2023-06-07,34.36109954050025,4313.866377277859
|
||||
2023-06-08,-25.452505493132982,4288.413871784726
|
||||
2023-06-09,7.08099194612123,4295.494863730848
|
||||
2023-06-10,121.25000000000003,4416.744863730848
|
||||
2023-06-11,-20.937848671894088,4395.807015058954
|
||||
2023-06-12,-21.37821245611659,4374.428802602837
|
||||
2023-06-13,-13.703676058378367,4360.725126544458
|
||||
2023-06-14,166.27160175206745,4526.996728296526
|
||||
2023-06-15,15.069476387286578,4542.066204683812
|
||||
2023-06-16,106.96240346155682,4649.02860814537
|
||||
2023-06-17,-11.250000000000341,4637.77860814537
|
||||
2023-06-18,-10.120078428463955,4627.658529716906
|
||||
2023-06-19,-38.97345934789618,4588.68507036901
|
||||
2023-06-20,42.86461601305588,4631.549686382065
|
||||
2023-06-21,187.49999999999937,4819.0496863820645
|
||||
2023-06-22,31.80896255236462,4850.858648934429
|
||||
2023-06-23,23.85220828852076,4874.71085722295
|
||||
2023-06-24,-30.371879505034936,4844.338977717915
|
||||
2023-06-25,12.45391150222293,4856.792889220138
|
||||
2023-06-26,78.4973773289051,4935.290266549043
|
||||
2023-06-27,-39.87042272482522,4895.419843824217
|
||||
2023-06-28,88.12499999999999,4983.544843824217
|
||||
2023-06-29,-40.51516243372697,4943.029681390491
|
||||
2023-06-30,105.46931766571112,5048.498999056202
|
||||
2023-07-01,-13.796315557439133,5034.702683498763
|
||||
2023-07-02,-41.59344453195906,4993.109238966804
|
||||
2023-07-03,28.89550853439942,5022.004747501203
|
||||
2023-07-04,-5.3332824068334626,5016.6714650943695
|
||||
2023-07-05,90.00000000000007,5106.6714650943695
|
||||
2023-07-06,65.47404542743519,5172.145510521805
|
||||
2023-07-07,-8.375646476725684,5163.769864045079
|
||||
2023-07-08,2.0825098762478143,5165.852373921327
|
||||
2023-07-09,-12.284261589217671,5153.568112332109
|
||||
2023-07-10,4.7069840687455775,5158.275096400855
|
||||
2023-07-11,29.744227743509978,5188.019324144365
|
||||
2023-07-12,13.786259374056783,5201.805583518421
|
||||
2023-07-13,179.07533869993108,5380.880922218353
|
||||
2023-07-14,74.05844966531129,5454.939371883664
|
||||
2023-07-15,-24.570748403829185,5430.368623479834
|
||||
2023-07-16,6.803094664210427,5437.1717181440445
|
||||
2023-07-17,27.410515235666534,5464.582233379711
|
||||
2023-07-18,4.897911857902959,5469.480145237614
|
||||
2023-07-19,6.308694855454021,5475.788840093068
|
||||
2023-07-20,41.93544480774592,5517.7242849008135
|
||||
2023-07-21,16.096325607887692,5533.820610508701
|
||||
2023-07-22,-2.7025252562022395,5531.1180852524985
|
||||
2023-07-23,-36.865989707872636,5494.252095544626
|
||||
2023-07-24,86.80264475362813,5581.054740298255
|
||||
2023-07-25,-24.16611590079934,5556.888624397456
|
||||
2023-07-26,-12.803087110558725,5544.085537286897
|
||||
2023-07-27,29.206711819629362,5573.2922491065265
|
||||
2023-07-28,13.812242058805946,5587.104491165333
|
||||
2023-07-29,11.977355224058611,5599.081846389391
|
||||
2023-07-30,-5.150633707859324,5593.931212681532
|
||||
2023-07-31,-31.586985170360826,5562.344227511171
|
||||
2023-08-01,34.57747987841326,5596.921707389584
|
||||
2023-08-02,19.53988833101348,5616.461595720598
|
||||
2023-08-03,26.963711901014904,5643.425307621613
|
||||
2023-08-04,0.4840259877677071,5643.90933360938
|
||||
2023-08-05,8.851832296486007,5652.761165905866
|
||||
2023-08-06,-4.2929518635524,5648.468214042313
|
||||
2023-08-07,4.423504364914294,5652.891718407227
|
||||
2023-08-08,79.37499999999964,5732.266718407227
|
||||
2023-08-09,-1.0555745361586295,5731.211143871068
|
||||
2023-08-10,-15.802261213704188,5715.408882657364
|
||||
2023-08-11,-19.434997227631737,5695.973885429733
|
||||
2023-08-12,-14.180480342977727,5681.793405086755
|
||||
2023-08-13,-25.33078616920967,5656.462618917545
|
||||
2023-08-14,-29.365151992789308,5627.097466924756
|
||||
2023-08-15,-1.25,5625.847466924756
|
||||
2023-08-16,90.00000000000014,5715.847466924756
|
||||
2023-08-17,181.8750000000003,5897.722466924756
|
||||
2023-08-18,20.625000000000128,5918.347466924756
|
||||
2023-08-19,-27.59032679226934,5890.757140132487
|
||||
2023-08-20,-23.87577257083145,5866.881367561656
|
||||
2023-08-21,-22.59314579170043,5844.288221769955
|
||||
2023-08-22,113.75000000000014,5958.038221769955
|
||||
2023-08-23,8.70637062854804,5966.744592398503
|
||||
2023-08-24,22.993336529216194,5989.73792892772
|
||||
2023-08-25,-41.837123197460336,5947.900805730259
|
||||
2023-08-26,-20.238929404490822,5927.661876325768
|
||||
2023-08-27,-2.006488273327664,5925.655388052441
|
||||
2023-08-28,14.024404963407886,5939.679793015848
|
||||
2023-08-29,143.13137132947674,6082.811164345325
|
||||
2023-08-30,-43.125000000000064,6039.686164345325
|
||||
2023-08-31,31.6249079218707,6071.311072267195
|
||||
2023-09-01,56.25000000000017,6127.561072267195
|
||||
2023-09-02,-27.62863635821808,6099.932435908977
|
||||
2023-09-03,-18.380622263138324,6081.551813645839
|
||||
2023-09-04,-4.629639415293694,6076.922174230545
|
||||
2023-09-05,24.14433818655366,6101.066512417099
|
||||
2023-09-06,-46.72867445132057,6054.337837965779
|
||||
2023-09-07,-3.6562754869909058,6050.681562478788
|
||||
2023-09-08,42.254582050520796,6092.936144529309
|
||||
2023-09-09,-30.94267544057865,6061.99346908873
|
||||
2023-09-10,53.2912479279699,6115.284717016701
|
||||
2023-09-11,146.25000000000028,6261.534717016701
|
||||
2023-09-12,-18.31728723379048,6243.21742978291
|
||||
2023-09-13,-2.141383984249483,6241.07604579866
|
||||
2023-09-14,56.24999999999978,6297.32604579866
|
||||
2023-09-15,-37.98094850138423,6259.345097297276
|
||||
2023-09-16,-33.38082020481237,6225.964277092464
|
||||
2023-09-17,-2.737757522168219,6223.226519570296
|
||||
2023-09-18,28.781772740126208,6252.008292310422
|
||||
2023-09-19,4.273043833846757,6256.281336144269
|
||||
2023-09-20,24.242189180030334,6280.523525324299
|
||||
2023-09-21,90.00000000000011,6370.523525324299
|
||||
2023-09-22,-30.115416338642312,6340.408108985656
|
||||
2023-09-23,-24.28225302122884,6316.1258559644275
|
||||
2023-09-24,-42.98193078708244,6273.143925177345
|
||||
2023-09-25,-5.561737200674414,6267.582187976671
|
||||
2023-09-26,19.48204133300013,6287.064229309671
|
||||
2023-09-27,56.24999999999976,6343.314229309671
|
||||
2023-09-28,109.05931606292351,6452.373545372594
|
||||
2023-09-29,-3.7500000000000906,6448.623545372594
|
||||
2023-09-30,34.11899846667325,6482.7425438392675
|
||||
2023-10-01,94.78119432198413,6577.5237381612515
|
||||
2023-10-02,-43.125,6534.3987381612515
|
||||
2023-10-03,-1.875,6532.5237381612515
|
||||
2023-10-04,60.137202072927415,6592.660940234179
|
||||
2023-10-05,-47.15508896159986,6545.505851272579
|
||||
2023-10-06,-41.50509357550347,6504.000757697076
|
||||
2023-10-07,-21.848785430736374,6482.151972266339
|
||||
2023-10-08,-2.133334555954726,6480.018637710384
|
||||
2023-10-09,120.94752554130996,6600.966163251694
|
||||
2023-10-10,-30.55517668632468,6570.410986565369
|
||||
2023-10-11,12.918899632245676,6583.3298861976145
|
||||
2023-10-12,86.41945469513863,6669.749340892753
|
||||
2023-10-13,-38.92743296812289,6630.82190792463
|
||||
2023-10-14,-9.664398308242207,6621.157509616388
|
||||
2023-10-15,0.32442084565507256,6621.481930462043
|
||||
2023-10-16,17.666860566236,6639.148791028279
|
||||
2023-10-17,71.96782243080129,6711.11661345908
|
||||
2023-10-18,-26.10871664471985,6685.007896814361
|
||||
2023-10-19,27.131717309356045,6712.1396141237165
|
||||
2023-10-20,104.9999999999995,6817.139614123716
|
||||
2023-10-21,95.53563465609497,6912.67524877981
|
||||
2023-10-22,-39.07986275419109,6873.5953860256195
|
||||
2023-10-23,159.37499999999892,7032.970386025619
|
||||
2023-10-24,125.89084826115247,7158.861234286771
|
||||
2023-10-25,-39.7457464430455,7119.1154878437255
|
||||
2023-10-26,76.84599913941024,7195.961486983136
|
||||
2023-10-27,-40.93958561285608,7155.02190137028
|
||||
2023-10-28,-0.12582720298443117,7154.896074167295
|
||||
2023-10-29,1.7653990903884869,7156.661473257684
|
||||
2023-10-30,14.80106712479257,7171.462540382477
|
||||
2023-10-31,-0.7859893532677678,7170.676551029209
|
||||
2023-11-01,45.63294697011628,7216.309497999325
|
||||
2023-11-02,66.38708795013648,7282.696585949461
|
||||
2023-11-03,7.818102574679843,7290.514688524141
|
||||
2023-11-04,78.74999999999962,7369.264688524141
|
||||
2023-11-05,93.530612820923,7462.795301345064
|
||||
2023-11-06,-8.841261133494985,7453.954040211569
|
||||
2023-11-07,13.587699262662081,7467.541739474231
|
||||
2023-11-08,-27.39298530178767,7440.148754172443
|
||||
2023-11-09,233.65340882150477,7673.802162993948
|
||||
2023-11-10,-43.1250000000002,7630.677162993948
|
||||
2023-11-11,65.04467136509504,7695.721834359043
|
||||
2023-11-12,-24.51389709666313,7671.20793726238
|
||||
2023-11-13,6.381878610566607,7677.589815872947
|
||||
2023-11-14,124.7302217309696,7802.320037603917
|
||||
2023-11-15,-15.4428344502457,7786.877203153671
|
||||
2023-11-16,22.788163027994152,7809.665366181665
|
||||
2023-11-17,-40.58878257444296,7769.076583607222
|
||||
2023-11-18,10.05941544996627,7779.135999057188
|
||||
2023-11-19,77.18072310694912,7856.316722164137
|
||||
2023-11-20,-33.719119790508586,7822.597602373628
|
||||
2023-11-21,-39.39362250328517,7783.203979870344
|
||||
2023-11-22,62.49999999999903,7845.703979870343
|
||||
2023-11-23,-43.750000000000135,7801.953979870343
|
||||
2023-11-24,43.12499999999979,7845.078979870343
|
||||
2023-11-25,2.7372815485791326,7847.816261418921
|
||||
2023-11-26,-19.434424922416646,7828.381836496505
|
||||
2023-11-27,74.22705085378658,7902.608887350291
|
||||
2023-11-28,-21.10900157778203,7881.4998857725095
|
||||
2023-11-29,1.584413397259107,7883.084299169768
|
||||
2023-11-30,-5.586662127245132,7877.497637042523
|
||||
2023-12-01,68.65652016602549,7946.154157208548
|
||||
2023-12-02,116.24999999999937,8062.404157208547
|
||||
2023-12-03,48.229742583832405,8110.633899792379
|
||||
2023-12-04,13.529045410407036,8124.162945202786
|
||||
2023-12-05,93.37552564770242,8217.538470850488
|
||||
2023-12-06,-39.37279199427863,8178.1656788562095
|
||||
2023-12-07,160.37637314576938,8338.54205200198
|
||||
2023-12-08,-32.94183697847613,8305.600215023504
|
||||
2023-12-09,-22.342388616671474,8283.257826406832
|
||||
2023-12-10,-29.00075720912898,8254.257069197703
|
||||
2023-12-11,112.22981231135489,8366.486881509058
|
||||
2023-12-12,-46.76135746492969,8319.725524044128
|
||||
2023-12-13,102.21323939340017,8421.938763437529
|
||||
2023-12-14,33.74999999999983,8455.688763437529
|
||||
2023-12-15,63.75916511664332,8519.447928554173
|
||||
2023-12-16,-32.300370998108704,8487.147557556063
|
||||
2023-12-17,17.066032222984127,8504.213589779047
|
||||
2023-12-18,168.68048765961714,8672.894077438665
|
||||
2023-12-19,25.118359089859197,8698.012436528525
|
||||
2023-12-20,-43.080721035673186,8654.931715492852
|
||||
2023-12-21,-31.737797271451193,8623.193918221401
|
||||
2023-12-22,47.49999999999983,8670.693918221401
|
||||
2023-12-23,20.47855147595199,8691.172469697352
|
||||
2023-12-24,108.453203178383,8799.625672875736
|
||||
2023-12-25,-34.74504529363933,8764.880627582097
|
||||
2023-12-26,104.99999999999984,8869.880627582097
|
||||
2023-12-27,204.66554953165667,9074.546177113754
|
||||
2023-12-28,-5.606769621811815,9068.939407491942
|
||||
2023-12-29,220.95172689013637,9289.891134382078
|
||||
2023-12-30,-15.105228436109885,9274.785905945968
|
||||
2023-12-31,-0.005881281931285898,9274.780024664036
|
||||
|
3657
strategy/backtest_2023_trades.csv
Normal file
3657
strategy/backtest_2023_trades.csv
Normal file
File diff suppressed because it is too large
Load Diff
298
strategy/backtest_engine.py
Normal file
298
strategy/backtest_engine.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
回测引擎 - 完整模拟手续费、返佣延迟到账、每日回撤限制、持仓时间约束
|
||||
支持同时持有多单并发,严格控制每日最大回撤
|
||||
"""
|
||||
import datetime
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trade:
|
||||
entry_time: pd.Timestamp
|
||||
exit_time: Optional[pd.Timestamp] = None
|
||||
direction: int = 0
|
||||
entry_price: float = 0.0
|
||||
exit_price: float = 0.0
|
||||
pnl: float = 0.0
|
||||
fee: float = 0.0
|
||||
rebate: float = 0.0
|
||||
holding_bars: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenPosition:
|
||||
direction: int = 0
|
||||
entry_price: float = 0.0
|
||||
entry_time: pd.Timestamp = None
|
||||
hold_bars: int = 0
|
||||
|
||||
|
||||
class BacktestEngine:
|
||||
def __init__(
|
||||
self,
|
||||
initial_capital: float = 1000.0,
|
||||
margin_per_trade: float = 25.0,
|
||||
leverage: int = 50,
|
||||
fee_rate: float = 0.0005,
|
||||
rebate_ratio: float = 0.70,
|
||||
max_daily_drawdown: float = 50.0,
|
||||
min_hold_bars: int = 1,
|
||||
stop_loss_pct: float = 0.005,
|
||||
take_profit_pct: float = 0.01,
|
||||
max_positions: int = 3,
|
||||
):
|
||||
self.initial_capital = initial_capital
|
||||
self.margin = margin_per_trade
|
||||
self.leverage = leverage
|
||||
self.notional = margin_per_trade * leverage
|
||||
self.fee_rate = fee_rate
|
||||
self.rebate_ratio = rebate_ratio
|
||||
self.max_daily_dd = max_daily_drawdown
|
||||
self.min_hold_bars = min_hold_bars
|
||||
self.sl_pct = stop_loss_pct
|
||||
self.tp_pct = take_profit_pct
|
||||
self.max_positions = max_positions
|
||||
|
||||
def _close_position(self, pos, exit_price, t, today, trades, pending_rebates):
|
||||
"""平仓一个持仓,返回 net_pnl"""
|
||||
qty = self.notional / pos.entry_price
|
||||
if pos.direction == 1:
|
||||
raw_pnl = qty * (exit_price - pos.entry_price)
|
||||
else:
|
||||
raw_pnl = qty * (pos.entry_price - exit_price)
|
||||
|
||||
close_fee = self.notional * self.fee_rate
|
||||
net_pnl = raw_pnl - close_fee
|
||||
total_fee = self.notional * self.fee_rate * 2
|
||||
rebate = total_fee * self.rebate_ratio
|
||||
|
||||
rebate_date = today + datetime.timedelta(days=1)
|
||||
pending_rebates.append((rebate_date, rebate))
|
||||
|
||||
trades.append(Trade(
|
||||
entry_time=pos.entry_time, exit_time=t,
|
||||
direction=pos.direction, entry_price=pos.entry_price,
|
||||
exit_price=exit_price, pnl=net_pnl, fee=total_fee,
|
||||
rebate=rebate, holding_bars=pos.hold_bars,
|
||||
))
|
||||
return net_pnl
|
||||
|
||||
def _worst_unrealized(self, positions, h, lo):
|
||||
"""计算所有持仓在本K线内的最坏浮动亏损(用 high/low)"""
|
||||
worst = 0.0
|
||||
for pos in positions:
|
||||
qty = self.notional / pos.entry_price
|
||||
if pos.direction == 1:
|
||||
# 多单最坏情况: 价格跌到 low
|
||||
worst += qty * (lo - pos.entry_price)
|
||||
else:
|
||||
# 空单最坏情况: 价格涨到 high
|
||||
worst += qty * (pos.entry_price - h)
|
||||
return worst
|
||||
|
||||
def run(self, df: pd.DataFrame, score: pd.Series, open_threshold: float = 0.3) -> dict:
|
||||
capital = self.initial_capital
|
||||
trades: List[Trade] = []
|
||||
daily_pnl = {}
|
||||
pending_rebates = []
|
||||
positions: List[OpenPosition] = []
|
||||
used_margin = 0.0
|
||||
|
||||
current_date = None
|
||||
day_pnl = 0.0
|
||||
day_stopped = False
|
||||
|
||||
close_arr = df['close'].values
|
||||
high_arr = df['high'].values
|
||||
low_arr = df['low'].values
|
||||
times = df.index
|
||||
scores = score.values
|
||||
|
||||
for i in range(len(df)):
|
||||
t = times[i]
|
||||
c = close_arr[i]
|
||||
h = high_arr[i]
|
||||
lo = low_arr[i]
|
||||
s = scores[i]
|
||||
today = t.date()
|
||||
|
||||
# --- 日切换 ---
|
||||
if today != current_date:
|
||||
if current_date is not None:
|
||||
daily_pnl[current_date] = day_pnl
|
||||
current_date = today
|
||||
day_pnl = 0.0
|
||||
day_stopped = False
|
||||
|
||||
arrived = []
|
||||
remaining = []
|
||||
for rd, ra in pending_rebates:
|
||||
if today >= rd:
|
||||
arrived.append(ra)
|
||||
else:
|
||||
remaining.append((rd, ra))
|
||||
if arrived:
|
||||
capital += sum(arrived)
|
||||
pending_rebates = remaining
|
||||
|
||||
if day_stopped:
|
||||
for pos in positions:
|
||||
pos.hold_bars += 1
|
||||
continue
|
||||
|
||||
# --- 正常止损止盈逻辑 ---
|
||||
closed_indices = []
|
||||
for pi, pos in enumerate(positions):
|
||||
pos.hold_bars += 1
|
||||
qty = self.notional / pos.entry_price
|
||||
|
||||
if pos.direction == 1:
|
||||
sl_price = pos.entry_price * (1 - self.sl_pct)
|
||||
tp_price = pos.entry_price * (1 + self.tp_pct)
|
||||
hit_sl = lo <= sl_price
|
||||
hit_tp = h >= tp_price
|
||||
else:
|
||||
sl_price = pos.entry_price * (1 + self.sl_pct)
|
||||
tp_price = pos.entry_price * (1 - self.tp_pct)
|
||||
hit_sl = h >= sl_price
|
||||
hit_tp = lo <= tp_price
|
||||
|
||||
should_close = False
|
||||
exit_price = c
|
||||
|
||||
# 止损始终生效(不受持仓时间限制)
|
||||
if hit_sl:
|
||||
should_close = True
|
||||
exit_price = sl_price
|
||||
elif pos.hold_bars >= self.min_hold_bars:
|
||||
# 止盈和信号反转需要满足最小持仓时间
|
||||
if hit_tp:
|
||||
should_close = True
|
||||
exit_price = tp_price
|
||||
elif (pos.direction == 1 and s < -open_threshold) or \
|
||||
(pos.direction == -1 and s > open_threshold):
|
||||
should_close = True
|
||||
exit_price = c
|
||||
|
||||
if should_close:
|
||||
net = self._close_position(pos, exit_price, t, today, trades, pending_rebates)
|
||||
capital += net
|
||||
used_margin -= self.margin
|
||||
day_pnl += net
|
||||
closed_indices.append(pi)
|
||||
|
||||
# 每笔平仓后立即检查日回撤
|
||||
if day_pnl < -self.max_daily_dd:
|
||||
# 熔断剩余持仓
|
||||
for pj, pos2 in enumerate(positions):
|
||||
if pj not in closed_indices:
|
||||
pos2.hold_bars += 1
|
||||
net2 = self._close_position(pos2, c, t, today, trades, pending_rebates)
|
||||
capital += net2
|
||||
used_margin -= self.margin
|
||||
day_pnl += net2
|
||||
closed_indices.append(pj)
|
||||
day_stopped = True
|
||||
break
|
||||
|
||||
for pi in sorted(set(closed_indices), reverse=True):
|
||||
positions.pop(pi)
|
||||
|
||||
if day_stopped:
|
||||
continue
|
||||
|
||||
# --- 开仓 ---
|
||||
if len(positions) < self.max_positions:
|
||||
if np.isnan(s):
|
||||
continue
|
||||
|
||||
# 开仓前检查: 当前所有持仓 + 新仓同时止损的最大亏损
|
||||
n_after = len(positions) + 1
|
||||
worst_total_sl = n_after * (self.notional * self.sl_pct + self.notional * self.fee_rate * 2)
|
||||
if day_pnl - worst_total_sl < -self.max_daily_dd:
|
||||
continue # 风险敞口太大
|
||||
|
||||
open_fee = self.notional * self.fee_rate
|
||||
if capital - used_margin < self.margin + open_fee:
|
||||
continue
|
||||
|
||||
new_dir = 0
|
||||
if s > open_threshold:
|
||||
new_dir = 1
|
||||
elif s < -open_threshold:
|
||||
new_dir = -1
|
||||
|
||||
if new_dir != 0:
|
||||
positions.append(OpenPosition(
|
||||
direction=new_dir, entry_price=c,
|
||||
entry_time=t, hold_bars=0,
|
||||
))
|
||||
capital -= open_fee
|
||||
used_margin += self.margin
|
||||
day_pnl -= open_fee
|
||||
|
||||
# 最后一天
|
||||
if current_date is not None:
|
||||
daily_pnl[current_date] = day_pnl
|
||||
|
||||
# 强制平仓
|
||||
if positions and len(df) > 0:
|
||||
last_close = close_arr[-1]
|
||||
for pos in positions:
|
||||
qty = self.notional / pos.entry_price
|
||||
if pos.direction == 1:
|
||||
raw_pnl = qty * (last_close - pos.entry_price)
|
||||
else:
|
||||
raw_pnl = qty * (pos.entry_price - last_close)
|
||||
fee = self.notional * self.fee_rate
|
||||
net_pnl = raw_pnl - fee
|
||||
capital += net_pnl
|
||||
trades.append(Trade(
|
||||
entry_time=pos.entry_time, exit_time=times[-1],
|
||||
direction=pos.direction, entry_price=pos.entry_price,
|
||||
exit_price=last_close, pnl=net_pnl,
|
||||
fee=self.notional * self.fee_rate * 2,
|
||||
rebate=0, holding_bars=pos.hold_bars,
|
||||
))
|
||||
|
||||
remaining_rebate = sum(amt for _, amt in pending_rebates)
|
||||
capital += remaining_rebate
|
||||
|
||||
return self._build_result(trades, daily_pnl, capital)
|
||||
|
||||
def _build_result(self, trades, daily_pnl, final_capital):
|
||||
if not trades:
|
||||
return {
|
||||
'total_pnl': 0, 'final_capital': final_capital,
|
||||
'num_trades': 0, 'win_rate': 0, 'avg_pnl': 0,
|
||||
'max_daily_dd': 0, 'avg_daily_pnl': 0,
|
||||
'profit_factor': 0, 'trades': [], 'daily_pnl': daily_pnl,
|
||||
'total_fee': 0, 'total_rebate': 0,
|
||||
}
|
||||
|
||||
pnls = [t.pnl for t in trades]
|
||||
wins = [p for p in pnls if p > 0]
|
||||
losses = [p for p in pnls if p <= 0]
|
||||
daily_vals = list(daily_pnl.values())
|
||||
total_fee = sum(t.fee for t in trades)
|
||||
total_rebate = sum(t.rebate for t in trades)
|
||||
gross_profit = sum(wins) if wins else 0
|
||||
gross_loss = abs(sum(losses)) if losses else 1e-10
|
||||
|
||||
return {
|
||||
'total_pnl': sum(pnls) + total_rebate,
|
||||
'final_capital': final_capital,
|
||||
'num_trades': len(trades),
|
||||
'win_rate': len(wins) / len(trades) if trades else 0,
|
||||
'avg_pnl': np.mean(pnls),
|
||||
'max_daily_dd': min(daily_vals) if daily_vals else 0,
|
||||
'avg_daily_pnl': np.mean(daily_vals) if daily_vals else 0,
|
||||
'profit_factor': gross_profit / gross_loss,
|
||||
'total_fee': total_fee,
|
||||
'total_rebate': total_rebate,
|
||||
'trades': trades,
|
||||
'daily_pnl': daily_pnl,
|
||||
}
|
||||
53
strategy/best_params_2020_2022.json
Normal file
53
strategy/best_params_2020_2022.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"bb_period": 36,
|
||||
"bb_std": 3.3000000000000003,
|
||||
"kc_period": 24,
|
||||
"kc_mult": 1.3,
|
||||
"dc_period": 41,
|
||||
"ema_fast": 3,
|
||||
"ema_slow": 15,
|
||||
"macd_fast": 9,
|
||||
"macd_slow": 34,
|
||||
"macd_signal": 15,
|
||||
"adx_period": 16,
|
||||
"st_period": 5,
|
||||
"st_mult": 1.4,
|
||||
"rsi_period": 7,
|
||||
"stoch_k": 18,
|
||||
"stoch_d": 6,
|
||||
"stoch_smooth": 3,
|
||||
"cci_period": 12,
|
||||
"wr_period": 9,
|
||||
"wma_period": 47,
|
||||
"bb_oversold": -0.19999999999999998,
|
||||
"bb_overbought": 1.3,
|
||||
"kc_oversold": 0.2,
|
||||
"kc_overbought": 0.75,
|
||||
"dc_oversold": 0.05,
|
||||
"dc_overbought": 0.75,
|
||||
"adx_threshold": 15.0,
|
||||
"rsi_overbought": 70.0,
|
||||
"rsi_oversold": 18.0,
|
||||
"stoch_overbought": 89.0,
|
||||
"stoch_oversold": 10.0,
|
||||
"cci_overbought": 80.0,
|
||||
"cci_oversold": -140.0,
|
||||
"wr_overbought": -28.0,
|
||||
"wr_oversold": -90.0,
|
||||
"w_bb": 0.15000000000000002,
|
||||
"w_kc": 0.4,
|
||||
"w_dc": 0.0,
|
||||
"w_ema": 0.8500000000000001,
|
||||
"w_macd": 0.35000000000000003,
|
||||
"w_adx": 0.0,
|
||||
"w_st": 0.15000000000000002,
|
||||
"w_rsi": 0.4,
|
||||
"w_stoch": 0.15000000000000002,
|
||||
"w_cci": 0.1,
|
||||
"w_wr": 0.0,
|
||||
"w_wma": 0.4,
|
||||
"open_threshold": 0.22,
|
||||
"max_positions": 3,
|
||||
"take_profit_pct": 0.024999999999999998,
|
||||
"stop_loss_pct": 0.008
|
||||
}
|
||||
@@ -1,53 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
"""
|
||||
数据加载模块 - 从 SQLite 加载多周期K线数据为 DataFrame
|
||||
"""
|
||||
import pandas as pd
|
||||
import sqlite3
|
||||
from peewee import SqliteDatabase
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent.parent / 'models' / 'database.db'
|
||||
|
||||
# 周期 -> 表名
|
||||
PERIOD_MAP = {
|
||||
'1m': 'bitmart_eth_1m',
|
||||
'3m': 'bitmart_eth_3m',
|
||||
'5m': 'bitmart_eth_5m',
|
||||
'15m': 'bitmart_eth_15m',
|
||||
'30m': 'bitmart_eth_30m',
|
||||
'1h': 'bitmart_eth_1h',
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KlineSource:
|
||||
db_path: Path
|
||||
table_name: str
|
||||
def load_klines(period: str, start_date: str, end_date: str) -> pd.DataFrame:
|
||||
"""
|
||||
加载指定周期、指定日期范围的K线数据
|
||||
:param period: '1m','3m','5m','15m','30m','1h'
|
||||
:param start_date: 'YYYY-MM-DD'
|
||||
:param end_date: 'YYYY-MM-DD' (不包含该日)
|
||||
:return: DataFrame with columns: datetime, open, high, low, close
|
||||
"""
|
||||
table = PERIOD_MAP.get(period)
|
||||
if not table:
|
||||
raise ValueError(f"不支持的周期: {period}, 可选: {list(PERIOD_MAP.keys())}")
|
||||
|
||||
start_ts = int(pd.Timestamp(start_date).timestamp() * 1000)
|
||||
end_ts = int(pd.Timestamp(end_date).timestamp() * 1000)
|
||||
|
||||
def _to_ms(dt: datetime) -> int:
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return int(dt.timestamp() * 1000)
|
||||
db = SqliteDatabase(str(DB_PATH))
|
||||
db.connect()
|
||||
cursor = db.execute_sql(
|
||||
f'SELECT id, open, high, low, close FROM [{table}] '
|
||||
f'WHERE id >= ? AND id < ? ORDER BY id',
|
||||
(start_ts, end_ts)
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
db.close()
|
||||
|
||||
|
||||
def load_klines(
|
||||
source: KlineSource,
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
) -> pd.DataFrame:
|
||||
start_ms = _to_ms(start)
|
||||
end_ms = _to_ms(end)
|
||||
|
||||
con = sqlite3.connect(str(source.db_path))
|
||||
try:
|
||||
df = pd.read_sql_query(
|
||||
f"SELECT id, open, high, low, close FROM {source.table_name} WHERE id >= ? AND id <= ? ORDER BY id ASC",
|
||||
con,
|
||||
params=(start_ms, end_ms),
|
||||
)
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
df["timestamp_ms"] = df["id"].astype("int64")
|
||||
df["dt"] = pd.to_datetime(df["timestamp_ms"], unit="ms", utc=True)
|
||||
df = df.drop(columns=["id"]).set_index("dt")
|
||||
|
||||
for c in ("open", "high", "low", "close"):
|
||||
df[c] = pd.to_numeric(df[c], errors="coerce")
|
||||
|
||||
df = df.dropna(subset=["open", "high", "low", "close"])
|
||||
df = pd.DataFrame(rows, columns=['timestamp_ms', 'open', 'high', 'low', 'close'])
|
||||
df['datetime'] = pd.to_datetime(df['timestamp_ms'], unit='ms')
|
||||
df.set_index('datetime', inplace=True)
|
||||
df.drop(columns=['timestamp_ms'], inplace=True)
|
||||
df = df.astype(float)
|
||||
return df
|
||||
|
||||
|
||||
def load_multi_period(periods: list, start_date: str, end_date: str) -> dict:
|
||||
"""
|
||||
加载多个周期的数据
|
||||
:return: {period: DataFrame}
|
||||
"""
|
||||
result = {}
|
||||
for p in periods:
|
||||
result[p] = load_klines(p, start_date, end_date)
|
||||
print(f" 加载 {p}: {len(result[p])} 条 ({start_date} ~ {end_date})")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
data = load_multi_period(['5m', '15m', '1h'], '2020-01-01', '2024-01-01')
|
||||
for k, v in data.items():
|
||||
print(f"{k}: {v.shape}, {v.index[0]} ~ {v.index[-1]}")
|
||||
|
||||
125
strategy/strategy_signal.py
Normal file
125
strategy/strategy_signal.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
信号生成模块 - 多指标加权投票 + 多时间框架过滤
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def generate_indicator_signals(df: pd.DataFrame, params: dict) -> pd.DataFrame:
|
||||
"""
|
||||
根据指标值生成每个指标的独立信号 (+1 做多 / -1 做空 / 0 中性)
|
||||
df 必须已经包含 compute_all_indicators 计算出的列
|
||||
"""
|
||||
out = df.copy()
|
||||
|
||||
# --- 布林带 %B ---
|
||||
out['sig_bb'] = 0
|
||||
out.loc[out['bb_pct'] < params.get('bb_oversold', 0.0), 'sig_bb'] = 1
|
||||
out.loc[out['bb_pct'] > params.get('bb_overbought', 1.0), 'sig_bb'] = -1
|
||||
|
||||
# --- 肯特纳通道 ---
|
||||
out['sig_kc'] = 0
|
||||
out.loc[out['kc_pct'] < params.get('kc_oversold', 0.0), 'sig_kc'] = 1
|
||||
out.loc[out['kc_pct'] > params.get('kc_overbought', 1.0), 'sig_kc'] = -1
|
||||
|
||||
# --- 唐奇安通道 ---
|
||||
out['sig_dc'] = 0
|
||||
out.loc[out['dc_pct'] < params.get('dc_oversold', 0.2), 'sig_dc'] = 1
|
||||
out.loc[out['dc_pct'] > params.get('dc_overbought', 0.8), 'sig_dc'] = -1
|
||||
|
||||
# --- EMA 交叉 ---
|
||||
out['sig_ema'] = 0
|
||||
out.loc[out['ema_diff'] > 0, 'sig_ema'] = 1
|
||||
out.loc[out['ema_diff'] < 0, 'sig_ema'] = -1
|
||||
|
||||
# --- MACD ---
|
||||
out['sig_macd'] = 0
|
||||
out.loc[out['macd_hist'] > 0, 'sig_macd'] = 1
|
||||
out.loc[out['macd_hist'] < 0, 'sig_macd'] = -1
|
||||
|
||||
# --- ADX + DI ---
|
||||
adx_thresh = params.get('adx_threshold', 25)
|
||||
out['sig_adx'] = 0
|
||||
out.loc[(out['adx'] > adx_thresh) & (out['di_diff'] > 0), 'sig_adx'] = 1
|
||||
out.loc[(out['adx'] > adx_thresh) & (out['di_diff'] < 0), 'sig_adx'] = -1
|
||||
|
||||
# --- Supertrend ---
|
||||
out['sig_st'] = out['st_dir']
|
||||
|
||||
# --- RSI ---
|
||||
rsi_ob = params.get('rsi_overbought', 70)
|
||||
rsi_os = params.get('rsi_oversold', 30)
|
||||
out['sig_rsi'] = 0
|
||||
out.loc[out['rsi'] < rsi_os, 'sig_rsi'] = 1
|
||||
out.loc[out['rsi'] > rsi_ob, 'sig_rsi'] = -1
|
||||
|
||||
# --- Stochastic ---
|
||||
stoch_ob = params.get('stoch_overbought', 80)
|
||||
stoch_os = params.get('stoch_oversold', 20)
|
||||
out['sig_stoch'] = 0
|
||||
out.loc[(out['stoch_k'] < stoch_os) & (out['stoch_k'] > out['stoch_d']), 'sig_stoch'] = 1
|
||||
out.loc[(out['stoch_k'] > stoch_ob) & (out['stoch_k'] < out['stoch_d']), 'sig_stoch'] = -1
|
||||
|
||||
# --- CCI ---
|
||||
cci_ob = params.get('cci_overbought', 100)
|
||||
cci_os = params.get('cci_oversold', -100)
|
||||
out['sig_cci'] = 0
|
||||
out.loc[out['cci'] < cci_os, 'sig_cci'] = 1
|
||||
out.loc[out['cci'] > cci_ob, 'sig_cci'] = -1
|
||||
|
||||
# --- Williams %R ---
|
||||
wr_ob = params.get('wr_overbought', -20)
|
||||
wr_os = params.get('wr_oversold', -80)
|
||||
out['sig_wr'] = 0
|
||||
out.loc[out['wr'] < wr_os, 'sig_wr'] = 1
|
||||
out.loc[out['wr'] > wr_ob, 'sig_wr'] = -1
|
||||
|
||||
# --- WMA ---
|
||||
out['sig_wma'] = 0
|
||||
out.loc[out['wma_diff'] > 0, 'sig_wma'] = 1
|
||||
out.loc[out['wma_diff'] < 0, 'sig_wma'] = -1
|
||||
|
||||
return out
|
||||
|
||||
|
||||
SIGNAL_COLS = [
|
||||
'sig_bb', 'sig_kc', 'sig_dc', 'sig_ema', 'sig_macd',
|
||||
'sig_adx', 'sig_st', 'sig_rsi', 'sig_stoch', 'sig_cci',
|
||||
'sig_wr', 'sig_wma',
|
||||
]
|
||||
|
||||
WEIGHT_KEYS = [
|
||||
'w_bb', 'w_kc', 'w_dc', 'w_ema', 'w_macd',
|
||||
'w_adx', 'w_st', 'w_rsi', 'w_stoch', 'w_cci',
|
||||
'w_wr', 'w_wma',
|
||||
]
|
||||
|
||||
|
||||
def compute_composite_score(df: pd.DataFrame, params: dict) -> pd.Series:
|
||||
"""
|
||||
加权投票计算综合得分 (-1 ~ +1)
|
||||
"""
|
||||
weights = np.array([params.get(k, 1.0) for k in WEIGHT_KEYS])
|
||||
total_w = weights.sum()
|
||||
if total_w == 0:
|
||||
total_w = 1.0
|
||||
|
||||
signals = df[SIGNAL_COLS].values # (N, 12)
|
||||
score = (signals * weights).sum(axis=1) / total_w
|
||||
return pd.Series(score, index=df.index, name='score')
|
||||
|
||||
|
||||
def apply_htf_filter(score: pd.Series, htf_df: pd.DataFrame, params: dict) -> pd.Series:
|
||||
"""
|
||||
用高时间框架(如1h)的趋势方向过滤信号
|
||||
htf_df 需要包含 'ema_diff' 列
|
||||
只允许与大趋势同向的信号通过
|
||||
"""
|
||||
# 将 htf 的 ema_diff reindex 到主时间框架
|
||||
htf_trend = htf_df['ema_diff'].reindex(score.index, method='ffill')
|
||||
filtered = score.copy()
|
||||
# 大趋势向上时,屏蔽做空信号
|
||||
filtered.loc[(htf_trend > 0) & (filtered < 0)] = 0
|
||||
# 大趋势向下时,屏蔽做多信号
|
||||
filtered.loc[(htf_trend < 0) & (filtered > 0)] = 0
|
||||
return filtered
|
||||
226
strategy/train.py
Normal file
226
strategy/train.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Optuna 训练入口 - 在 2020-2022 数据上搜索最优参数
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import optuna
|
||||
from optuna.samplers import TPESampler
|
||||
import numpy as np
|
||||
|
||||
from strategy.data_loader import load_klines
|
||||
from strategy.indicators import compute_all_indicators
|
||||
from strategy.strategy_signal import (
|
||||
generate_indicator_signals, compute_composite_score,
|
||||
apply_htf_filter, WEIGHT_KEYS,
|
||||
)
|
||||
from strategy.backtest_engine import BacktestEngine
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 全局加载数据 (只加载一次)
|
||||
# ============================================================
|
||||
print("正在加载 2020-2022 训练数据...")
|
||||
DF_5M = load_klines('5m', '2020-01-01', '2023-01-01')
|
||||
DF_1H = load_klines('1h', '2020-01-01', '2023-01-01')
|
||||
print(f" 5m: {len(DF_5M)} 条, 1h: {len(DF_1H)} 条")
|
||||
print("数据加载完成。\n")
|
||||
|
||||
|
||||
def build_params(trial: optuna.Trial) -> dict:
|
||||
"""从 Optuna trial 构建完整参数字典"""
|
||||
p = {}
|
||||
|
||||
# --- 指标参数 ---
|
||||
p['bb_period'] = trial.suggest_int('bb_period', 10, 50)
|
||||
p['bb_std'] = trial.suggest_float('bb_std', 1.0, 3.5, step=0.1)
|
||||
|
||||
p['kc_period'] = trial.suggest_int('kc_period', 10, 50)
|
||||
p['kc_mult'] = trial.suggest_float('kc_mult', 0.5, 3.0, step=0.1)
|
||||
|
||||
p['dc_period'] = trial.suggest_int('dc_period', 10, 50)
|
||||
|
||||
p['ema_fast'] = trial.suggest_int('ema_fast', 3, 20)
|
||||
p['ema_slow'] = trial.suggest_int('ema_slow', 15, 60)
|
||||
|
||||
p['macd_fast'] = trial.suggest_int('macd_fast', 6, 20)
|
||||
p['macd_slow'] = trial.suggest_int('macd_slow', 18, 40)
|
||||
p['macd_signal'] = trial.suggest_int('macd_signal', 5, 15)
|
||||
|
||||
p['adx_period'] = trial.suggest_int('adx_period', 7, 30)
|
||||
|
||||
p['st_period'] = trial.suggest_int('st_period', 5, 20)
|
||||
p['st_mult'] = trial.suggest_float('st_mult', 1.0, 5.0, step=0.1)
|
||||
|
||||
p['rsi_period'] = trial.suggest_int('rsi_period', 7, 28)
|
||||
|
||||
p['stoch_k'] = trial.suggest_int('stoch_k', 5, 21)
|
||||
p['stoch_d'] = trial.suggest_int('stoch_d', 2, 7)
|
||||
p['stoch_smooth'] = trial.suggest_int('stoch_smooth', 2, 7)
|
||||
|
||||
p['cci_period'] = trial.suggest_int('cci_period', 10, 40)
|
||||
|
||||
p['wr_period'] = trial.suggest_int('wr_period', 7, 28)
|
||||
|
||||
p['wma_period'] = trial.suggest_int('wma_period', 10, 50)
|
||||
|
||||
# --- 信号阈值参数 ---
|
||||
p['bb_oversold'] = trial.suggest_float('bb_oversold', -0.3, 0.3, step=0.05)
|
||||
p['bb_overbought'] = trial.suggest_float('bb_overbought', 0.7, 1.3, step=0.05)
|
||||
p['kc_oversold'] = trial.suggest_float('kc_oversold', -0.3, 0.3, step=0.05)
|
||||
p['kc_overbought'] = trial.suggest_float('kc_overbought', 0.7, 1.3, step=0.05)
|
||||
p['dc_oversold'] = trial.suggest_float('dc_oversold', 0.0, 0.3, step=0.05)
|
||||
p['dc_overbought'] = trial.suggest_float('dc_overbought', 0.7, 1.0, step=0.05)
|
||||
p['adx_threshold'] = trial.suggest_float('adx_threshold', 15, 35, step=1)
|
||||
p['rsi_overbought'] = trial.suggest_float('rsi_overbought', 60, 85, step=1)
|
||||
p['rsi_oversold'] = trial.suggest_float('rsi_oversold', 15, 40, step=1)
|
||||
p['stoch_overbought'] = trial.suggest_float('stoch_overbought', 70, 90, step=1)
|
||||
p['stoch_oversold'] = trial.suggest_float('stoch_oversold', 10, 30, step=1)
|
||||
p['cci_overbought'] = trial.suggest_float('cci_overbought', 80, 200, step=5)
|
||||
p['cci_oversold'] = trial.suggest_float('cci_oversold', -200, -80, step=5)
|
||||
p['wr_overbought'] = trial.suggest_float('wr_overbought', -30, -10, step=1)
|
||||
p['wr_oversold'] = trial.suggest_float('wr_oversold', -90, -70, step=1)
|
||||
|
||||
# --- 权重 ---
|
||||
for wk in WEIGHT_KEYS:
|
||||
p[wk] = trial.suggest_float(wk, 0.0, 1.0, step=0.05)
|
||||
|
||||
# --- 回测参数 ---
|
||||
p['open_threshold'] = trial.suggest_float('open_threshold', 0.1, 0.6, step=0.02)
|
||||
p['max_positions'] = trial.suggest_int('max_positions', 1, 3)
|
||||
p['take_profit_pct'] = trial.suggest_float('take_profit_pct', 0.003, 0.025, step=0.001)
|
||||
|
||||
# 止损约束: N单同时止损 + 手续费 <= 50U
|
||||
# N * 1250 * sl_pct + N * 1.25 <= 50
|
||||
# sl_pct <= (50 - N*1.25) / (N*1250)
|
||||
n = p['max_positions']
|
||||
max_sl = (50.0 - n * 1.25) / (n * 1250.0)
|
||||
max_sl = round(max(max_sl, 0.002), 3) # 至少 0.2%
|
||||
p['stop_loss_pct'] = trial.suggest_float('stop_loss_pct', 0.002, max_sl, step=0.001)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
params = build_params(trial)
|
||||
|
||||
# 确保 ema_slow > ema_fast, macd_slow > macd_fast
|
||||
if params['ema_slow'] <= params['ema_fast']:
|
||||
return -1e6
|
||||
if params['macd_slow'] <= params['macd_fast']:
|
||||
return -1e6
|
||||
|
||||
try:
|
||||
# 计算指标
|
||||
df_5m = compute_all_indicators(DF_5M, params)
|
||||
df_1h = compute_all_indicators(DF_1H, params)
|
||||
|
||||
# 生成信号
|
||||
df_5m = generate_indicator_signals(df_5m, params)
|
||||
df_1h = generate_indicator_signals(df_1h, params)
|
||||
|
||||
# 综合得分
|
||||
score = compute_composite_score(df_5m, params)
|
||||
|
||||
# 高时间框架过滤
|
||||
score = apply_htf_filter(score, df_1h, params)
|
||||
|
||||
# 回测
|
||||
engine = BacktestEngine(
|
||||
initial_capital=1000.0,
|
||||
margin_per_trade=25.0,
|
||||
leverage=50,
|
||||
fee_rate=0.0005,
|
||||
rebate_ratio=0.70,
|
||||
max_daily_drawdown=50.0,
|
||||
min_hold_bars=1,
|
||||
stop_loss_pct=params['stop_loss_pct'],
|
||||
take_profit_pct=params['take_profit_pct'],
|
||||
max_positions=params['max_positions'],
|
||||
)
|
||||
|
||||
result = engine.run(df_5m, score, open_threshold=params['open_threshold'])
|
||||
|
||||
num_trades = result['num_trades']
|
||||
if num_trades < 50:
|
||||
return -1e6 # 交易次数太少,不可靠
|
||||
|
||||
total_pnl = result['total_pnl']
|
||||
max_dd = result['max_daily_dd'] # 负数 (引擎已保证 >= -50)
|
||||
avg_daily = result['avg_daily_pnl']
|
||||
|
||||
# 引擎内部已经有每日 50U 回撤熔断,这里不再硬约束
|
||||
# 目标: 最大化总收益
|
||||
score_val = total_pnl
|
||||
|
||||
# 奖励日均收益高的方案
|
||||
if avg_daily >= 50:
|
||||
score_val *= 1.3
|
||||
elif avg_daily >= 30:
|
||||
score_val *= 1.15
|
||||
|
||||
trial.set_user_attr('total_pnl', total_pnl)
|
||||
trial.set_user_attr('num_trades', num_trades)
|
||||
trial.set_user_attr('win_rate', result['win_rate'])
|
||||
trial.set_user_attr('max_daily_dd', max_dd)
|
||||
trial.set_user_attr('avg_daily_pnl', avg_daily)
|
||||
trial.set_user_attr('profit_factor', result['profit_factor'])
|
||||
|
||||
return score_val
|
||||
|
||||
except Exception as e:
|
||||
print(f"Trial {trial.number} 异常: {e}")
|
||||
return -1e6
|
||||
|
||||
|
||||
def main():
|
||||
study = optuna.create_study(
|
||||
direction='maximize',
|
||||
sampler=TPESampler(seed=42, n_startup_trials=30),
|
||||
study_name='eth_strategy_v1',
|
||||
)
|
||||
|
||||
# 设置日志级别
|
||||
optuna.logging.set_verbosity(optuna.logging.WARNING)
|
||||
|
||||
n_trials = 1000
|
||||
print(f"开始 Optuna 优化, 共 {n_trials} 次试验 (多单并发版)...")
|
||||
print("=" * 60)
|
||||
|
||||
def callback(study, trial):
|
||||
if trial.number % 10 == 0:
|
||||
best = study.best_trial
|
||||
print(f"[Trial {trial.number:>4d}] "
|
||||
f"当前值={trial.value:.2f} | "
|
||||
f"最佳值={best.value:.2f} | "
|
||||
f"PnL={best.user_attrs.get('total_pnl', 0):.1f}U | "
|
||||
f"胜率={best.user_attrs.get('win_rate', 0):.1%} | "
|
||||
f"日均={best.user_attrs.get('avg_daily_pnl', 0):.1f}U | "
|
||||
f"最大日回撤={best.user_attrs.get('max_daily_dd', 0):.1f}U")
|
||||
|
||||
study.optimize(objective, n_trials=n_trials, callbacks=[callback], show_progress_bar=True)
|
||||
|
||||
# 输出最佳结果
|
||||
best = study.best_trial
|
||||
print("\n" + "=" * 60)
|
||||
print("训练完成!最佳参数:")
|
||||
print("=" * 60)
|
||||
print(f" 目标值: {best.value:.4f}")
|
||||
print(f" 总收益: {best.user_attrs.get('total_pnl', 0):.2f}U")
|
||||
print(f" 交易次数: {best.user_attrs.get('num_trades', 0)}")
|
||||
print(f" 胜率: {best.user_attrs.get('win_rate', 0):.2%}")
|
||||
print(f" 日均收益: {best.user_attrs.get('avg_daily_pnl', 0):.2f}U")
|
||||
print(f" 最大日回撤: {best.user_attrs.get('max_daily_dd', 0):.2f}U")
|
||||
print(f" 盈亏比: {best.user_attrs.get('profit_factor', 0):.2f}")
|
||||
|
||||
# 保存最佳参数
|
||||
output_path = os.path.join(os.path.dirname(__file__), 'best_params_2020_2022.json')
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(best.params, f, indent=2, ensure_ascii=False)
|
||||
print(f"\n最佳参数已保存到: {output_path}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user