From 6e78bba16a3965856edf00c010e5af88ba82b80a Mon Sep 17 00:00:00 2001 From: anyongjin Date: Mon, 24 Aug 2026 20:53:21 +0800 Subject: [PATCH] init --- .gitignore | 11 + adv/pairs.go | 922 ++++++++++++++++++++++ arb/common.go | 159 ++++ arb/stat_group.go | 52 ++ arb/stat_job.go | 271 +++++++ arb/stat_mean.go | 373 +++++++++ arb/stat_prof.go | 348 +++++++++ arb/stat_showTwo.go | 203 +++++ arb/stat_spike.go | 31 + arb/stat_test.go | 391 ++++++++++ com/common.go | 91 +++ com/cross.go | 38 + com/eff_trail_stop.go | 44 ++ com/ema.go | 28 + com/exm.go | 135 ++++ com/inds.go | 41 + com/slop.go | 65 ++ com/trailing_stop.go | 85 +++ com/types.go | 6 + com/utils.go | 51 ++ demo/bar_agg.go | 79 ++ demo/bar_stat.go | 40 + demo/dca.go | 48 ++ demo/demo.go | 71 ++ demo/freqtrade/Strategy004.go | 43 ++ demo/freqtrade/Strategy005.go | 26 + demo/freqtrade/common.go | 11 + demo/freqtrade/strategy001.go | 46 ++ demo/here.java | 3 + demo/hft.go | 43 ++ demo/hold.go | 19 + demo/holdSl.go | 37 + demo/limits.go | 48 ++ demo/ma_er.go | 31 + demo/made.go | 86 +++ demo/main.go | 26 + demo/pairs_test.go | 97 +++ demo/risk_reward.go | 63 ++ demo/sample.go | 25 + demo/show.go | 45 ++ demo/test.go | 45 ++ demo/tmp.go | 35 + demo/tmp1.go | 29 + demo/trailing_stop.go | 27 + ent/fund_rate.go | 124 +++ ent/his_sharpe.go | 137 ++++ ent/main.go | 76 ++ ent/max_chg.go | 195 +++++ ent/max_vol.go | 182 +++++ ent/tools.go | 31 + fac/best.go | 172 +++++ filters/exp_pos.go | 88 +++ flux/grid/common.go | 302 ++++++++ flux/grid/inv.go | 81 ++ flux/grid/long.go | 83 ++ go.mod | 147 ++++ go.sum | 435 +++++++++++ llmjob/128.go | 189 +++++ llmjob/1357.go | 59 ++ llmjob/1525.go | 525 +++++++++++++ llmjob/1569.go | 105 +++ llmjob/1662.go | 197 +++++ llmjob/1665.go | 278 +++++++ llmjob/199.go | 116 +++ llmjob/2251.go | 68 ++ llmjob/2391.go | 174 +++++ llmjob/2439.go | 144 ++++ llmjob/2523.go | 103 +++ llmjob/2625.go | 168 +++++ llmjob/2775.go | 104 +++ llmjob/2874.go | 94 +++ llmjob/2876.go | 110 +++ llmjob/2984.go | 269 +++++++ llmjob/3087.go | 154 ++++ llmjob/3121.go | 88 +++ llmjob/3127.go | 55 ++ llmjob/3137.go | 127 ++++ llmjob/32.go | 64 ++ llmjob/3464.go | 173 +++++ llmjob/3614.go | 66 ++ llmjob/3644.go | 218 ++++++ llmjob/3683.go | 86 +++ llmjob/3707.go | 72 ++ llmjob/3746.go | 163 ++++ llmjob/3762.go | 270 +++++++ llmjob/3782.go | 310 ++++++++ llmjob/3827.go | 144 ++++ llmjob/3939.go | 160 ++++ llmjob/3955.go | 109 +++ llmjob/3956.go | 94 +++ llmjob/3992.go | 113 +++ llmjob/4009.go | 177 +++++ llmjob/4034.go | 128 ++++ llmjob/4067.go | 477 ++++++++++++ llmjob/419.go | 134 ++++ llmjob/4217.go | 110 +++ llmjob/4225.go | 113 +++ llmjob/4267.go | 156 ++++ llmjob/4316.go | 115 +++ llmjob/4513.go | 268 +++++++ llmjob/4583.go | 96 +++ llmjob/4670.go | 71 ++ llmjob/4994.go | 90 +++ llmjob/5115.go | 88 +++ llmjob/5129.go | 249 ++++++ llmjob/559.go | 64 ++ llmjob/5609.go | 215 ++++++ llmjob/5689.go | 204 +++++ llmjob/570.go | 104 +++ llmjob/5743.go | 100 +++ llmjob/5763.go | 131 ++++ llmjob/587.go | 144 ++++ llmjob/592.go | 111 +++ llmjob/656.go | 299 ++++++++ llmjob/6697.go | 60 ++ llmjob/6969.go | 89 +++ llmjob/7046.go | 131 ++++ llmjob/7050.go | 96 +++ llmjob/7096.go | 99 +++ llmjob/7593.go | 74 ++ llmjob/7699.go | 624 +++++++++++++++ llmjob/7801.go | 72 ++ llmjob/7854.go | 102 +++ llmjob/8179.go | 61 ++ llmjob/845.go | 226 ++++++ llmjob/README.md | 66 ++ llmjob/his/3762.go | 214 ++++++ llmjob/his/3782.go | 270 +++++++ llmjob/his/3782.yml | 66 ++ llmjob/main.go | 81 ++ main.go | 65 ++ old/CorrMeanRev.go | 117 +++ old/boll_back_com.go | 44 ++ old/boll_back_v1.go | 78 ++ old/boll_back_v2.go | 75 ++ old/breakout_v1.go | 75 ++ old/common.go | 26 + old/eat.go | 165 ++++ old/macd_v1.go | 127 ++++ old/ped.go | 71 ++ old/rev/01.go | 119 +++ old/rsi_ptn_rev/v1.go | 57 ++ old/s01/main.go | 163 ++++ old/s01/v1.go | 71 ++ old/s01/v2.go | 120 +++ old/s01/v3.go | 64 ++ old/t01/1h.go | 23 + old/t01/20240322_24_1h.xlsx | Bin 0 -> 10323 bytes old/t01/20240322_24_4h.xlsx | Bin 0 -> 10407 bytes old/t01/20240322_24_5m.xlsx | Bin 0 -> 10579 bytes old/t01/4h.go | 23 + old/t01/common.go | 138 ++++ old/t01/v1.go | 27 + old/xem4o.go | 89 +++ org/day/01.go | 36 + org/day/02.go | 62 ++ org/day/03.go | 47 ++ org/day/04.go | 73 ++ org/day/05.go | 58 ++ org/day/06.go | 40 + org/day/07.go | 162 ++++ org/day/main.go | 15 + org/digo/common.go | 33 + org/digo/v1.go | 209 +++++ org/digo/v2.go | 181 +++++ org/digo/v4.go | 112 +++ org/fmz/BearMACD.go | 64 ++ org/fmz/ComboMaBear.go | 91 +++ org/fmz/common.go | 10 + org/ft/BBMod2.go | 177 +++++ org/ft/BBMod7.go | 224 ++++++ org/ft/BinHModWhiteHOV0.go | 80 ++ org/ft/BinHV27.go | 101 +++ org/ft/Cenderawasih.go | 81 ++ org/ft/Clucha.go | 108 +++ org/ft/HPSV7.go | 79 ++ org/ft/MadV9HO.go | 144 ++++ org/ft/SMAOG.go | 69 ++ org/ft/Synthesis.go | 47 ++ org/ft/bbrsitv.go | 64 ++ org/ft/bin_ha_cluc_v8.go | 245 ++++++ org/ft/binance.go | 97 +++ org/ft/common.go | 35 + org/ft/e0v1e.go | 165 ++++ org/ft/ichiV1.go | 80 ++ org/ft/ichiv3.go | 78 ++ org/ft/noAnSmaOff.go | 74 ++ org/ft/sma_offset.go | 73 ++ org/ft/sma_offv2.go | 74 ++ org/ft/x_zvwap06.go | 197 +++++ org/jesse/AlligatorAi.go | 109 +++ org/jesse/IFR2.go | 20 + org/jesse/IchimokuCloud.go | 53 ++ org/jesse/KAMATrendFollow.go | 72 ++ org/jesse/MACDEMA.go | 37 + org/jesse/RSITrend.go | 42 ++ org/jesse/SimpleBollinger.go | 36 + org/jesse/TTMSqueezeWithAlma.go | 52 ++ org/jesse/TrendSwingV1.go | 73 ++ org/jesse/TurtleAI.go | 82 ++ org/jesse/common.go | 18 + org/jesse/donchian.go | 39 + org/jesse/jesse_test.go | 40 + org/llm/common.go | 7 + org/sf/01.go | 74 ++ org/sf/02.go | 87 +++ org/sf/03.go | 85 +++ org/sf/04.go | 61 ++ org/sf/05.go | 83 ++ org/sf/06.go | 67 ++ org/sf/08.go | 75 ++ org/sf/10.go | 72 ++ org/sf/14.go | 80 ++ org/sf/17.go | 67 ++ org/sf/18.go | 137 ++++ org/sf/19.go | 73 ++ org/sf/21.go | 68 ++ org/sf/30_v0.go | 75 ++ org/sf/30_v1.go | 78 ++ org/sf/33.go | 103 +++ org/sf/35.go | 112 +++ org/sf/36.go | 138 ++++ org/sf/37.go | 97 +++ org/sf/38.go | 98 +++ org/sf/common.go | 50 ++ ptn/all/main.go | 18 + ptn/classic.go | 62 ++ ptn/common.go | 28 + ptn/data.go | 6 + ptn/hammer/common.go | 179 +++++ ptn/hammer/v1.go | 45 ++ ptn/hammer/v2.go | 27 + ptn/hammer/v3.go | 35 + ptn/hammer/v4.go | 35 + ptn/hammer/v5.go | 96 +++ ptn/tunmo.go | 110 +++ ptn/types.go | 8 + rpc_fea/aifea.go | 121 +++ rpc_fea/aifea2.go | 107 +++ rpc_fea/aifea_test.go | 176 +++++ rpc_fea/common.go | 745 ++++++++++++++++++ rpc_fea/fea_base.go | 124 +++ rpc_fea/fea_times.go | 107 +++ rpc_fea/fea_trade1.go | 383 ++++++++++ rpc_fea/fea_trade2.go | 324 ++++++++ rpc_fea/main.go | 14 + scripts/migrate_backtests.ps1 | 1259 +++++++++++++++++++++++++++++++ surge/alldown.go | 157 ++++ surge/common.go | 10 + surge/down_test.go | 108 +++ surge/max_vol.go | 43 ++ trend/20250208/MACDEMAV2.go | 113 +++ trend/20250208/SMACrossV1.go | 107 +++ trend/20250208/SuperFollow.go | 98 +++ trend/20250208/main.go | 11 + trend/20250225/MACDEMAV2.go | 113 +++ trend/20250225/MACDX.go | 91 +++ trend/20250225/SMACrossV1.go | 107 +++ trend/20250225/SuperFollow.go | 111 +++ trend/MACDEMAV2.go | 113 +++ trend/MACDEMAV3.go | 114 +++ trend/MACDX.go | 105 +++ trend/SMACrossV1.go | 116 +++ trend/SMACrossV2.go | 144 ++++ trend/SuperFollow.go | 111 +++ trend/cci100.go | 41 + trend/cciMa.go | 53 ++ trend/common.go | 39 + trend/dv2.go | 59 ++ trend/eelfranzTDI.go | 86 +++ trend/fastio.go | 53 ++ trend/kdjBig.go | 46 ++ trend/kdjX.go | 45 ++ trend/ma1/common.go | 68 ++ trend/ma1/v1.go | 63 ++ trend/ma1/v2.1.go | 80 ++ trend/ma1/v2.go | 84 +++ trend/ma1/v3.go | 74 ++ trend/maMulti.go | 55 ++ trend/ma_cross.go | 58 ++ trend/macd.go | 47 ++ trend/pcmp.go | 59 ++ trend/randCut.go | 54 ++ trend/rsi_macd.go | 50 ++ trend/smp1.go | 119 +++ trend/swing.go | 98 +++ trend/t01v1.go | 84 +++ 287 files changed, 32443 insertions(+) create mode 100644 .gitignore create mode 100644 adv/pairs.go create mode 100644 arb/common.go create mode 100644 arb/stat_group.go create mode 100644 arb/stat_job.go create mode 100644 arb/stat_mean.go create mode 100644 arb/stat_prof.go create mode 100644 arb/stat_showTwo.go create mode 100644 arb/stat_spike.go create mode 100644 arb/stat_test.go create mode 100644 com/common.go create mode 100644 com/cross.go create mode 100644 com/eff_trail_stop.go create mode 100644 com/ema.go create mode 100644 com/exm.go create mode 100644 com/inds.go create mode 100644 com/slop.go create mode 100644 com/trailing_stop.go create mode 100644 com/types.go create mode 100644 com/utils.go create mode 100644 demo/bar_agg.go create mode 100644 demo/bar_stat.go create mode 100644 demo/dca.go create mode 100644 demo/demo.go create mode 100644 demo/freqtrade/Strategy004.go create mode 100644 demo/freqtrade/Strategy005.go create mode 100644 demo/freqtrade/common.go create mode 100644 demo/freqtrade/strategy001.go create mode 100644 demo/here.java create mode 100644 demo/hft.go create mode 100644 demo/hold.go create mode 100644 demo/holdSl.go create mode 100644 demo/limits.go create mode 100644 demo/ma_er.go create mode 100644 demo/made.go create mode 100644 demo/main.go create mode 100644 demo/pairs_test.go create mode 100644 demo/risk_reward.go create mode 100644 demo/sample.go create mode 100644 demo/show.go create mode 100644 demo/test.go create mode 100644 demo/tmp.go create mode 100644 demo/tmp1.go create mode 100644 demo/trailing_stop.go create mode 100644 ent/fund_rate.go create mode 100644 ent/his_sharpe.go create mode 100644 ent/main.go create mode 100644 ent/max_chg.go create mode 100644 ent/max_vol.go create mode 100644 ent/tools.go create mode 100644 fac/best.go create mode 100644 filters/exp_pos.go create mode 100644 flux/grid/common.go create mode 100644 flux/grid/inv.go create mode 100644 flux/grid/long.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 llmjob/128.go create mode 100644 llmjob/1357.go create mode 100644 llmjob/1525.go create mode 100644 llmjob/1569.go create mode 100644 llmjob/1662.go create mode 100644 llmjob/1665.go create mode 100644 llmjob/199.go create mode 100644 llmjob/2251.go create mode 100644 llmjob/2391.go create mode 100644 llmjob/2439.go create mode 100644 llmjob/2523.go create mode 100644 llmjob/2625.go create mode 100644 llmjob/2775.go create mode 100644 llmjob/2874.go create mode 100644 llmjob/2876.go create mode 100644 llmjob/2984.go create mode 100644 llmjob/3087.go create mode 100644 llmjob/3121.go create mode 100644 llmjob/3127.go create mode 100644 llmjob/3137.go create mode 100644 llmjob/32.go create mode 100644 llmjob/3464.go create mode 100644 llmjob/3614.go create mode 100644 llmjob/3644.go create mode 100644 llmjob/3683.go create mode 100644 llmjob/3707.go create mode 100644 llmjob/3746.go create mode 100644 llmjob/3762.go create mode 100644 llmjob/3782.go create mode 100644 llmjob/3827.go create mode 100644 llmjob/3939.go create mode 100644 llmjob/3955.go create mode 100644 llmjob/3956.go create mode 100644 llmjob/3992.go create mode 100644 llmjob/4009.go create mode 100644 llmjob/4034.go create mode 100644 llmjob/4067.go create mode 100644 llmjob/419.go create mode 100644 llmjob/4217.go create mode 100644 llmjob/4225.go create mode 100644 llmjob/4267.go create mode 100644 llmjob/4316.go create mode 100644 llmjob/4513.go create mode 100644 llmjob/4583.go create mode 100644 llmjob/4670.go create mode 100644 llmjob/4994.go create mode 100644 llmjob/5115.go create mode 100644 llmjob/5129.go create mode 100644 llmjob/559.go create mode 100644 llmjob/5609.go create mode 100644 llmjob/5689.go create mode 100644 llmjob/570.go create mode 100644 llmjob/5743.go create mode 100644 llmjob/5763.go create mode 100644 llmjob/587.go create mode 100644 llmjob/592.go create mode 100644 llmjob/656.go create mode 100644 llmjob/6697.go create mode 100644 llmjob/6969.go create mode 100644 llmjob/7046.go create mode 100644 llmjob/7050.go create mode 100644 llmjob/7096.go create mode 100644 llmjob/7593.go create mode 100644 llmjob/7699.go create mode 100644 llmjob/7801.go create mode 100644 llmjob/7854.go create mode 100644 llmjob/8179.go create mode 100644 llmjob/845.go create mode 100644 llmjob/README.md create mode 100644 llmjob/his/3762.go create mode 100644 llmjob/his/3782.go create mode 100644 llmjob/his/3782.yml create mode 100644 llmjob/main.go create mode 100644 main.go create mode 100644 old/CorrMeanRev.go create mode 100644 old/boll_back_com.go create mode 100644 old/boll_back_v1.go create mode 100644 old/boll_back_v2.go create mode 100644 old/breakout_v1.go create mode 100644 old/common.go create mode 100644 old/eat.go create mode 100644 old/macd_v1.go create mode 100644 old/ped.go create mode 100644 old/rev/01.go create mode 100644 old/rsi_ptn_rev/v1.go create mode 100644 old/s01/main.go create mode 100644 old/s01/v1.go create mode 100644 old/s01/v2.go create mode 100644 old/s01/v3.go create mode 100644 old/t01/1h.go create mode 100644 old/t01/20240322_24_1h.xlsx create mode 100644 old/t01/20240322_24_4h.xlsx create mode 100644 old/t01/20240322_24_5m.xlsx create mode 100644 old/t01/4h.go create mode 100644 old/t01/common.go create mode 100644 old/t01/v1.go create mode 100644 old/xem4o.go create mode 100644 org/day/01.go create mode 100644 org/day/02.go create mode 100644 org/day/03.go create mode 100644 org/day/04.go create mode 100644 org/day/05.go create mode 100644 org/day/06.go create mode 100644 org/day/07.go create mode 100644 org/day/main.go create mode 100644 org/digo/common.go create mode 100644 org/digo/v1.go create mode 100644 org/digo/v2.go create mode 100644 org/digo/v4.go create mode 100644 org/fmz/BearMACD.go create mode 100644 org/fmz/ComboMaBear.go create mode 100644 org/fmz/common.go create mode 100644 org/ft/BBMod2.go create mode 100644 org/ft/BBMod7.go create mode 100644 org/ft/BinHModWhiteHOV0.go create mode 100644 org/ft/BinHV27.go create mode 100644 org/ft/Cenderawasih.go create mode 100644 org/ft/Clucha.go create mode 100644 org/ft/HPSV7.go create mode 100644 org/ft/MadV9HO.go create mode 100644 org/ft/SMAOG.go create mode 100644 org/ft/Synthesis.go create mode 100644 org/ft/bbrsitv.go create mode 100644 org/ft/bin_ha_cluc_v8.go create mode 100644 org/ft/binance.go create mode 100644 org/ft/common.go create mode 100644 org/ft/e0v1e.go create mode 100644 org/ft/ichiV1.go create mode 100644 org/ft/ichiv3.go create mode 100644 org/ft/noAnSmaOff.go create mode 100644 org/ft/sma_offset.go create mode 100644 org/ft/sma_offv2.go create mode 100644 org/ft/x_zvwap06.go create mode 100644 org/jesse/AlligatorAi.go create mode 100644 org/jesse/IFR2.go create mode 100644 org/jesse/IchimokuCloud.go create mode 100644 org/jesse/KAMATrendFollow.go create mode 100644 org/jesse/MACDEMA.go create mode 100644 org/jesse/RSITrend.go create mode 100644 org/jesse/SimpleBollinger.go create mode 100644 org/jesse/TTMSqueezeWithAlma.go create mode 100644 org/jesse/TrendSwingV1.go create mode 100644 org/jesse/TurtleAI.go create mode 100644 org/jesse/common.go create mode 100644 org/jesse/donchian.go create mode 100644 org/jesse/jesse_test.go create mode 100644 org/llm/common.go create mode 100644 org/sf/01.go create mode 100644 org/sf/02.go create mode 100644 org/sf/03.go create mode 100644 org/sf/04.go create mode 100644 org/sf/05.go create mode 100644 org/sf/06.go create mode 100644 org/sf/08.go create mode 100644 org/sf/10.go create mode 100644 org/sf/14.go create mode 100644 org/sf/17.go create mode 100644 org/sf/18.go create mode 100644 org/sf/19.go create mode 100644 org/sf/21.go create mode 100644 org/sf/30_v0.go create mode 100644 org/sf/30_v1.go create mode 100644 org/sf/33.go create mode 100644 org/sf/35.go create mode 100644 org/sf/36.go create mode 100644 org/sf/37.go create mode 100644 org/sf/38.go create mode 100644 org/sf/common.go create mode 100644 ptn/all/main.go create mode 100644 ptn/classic.go create mode 100644 ptn/common.go create mode 100644 ptn/data.go create mode 100644 ptn/hammer/common.go create mode 100644 ptn/hammer/v1.go create mode 100644 ptn/hammer/v2.go create mode 100644 ptn/hammer/v3.go create mode 100644 ptn/hammer/v4.go create mode 100644 ptn/hammer/v5.go create mode 100644 ptn/tunmo.go create mode 100644 ptn/types.go create mode 100644 rpc_fea/aifea.go create mode 100644 rpc_fea/aifea2.go create mode 100644 rpc_fea/aifea_test.go create mode 100644 rpc_fea/common.go create mode 100644 rpc_fea/fea_base.go create mode 100644 rpc_fea/fea_times.go create mode 100644 rpc_fea/fea_trade1.go create mode 100644 rpc_fea/fea_trade2.go create mode 100644 rpc_fea/main.go create mode 100644 scripts/migrate_backtests.ps1 create mode 100644 surge/alldown.go create mode 100644 surge/common.go create mode 100644 surge/down_test.go create mode 100644 surge/max_vol.go create mode 100644 trend/20250208/MACDEMAV2.go create mode 100644 trend/20250208/SMACrossV1.go create mode 100644 trend/20250208/SuperFollow.go create mode 100644 trend/20250208/main.go create mode 100644 trend/20250225/MACDEMAV2.go create mode 100644 trend/20250225/MACDX.go create mode 100644 trend/20250225/SMACrossV1.go create mode 100644 trend/20250225/SuperFollow.go create mode 100644 trend/MACDEMAV2.go create mode 100644 trend/MACDEMAV3.go create mode 100644 trend/MACDX.go create mode 100644 trend/SMACrossV1.go create mode 100644 trend/SMACrossV2.go create mode 100644 trend/SuperFollow.go create mode 100644 trend/cci100.go create mode 100644 trend/cciMa.go create mode 100644 trend/common.go create mode 100644 trend/dv2.go create mode 100644 trend/eelfranzTDI.go create mode 100644 trend/fastio.go create mode 100644 trend/kdjBig.go create mode 100644 trend/kdjX.go create mode 100644 trend/ma1/common.go create mode 100644 trend/ma1/v1.go create mode 100644 trend/ma1/v2.1.go create mode 100644 trend/ma1/v2.go create mode 100644 trend/ma1/v3.go create mode 100644 trend/maMulti.go create mode 100644 trend/ma_cross.go create mode 100644 trend/macd.go create mode 100644 trend/pcmp.go create mode 100644 trend/randCut.go create mode 100644 trend/rsi_macd.go create mode 100644 trend/smp1.go create mode 100644 trend/swing.go create mode 100644 trend/t01v1.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..540a486 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea +.omx +tmp +bot +code +*.exe +*.profile +*.db +*.gob +*.log +.noai diff --git a/adv/pairs.go b/adv/pairs.go new file mode 100644 index 0000000..ff8fb2a --- /dev/null +++ b/adv/pairs.go @@ -0,0 +1,922 @@ +package adv + +import ( + "errors" + "flag" + "fmt" + "io" + "math" + "net/http" + "path/filepath" + "slices" + "sort" + "strings" + "time" + + "gonum.org/v1/gonum/floats" + + "go.uber.org/zap" + + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/entry" + "github.com/banbox/banbot/opt" + "github.com/banbox/banbot/orm" + "github.com/banbox/banbot/orm/ormo" + utils2 "github.com/banbox/banbot/utils" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + "github.com/banbox/banexg/utils" + "github.com/spf13/cobra" +) + +func init() { + runConfig := func(args *config.CmdArgs, run func(*config.CmdArgs) *errs.Error) error { + core.SetRunMode(core.RunModeOther) + args.Init() + return run(args) + } + bindCommon := func(command *cobra.Command, args *config.CmdArgs) { + flags := command.Flags() + flags.StringArrayVar((*[]string)(&args.Configs), "config", nil, "config path; may be repeated") + flags.StringVar(&args.ConfigData, "config-data", "", "inline YAML config") + flags.StringVar(&args.Logfile, "logfile", "", "log file path") + flags.StringVar(&args.DataDir, "datadir", "", "path to the data directory") + flags.BoolVar(&args.NoDefault, "no-default", false, "ignore config.yml and config.local.yml") + flags.StringVar(&args.LogLevel, "level", "info", "logging level") + flags.IntVar(&args.MaxPoolSize, "max-pool-size", 0, "maximum database pool size") + flags.BoolVar(&core.NetDisable, "net-off", false, "disable network requests") + } + newConfigCommand := func(name, help string, run func(*config.CmdArgs) *errs.Error, + bind func(*cobra.Command, *config.CmdArgs)) *cobra.Command { + args := &config.CmdArgs{} + command := &cobra.Command{ + Use: name, + Short: help, + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + return runConfig(args, run) + }, + } + bindCommon(command, args) + if bind != nil { + bind(command, args) + } + return command + } + + dayRets := newConfigCommand("dayRets", "calc return rate for pairs by months", CalcPairDayPerfs, + func(command *cobra.Command, args *config.CmdArgs) { + command.Flags().StringVar(&args.InPath, "in", "", "input file or directory") + }) + entry.AddCommand("tool", dayRets) + + entry.AddCommand("tool", newConfigCommand("updMkt", "update coin markets by month", UpdateCoinMarkets, nil)) + entry.AddCommand("tool", newConfigCommand("buildFac", "build factor samples", BuildFacSamples, nil)) + entry.AddCommand("tool", &cobra.Command{ + Use: "testFac", + Short: "test factors", + DisableFlagParsing: true, + RunE: func(_ *cobra.Command, args []string) error { + return TestFactors(args) + }, + }) +} + +var urlCoinMarket = "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/listings/historical?convertId=2781&date=%s&limit=%d&start=%d" +var coinSlugMap = map[string]string{ + "BTCDOM": "", + "1000XEC": "ecash", + "LUNA2": "terra-luna-v2", + "DODOX": "dodo", + "BEAMX": "onbeam", + "1000RATS": "", + "RONIN": "ronin", + "1000SHIB": "shiba-inu", + "1000LUNC": "terra-luna", + "1000FLOKI": "floki-inu", + "1000PEPE": "pepe", + "1000BONK": "bonk1", +} + +type PairReturns struct { + StartMS int64 + EndMS int64 + Pairs map[string]*ItemReturns +} + +type ItemReturns struct { + StartMS int64 + EndMS int64 + Arr []float64 +} + +type CoinMarket struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + ListMS int64 `json:"list_ms"` + Slug string `json:"slug"` + FirstMS int64 `json:"first_ms"` + LastMS int64 `json:"last_ms"` + MarketHis []*MarketSlice `json:"market_his"` +} + +type MarketSlice struct { + TimeMS int64 `json:"time_ms"` + CirculatingSupply float64 `json:"circulating_supply"` + TotalSupply float64 `json:"total_supply"` + Price float64 `json:"price"` + MarketCap float64 `json:"market_cap"` +} + +type CoinMarketHis struct { + Coins map[string]*CoinMarket + Times []int64 +} + +type CoinMarketResponse struct { + Data []struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + DateAdded string `json:"dateAdded"` + CirculatingSupply float64 `json:"circulatingSupply"` + TotalSupply float64 `json:"totalSupply"` + Quotes []struct { + Name string `json:"name"` + Price float64 `json:"price"` + MarketCap float64 `json:"marketCap"` + } `json:"quotes"` + } `json:"data"` + Status struct { + ErrorCode string `json:"error_code"` + ErrorMessage string `json:"error_message"` + } `json:"status"` +} + +/* +某个品种在某个时间的历史收益,市场、未来回报等信息 +*/ +type PairSample struct { + Offset int // 值int是在ItemReturns.Arr的位置索引 + Pair string + Code string + Price float64 + MarketCap float64 + Vols []float64 +} + +type FacHis struct { + Times []int64 + Pairs map[string]*ItemReturns + Data map[int64]map[string]*PairSample // time: pair: sample +} + +type FacState struct { + Ret *ItemReturns + Sample *PairSample + Groups map[string]string + Label string + Tmp float64 // 因子计算临时使用 + OutScore float64 // 输出分数 + OutID int // 输出分数排序序号 +} + +type GpState struct { + count float64 // 该块中FacState的数量 + sumScore float64 // 该块的平均分数 +} + +/* +CalcPairDayPerfs 从订单计算每个品种的每日收益率 +*/ +func CalcPairDayPerfs(args *config.CmdArgs) *errs.Error { + err := biz.SetupComs(args) + if err != nil { + return err + } + orders, err := ormo.LoadOrdersGob(args.InPath) + if err != nil { + return err + } + pairOrders := make(map[string][]*ormo.InOutOrder) + minTimeMS, maxTimeMS := btime.TimeMS(), int64(0) + totalCost := float64(0) + for _, o := range orders { + odList, _ := pairOrders[o.Symbol] + pairOrders[o.Symbol] = append(odList, o) + minTimeMS = min(minTimeMS, o.Enter.CreateAt) + maxTimeMS = max(maxTimeMS, o.Exit.CreateAt) + totalCost += o.EnterCost() / o.Leverage + } + // 假设初始金额能同时开50笔 + initStake := totalCost / float64(len(orders)) * 50 + // 解析开始时间和结束时间,对齐到月的1号 + startYear, startMon, _ := btime.MSToTime(minTimeMS).Date() + endYear, endMon, endDay := btime.MSToTime(maxTimeMS).Date() + endAddMon := 0 + if endDay > 1 { + endAddMon = 1 + } + startMS := time.Date(startYear, startMon, 1, 0, 0, 0, 0, time.UTC).UnixMilli() + endTime := time.Date(endYear, endMon, 1, 0, 0, 0, 0, time.UTC) + endMS := endTime.AddDate(0, endAddMon, 0).UnixMilli() + symbolMap := orm.GetExSymbolMap(core.ExgName, core.Market) + dayMSecs := int64(utils.TFToSecs("1d") * 1000) + retMap := make(map[string]*ItemReturns) + var addNum int + for pair, odList := range pairOrders { + sort.Slice(odList, func(i, j int) bool { + a, b := odList[i], odList[j] + return a.Enter.CreateAt < b.Enter.CreateAt + }) + exs := symbolMap[pair] + _, bars, err := orm.GetOHLCV(exs, "1d", startMS, endMS, 0, false) + if err != nil { + return err + } + var closes []float64 + if len(bars) > 0 { + bars, addNum = utils2.FillOHLCVLacks(bars, startMS, endMS, dayMSecs) + if addNum > 0 { + log.Info("fill ohlcv lack", zap.String("pair", pair), zap.Int("num", addNum)) + } + closes = make([]float64, len(bars)) + for i, b := range bars { + closes[i] = b.Close + } + } + // 计算每日回报率 + returns, dayOff, dayEnd := ormo.CalcUnitReturns(odList, closes, startMS, endMS, dayMSecs) + retRates := make([]float64, len(returns)) + for i, ret := range returns { + retRates[i] = ret / initStake + } + retMap[pair] = &ItemReturns{ + StartMS: startMS + int64(dayOff)*dayMSecs, + EndMS: startMS + int64(dayEnd)*dayMSecs, + Arr: retRates[dayOff:dayEnd], + } + } + outPath := filepath.Join(filepath.Dir(args.InPath), "out.gob") + return utils2.EncodeGob(outPath, &PairReturns{ + StartMS: startMS, + EndMS: endMS, + Pairs: retMap, + }) +} + +func loadHisMarkets() (*CoinMarketHis, string, *errs.Error) { + path := filepath.Join(config.GetDataDir(), "cryptoMarkets.gob") + var data = &CoinMarketHis{ + Coins: make(map[string]*CoinMarket), + Times: make([]int64, 0), + } + err := utils2.DecodeGobFile(path, data) + if err != nil { + return data, path, err + } + return data, path, nil +} + +/* +UpdateCoinMarkets 更新BanDataDir目录下的cryptoMarkets.gob全币种市场信息。每个月更新一次。 +*/ +func UpdateCoinMarkets(args *config.CmdArgs) *errs.Error { + err := biz.SetupComs(args) + if err != nil { + return err + } + data, path, err := loadHisMarkets() + if err != nil { + log.Warn("load cryptoMarkets error", zap.Error(err)) + } + + // 确定开始时间 + startTime := time.Date(2014, 1, 1, 0, 0, 0, 0, time.UTC) + if len(data.Times) > 0 { + startTime = time.UnixMilli(data.Times[len(data.Times)-1]).UTC().AddDate(0, 1, 0) + } + lastTimeMS := startTime.AddDate(0, -1, 0).UnixMilli() + batchSize := 2000 + isLast := false + + // 循环获取每个月的数据 + for { + if startTime.After(time.Now()) { + pyear, pmon, pday := time.Now().UTC().Date() + startTime = time.Date(pyear, pmon, pday, 0, 0, 0, 0, time.UTC).AddDate(0, 0, -1) + isLast = true + } + + // 获取该月所有币种数据 + monthData := make(map[string]*MarketSlice) + start := 1 + fetchNum, fixLack := 0, 0 + timeMS := startTime.UnixMilli() + for { + url := fmt.Sprintf(urlCoinMarket, startTime.Format("2006-01-02"), batchSize, start) + + time.Sleep(time.Second * 1) + resp, err_ := http.Get(url) + log.Info("request", zap.String("url", url)) + if err_ != nil { + return errs.New(errs.CodeNetFail, err_) + } + + body, err_ := io.ReadAll(resp.Body) + resp.Body.Close() + if err_ != nil { + return errs.New(errs.CodeNetFail, err_) + } + + var rsp CoinMarketResponse + if err_ = utils.Unmarshal(body, &rsp, utils.JsonNumAuto); err_ != nil { + return errs.New(errs.CodeUnmarshalFail, err_) + } + + if rsp.Status.ErrorCode != "0" { + return errs.NewMsg(errs.CodeRunTime, "code: %v, msg: %v", rsp.Status.ErrorCode, rsp.Status.ErrorMessage) + } + + if len(rsp.Data) == 0 { + break + } + + for _, coin := range rsp.Data { + slice := &MarketSlice{ + TimeMS: timeMS, + CirculatingSupply: coin.CirculatingSupply, + TotalSupply: coin.TotalSupply, + } + if len(coin.Quotes) > 0 { + slice.Price = coin.Quotes[0].Price + slice.MarketCap = coin.Quotes[0].MarketCap + } + monthData[coin.Slug] = slice + + // 更新或创建币种信息 + if cm, exists := data.Coins[coin.Slug]; exists { + if len(cm.MarketHis) == 0 || timeMS > cm.MarketHis[len(cm.MarketHis)-1].TimeMS { + // 检查是否有缺失的月份数据需要补充 + if !isLast { + if cm.LastMS < lastTimeMS { + fixLack += 1 + lastSlice := cm.MarketHis[len(cm.MarketHis)-1] + // 从上次更新的下个月开始,直到上个月为止,补充所有缺失的月份 + curTime := time.UnixMilli(cm.LastMS).UTC().AddDate(0, 1, 0) + for curTime.UnixMilli() <= lastTimeMS { + newSlice := &MarketSlice{ + TimeMS: curTime.UnixMilli(), + CirculatingSupply: lastSlice.CirculatingSupply, + TotalSupply: lastSlice.TotalSupply, + Price: lastSlice.Price, + MarketCap: lastSlice.MarketCap, + } + cm.MarketHis = append(cm.MarketHis, newSlice) + cm.LastMS = curTime.UnixMilli() + curTime = curTime.AddDate(0, 1, 0) + } + } + // 添加当前月份的数据 + cm.MarketHis = append(cm.MarketHis, slice) + cm.LastMS = timeMS + } + if cm.Name != coin.Name { + cm.Name = coin.Name + } + if cm.Symbol != coin.Symbol { + cm.Symbol = coin.Symbol + } + fetchNum += 1 + } + } else if !isLast { + listTime, _ := time.Parse("2006-01-02T15:04:05.000Z", coin.DateAdded) + data.Coins[coin.Slug] = &CoinMarket{ + Name: coin.Name, + Symbol: coin.Symbol, + ListMS: listTime.UnixMilli(), + Slug: coin.Slug, + FirstMS: timeMS, + LastMS: timeMS, + MarketHis: []*MarketSlice{slice}, + } + fetchNum += 1 + } + } + + // 这里可能返回数量不足batchSize个,但不代表结束了,继续请求返回为空才表示结束 + start += batchSize + } + + // 添加时间戳 + data.Times = append(data.Times, timeMS) + + // 保存数据 + if err := utils2.EncodeGob(path, data); err != nil { + log.Error("save market data failed", zap.Error(err)) + return err + } else { + log.Info("update coin markets ok", zap.String("date", startTime.Format("2006-01-02")), + zap.Int("fetch_num", fetchNum), zap.Int("fixLack", fixLack)) + } + + // 移动到下一个月 + lastTimeMS = timeMS + startTime = startTime.AddDate(0, 1, 0) + if isLast { + break + } + } + return nil +} + +/* +BuildFacSamples 构建因子定期采样历史 +*/ +func BuildFacSamples(args *config.CmdArgs) *errs.Error { + err := biz.SetupComs(args) + if err != nil { + return err + } + data, _, err := loadHisMarkets() + if err != nil { + return err + } + retData := &PairReturns{Pairs: make(map[string]*ItemReturns)} + retPath := "E:\\trade\\go\\bandata\\backtest\\383f7dc32a_all_pairs\\out.gob" + if !utils2.Exists(retPath) { + args.InPath = filepath.Join(filepath.Dir(retPath), "orders.gob") + err = CalcPairDayPerfs(args) + if err != nil { + return err + } + } + err = utils2.DecodeGobFile(retPath, retData) + if err != nil { + return err + } + if len(retData.Pairs) == 0 { + return errs.NewMsg(errs.CodeRunTime, "no Pairs for PairReturns") + } + res := &FacHis{Pairs: retData.Pairs, Data: make(map[int64]map[string]*PairSample)} + startMS := retData.StartMS + + // 构建Symbol到CoinMarket的映射 + var b strings.Builder + symbolToCoin := make(map[string]*CoinMarket) + badCoins := make(map[string]bool) + skipCoins := make(map[string]bool) + dupCoins := make(map[string][]string) + for _, coin := range data.Coins { + if old, ok := symbolToCoin[coin.Symbol]; ok { + dups, _ := dupCoins[coin.Symbol] + if len(dups) == 0 { + dups = append(dups, old.Slug) + } + dupCoins[coin.Symbol] = append(dups, coin.Slug) + continue + } + symbolToCoin[coin.Symbol] = coin + b.WriteString(coin.Symbol) + b.WriteString(", ") + } + symbolMap := orm.GetExSymbolMap(core.ExgName, core.Market) + log.Info("all coins", zap.String("v", b.String())) + if len(dupCoins) > 0 { + log.Info("dupCoins", zap.String("v", utils2.MapToStr(dupCoins, true, 0))) + } + + // 设置月份间隔(这里设置为3个月) + monNum := 2 + + // 将startMS转换为月初 + startTime := time.UnixMilli(startMS).UTC() + startTime = time.Date(startTime.Year(), startTime.Month(), 1, 0, 0, 0, 0, time.UTC) + startTime = startTime.AddDate(0, monNum, 0) + endTime := time.UnixMilli(retData.EndMS).UTC() + + // 从startTime开始遍历时间点 + for curTime := startTime; !curTime.After(endTime); curTime = curTime.AddDate(0, monNum, 0) { + timeMS := curTime.UnixMilli() + timeMap := make(map[string]*PairSample) + prevTime := curTime.AddDate(0, -monNum, 0) + nextTime := curTime.AddDate(0, monNum, 0) + + // 对每个pair的returns进行检查 + for pair, itemRet := range retData.Pairs { + // 检查时间范围是否符合要求 + if itemRet.StartMS > prevTime.UnixMilli() || itemRet.EndMS < nextTime.UnixMilli() { + continue + } + + // 计算当前时间点在Arr中的索引 + offset := int((timeMS - itemRet.StartMS) / (24 * 3600 * 1000)) // 转换为天数 + if offset < 0 || offset >= len(itemRet.Arr) { + continue + } + + // 查找对应的CoinMarket + baseCode, _, _, _ := core.SplitSymbol(pair) + coin, ok := symbolToCoin[baseCode] + if !ok { + if slug, ok := coinSlugMap[baseCode]; ok { + if slug == "" { + skipCoins[baseCode] = true + continue + } + coin = data.Coins[slug] + } else { + badCoins[baseCode] = true + continue + } + } + + // 查找当前时间点对应的MarketSlice + var mktSlice *MarketSlice + for _, slice := range coin.MarketHis { + if slice.TimeMS <= timeMS { + mktSlice = slice + } else { + break + } + } + if mktSlice == nil { + // 太新,跳过 + continue + } + + // 获取成交量数据 + exs := symbolMap[pair] + if exs == nil { + return errs.NewMsg(errs.CodeRunTime, "ExSymbol not exist: %v", pair) + } + + // 计算2周和1个月的成交量 + barStartMS := prevTime.UnixMilli() + _, bars, err := orm.GetOHLCV(exs, "1d", barStartMS, timeMS, 0, false) + if err != nil { + log.Warn("get volume failed", zap.String("pair", pair), zap.Error(err)) + continue + } + + // 计算成交量 + vols := make([]float64, len(bars)) + for i, bar := range bars { + vols[i] = bar.Close * bar.Volume + } + + // 构建PairSample + sample := &PairSample{ + Offset: offset, + Pair: pair, + Code: baseCode, + Price: mktSlice.Price, + MarketCap: mktSlice.MarketCap, + Vols: vols, + } + + timeMap[pair] = sample + } + + if len(timeMap) > 0 { + res.Data[timeMS] = timeMap + res.Times = append(res.Times, timeMS) + } + } + + if len(badCoins) > 0 { + log.Warn("unknown coins", zap.String("v", utils2.MapToStr(badCoins, false, 0))) + } + if len(skipCoins) > 0 { + log.Warn("skip coins", zap.String("v", utils2.MapToStr(skipCoins, false, 0))) + } + + // 保存结果 + name := fmt.Sprintf("facHis_%dM.gob", monNum) + outPath := filepath.Join(config.GetDataDir(), name) + return utils2.EncodeGob(outPath, res) +} + +func TestFactors(args []string) error { + parser := flag.NewFlagSet("", flag.ExitOnError) + var monNum, outNum, offNum int + var facStr, outBy string + parser.IntVar(&monNum, "mon", 2, "month for period") + parser.IntVar(&outNum, "out", 3, "output group num") + parser.IntVar(&offNum, "off", 0, "skip group num in head") + parser.StringVar(&facStr, "fac", "", "factors to test") + parser.StringVar(&outBy, "outBy", "", "calc output score by") + err_ := parser.Parse(args) + if err_ != nil { + return err_ + } + facArr := utils2.SplitSolid(facStr, ",", true) + if len(facArr) == 0 { + return errors.New("-fac is required") + } + dayNum := monNum * 30 + fac := &FacHis{Pairs: make(map[string]*ItemReturns), Data: make(map[int64]map[string]*PairSample)} + facPath := filepath.Join(config.GetDataDir(), fmt.Sprintf("facHis_%dM.gob", monNum)) + err := utils2.DecodeGobFile(facPath, fac) + if err != nil { + return err + } + groupMeas := make(map[string][]*GpState) + for i, stampMS := range fac.Times { + if i <= offNum { + continue + } + pairSamples := fac.Data[stampMS] + items := make([]*FacState, 0) + for pair, smp := range pairSamples { + ret := fac.Pairs[pair] + items = append(items, &FacState{ + Ret: ret, + Sample: smp, + Groups: make(map[string]string), + }) + } + // 计算每个采样所属的因子分组 + for _, factor := range facArr { + var pos []int + var tags []string + if factor == "marketCap" { + pos, tags = facMarketCap(items) + } else if factor == "vol1M" { + // 优于vol2w和marketCap + pos, tags = facVol1M(items) + } else if factor == "volStd" { + // 成交量波动较低的 + pos, tags = facVolStd(items) + } else if factor == "mont1" { + // 3M下,跟mont2不相上下 + pos, tags = facMont1(items, dayNum) + } else if factor == "mont2" { + pos, tags = facMont2(items, dayNum) + } else if factor == "mont1y" { + pos, tags = facMont1Y(items, dayNum) + } else if factor == "sharpe1y" { + pos, tags = facSharpe1Y(items, dayNum) + } else if factor == "score1y" { + pos, tags = facScore1Y(items, dayNum) + } else { + return errors.New("unknown factor: " + factor) + } + if len(pos)+1 != len(tags) { + return errors.New("invalid factor: " + factor) + } + start := 0 + for i, end := range pos { + tag := tags[i] + for j := start; j < end; j++ { + items[j].Groups[factor] = tag + } + start = end + } + tag := tags[len(tags)-1] + for j := start; j < len(items); j++ { + items[j].Groups[factor] = tag + } + } + // 组合因子分组 + groups := make(map[string][]*FacState) + for _, sta := range items { + var b strings.Builder + for _, f := range facArr { + b.WriteString(sta.Groups[f] + "+") + } + sta.Label = strings.TrimRight(b.String(), "+") + gpItems, _ := groups[sta.Label] + groups[sta.Label] = append(gpItems, sta) + end := sta.Sample.Offset + dayNum + if end > len(sta.Ret.Arr) { + end = len(sta.Ret.Arr) + } + // 计算未来分数,并进行分组打分 + futures := sta.Ret.Arr[sta.Sample.Offset:end] + if outBy == "profit" { + sta.OutScore = floats.Sum(futures) + } else if outBy == "sharpe" { + sta.OutScore, err_ = utils2.SharpeRatioBy(futures, 0.02, 365, true) + if err_ != nil { + return err_ + } + } else if outBy == "smart" || outBy == "" { + sharpe, err_ := utils2.SharpeRatioBy(futures, 0.02, 365, true) + if err_ != nil { + return err_ + } + profit := floats.Sum(futures) + sta.OutScore = math.Log2(profit) * sharpe + } else { + return errors.New("unknown outBy: " + outBy) + } + } + // 按输出分数降序 + sort.Slice(items, func(i, j int) bool { + return items[i].OutScore < items[j].OutScore + }) + for i, it := range items { + it.OutID = i + } + // 计算每个目标块应包含的FacState数量 + blockNum := int(math.Ceil(float64(len(items)) / float64(outNum))) + for key, gpList := range groups { + // 遍历分组中的每个FacState,根据其OutID确定所属块 + blocks, ok := groupMeas[key] + if !ok { + blocks = make([]*GpState, outNum) + for i := 0; i < outNum; i++ { + blocks[i] = &GpState{} + } + groupMeas[key] = blocks + } + for _, state := range gpList { + blockID := state.OutID / blockNum // 确定目标块ID + if blockID >= outNum { + blockID = outNum - 1 // 处理边界情况 + } + + // 更新该块的统计信息 + block := blocks[blockID] + block.count++ + block.sumScore += utils2.NanInfTo(state.OutScore, 0) + } + } + } + + countDsList := make([]*opt.ChartDs, len(groupMeas)) // 用于绘制数量分布图 + scoreDsList := make([]*opt.ChartDs, len(groupMeas)) // 用于绘制平均分数图 + // 生成x轴标签 + xLabels := make([]string, outNum) + for i := 0; i < outNum; i++ { + xLabels[i] = fmt.Sprintf("Block%d", i+1) + } + + keys := utils2.KeysOfMap(groupMeas) + slices.Sort(keys) + for i, key := range keys { + counts := make([]float64, outNum) + for j := 0; j < outNum; j++ { + counts[j] = groupMeas[key][j].count + } + countDsList[i] = &opt.ChartDs{ + Label: key, + Data: counts, + } + scores := make([]float64, outNum) + for j := 0; j < outNum; j++ { + bk := groupMeas[key][j] + scores[j] = bk.sumScore / bk.count + } + scoreDsList[i] = &opt.ChartDs{ + Label: key, + Data: scores, + } + } + + facNames := strings.Join(facArr, "+") + + // 生成数量分布折线图 + countPath := filepath.Join(config.GetDataDir(), fmt.Sprintf("fac_count_%dM.html", monNum)) + err = opt.DumpChart(countPath, "Distribution: "+facNames, xLabels, 5, nil, countDsList) + if err != nil { + return err + } + + // 生成平均分数折线图 + scorePath := filepath.Join(config.GetDataDir(), fmt.Sprintf("fac_score_%dM.html", monNum)) + err = opt.DumpChart(scorePath, "Average Scores: "+facNames, xLabels, 5, nil, scoreDsList) + if err != nil { + return err + } + log.Info(fmt.Sprintf("generate in BanDataDir: fac_score_%dM.html fac_count_%dM.html", monNum, monNum)) + return nil +} + +func facMarketCap(items []*FacState) ([]int, []string) { + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Sample.MarketCap - a.Sample.MarketCap) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} + +func facVol1M(items []*FacState) ([]int, []string) { + for _, it := range items { + start := max(0, len(it.Sample.Vols)-30) + it.Tmp = floats.Sum(it.Sample.Vols[start:]) + } + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Tmp - a.Tmp) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} + +func facVolStd(items []*FacState) ([]int, []string) { + for _, it := range items { + arr := it.Sample.Vols + data := make([]float64, 0, len(arr)) + for i, v := range arr[1:] { + data = append(data, v/arr[i]) + } + it.Tmp = utils2.StdDevVolatility(data, 1) + } + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Tmp - a.Tmp) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} + +func facMont1(items []*FacState, unitDays int) ([]int, []string) { + for _, it := range items { + start := max(0, it.Sample.Offset-unitDays) + it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset]) + } + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Tmp - a.Tmp) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} + +func facMont2(items []*FacState, unitDays int) ([]int, []string) { + for _, it := range items { + start := max(0, it.Sample.Offset-unitDays*2) + it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset]) + } + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Tmp - a.Tmp) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} + +func facMont1Y(items []*FacState, unitDays int) ([]int, []string) { + for _, it := range items { + start := max(0, it.Sample.Offset-unitDays*6) + it.Tmp = floats.Sum(it.Ret.Arr[start:it.Sample.Offset]) + } + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Tmp - a.Tmp) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} + +func facSharpe1Y(items []*FacState, unitDays int) ([]int, []string) { + for _, it := range items { + start := max(0, it.Sample.Offset-unitDays*6) + arr := it.Ret.Arr[start:it.Sample.Offset] + sharpe, err := utils2.SharpeRatioBy(arr, 0.02, 365, true) + if err != nil { + log.Info("calc sharpe fail", zap.Error(err)) + } + it.Tmp = sharpe + } + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Tmp - a.Tmp) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} + +func facScore1Y(items []*FacState, unitDays int) ([]int, []string) { + for _, it := range items { + start := max(0, it.Sample.Offset-unitDays*6) + arr := it.Ret.Arr[start:it.Sample.Offset] + sharpe, err_ := utils2.SharpeRatioBy(arr, 0.02, 365, true) + if err_ != nil { + log.Error("calc sharpe fail", zap.Error(err_)) + } + profit := floats.Sum(arr) + it.Tmp = math.Log2(profit) * sharpe + } + slices.SortFunc(items, func(a, b *FacState) int { + return int((b.Tmp - a.Tmp) / 1000) + }) + totalNum := float64(len(items)) + bigNum := int(totalNum * 0.3) + midEnd := int(totalNum*0.4) + bigNum + return []int{bigNum, midEnd}, []string{"big", "mid", "sml"} +} diff --git a/arb/common.go b/arb/common.go new file mode 100644 index 0000000..024a30a --- /dev/null +++ b/arb/common.go @@ -0,0 +1,159 @@ +package arb + +import ( + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/orm" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banstagy/com" + "gonum.org/v1/gonum/floats" + "math" + "slices" + "sort" +) + +func init() { + strat.AddStratGroup("arb", map[string]strat.FuncMakeStrat{ + "stat": MeanRegressStat, + }) +} + +type spikeTuple struct { + A *orm.ExSymbol + B *orm.ExSymbol + Score float64 // 分数,越高越好 + BigSlope float64 // 大趋势平均斜率 + PftRate float64 // (hh-ll)/minChg平均倍率 + RawXBig int // 价格比率与大周期均线交叉次数 + LastDiff float64 // 最新fast与大趋势偏移,是PftRate倍数,越小越好 +} + +/* +发现两个品种价格比率套利机会。 +通过此函数发现的机会实际上是交易量很小,价格变动百分比较大的小品种的波动。如XEM,最小变动0.6%,不如直接交易这个品种 +要求:EMA1333较稳定,(hh-ll)/maxChg较大,ratio和大均线交叉次数较多 +建议:pftRate>0.4,rawXBig>200,score>0.3 +*/ +func findSpikeTuples(endMS int64, tf string, barNum, hlWindow, itemMax int) ([]*spikeTuple, *errs.Error) { + // 获取所有标的的K线 + exgId := exg.Default.Info().ID + exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear) + exsList, barList, _, err := getExsBars(exg.Default, exsMap, tf, 0, endMS, barNum) + if err != nil { + return nil, err + } + // 计算周期内关键指标 + minChg := 0.0005 * 4 * 4 // 变动至少是两次开平仓手续费的4倍 + bigSlopeDist := 120 // 计算大周期斜率的距离 + items := make([]*spikeTuple, 0, len(exsList)*2) + halfWindow := hlWindow / 2 + if hlWindow%2 > 0 { + halfWindow += 1 + } + for i := 0; i < len(exsList); i++ { + barsA := barList[i] + for j := i + 1; j < len(exsList); j++ { + barsB := barList[j] + barsRes := getAlignBars(barsA, barsB) + a, b := barsRes[0], barsRes[1] + if len(a) < barNum/2 { + continue + } + var ma = com.NewEMA(0.08) + //var maSlow = common.NewEMA(0.03) + var maBig = com.NewEMA(0.0015) + var rawXBig = com.NewCrossLog() + //var rawXfa = common.NewCrossLog() + //var vmax, vmin = float64(0), float64(0) + var raw = make([]float64, 0, len(a)) + var bbRates = make([]float64, 0, len(a)/3) + var spikeNum = 0 + var maBigs = make([]float64, 0, len(a)) + var bigSlopes = make([]float64, 0, 100) + for m := 0; m < len(a); m++ { + ratio := a[m].Close / b[m].Close + raw = append(raw, ratio) + maVal := ma.Update(ratio) + maBigV := maBig.Update(ratio) + if math.IsNaN(maBigV) { + continue + } + maBigs = append(maBigs, maBigV) + if len(maBigs)%bigSlopeDist == 0 && len(maBigs) > 0 { + // 采样大周期斜率 + bigSlopes = append(bigSlopes, math.Abs(maBigV/maBigs[len(maBigs)-bigSlopeDist]-1)) + } + if m%halfWindow == 0 && len(raw) > hlWindow { + // 计算hh-ll的盈利空间 + near5 := raw[len(raw)-hlWindow:] + bbRates = append(bbRates, (slices.Max(near5)-slices.Min(near5))/maVal/minChg) + } + // 计算raw与big均线交叉次数 + if ratio != maBigV { + rawXBig.Update(ratio - maBigV) + fxs := rawXBig.Get() + if fxs == 1 { + // 只记录上穿,避免交叉太多 + spikeNum += 1 + } + } + // 计算比率在均线附近振幅可盈利的次数 + //rawXfa.Update(ratio - maVal) + //rxf := rawXfa.Get() + //if rxf == 1 || rxf == -1 { + // pft := (vmax-vmin)/maVal > minChg + // if pft { + // spikeNum += 1 + // } + // vmin = maVal + // vmax = maVal + //} + //if ratio > maVal { + // vmax = max(vmax, ratio) + //} else { + // vmin = min(vmin, ratio) + //} + } + // 平均大周期斜率 + avgSlope := floats.Sum(bigSlopes) * 50 / float64(len(bigSlopes)) + slopeRate := math.Pow(1-min(1, avgSlope), 1.5) + // 平均盈利空间 + sort.Slice(bbRates, func(i, j int) bool { + return bbRates[i] < bbRates[j] + }) + bbRates = bbRates[:len(bbRates)/2] + avgPftRate := floats.Sum(bbRates) / float64(len(bbRates)) + // 与大周期交叉的分数,交叉少于200权重降低 + hitScore := math.Log(float64(spikeNum)/200 + 1) + val := hitScore * avgPftRate * slopeRate + // 计算fast与大周期偏移 + lastDiff := math.Abs(ma.Val/maBig.Val-1) / minChg / avgPftRate + items = append(items, &spikeTuple{ + A: exsList[i], + B: exsList[j], + Score: val, + BigSlope: avgSlope, + PftRate: avgPftRate, + RawXBig: spikeNum, + LastDiff: lastDiff, + }) + } + } + sort.Slice(items, func(i, j int) bool { + return items[i].Score > items[j].Score + }) + tops := items[:min(1000, int(float64(len(items))*0.1))] + res := make([]*spikeTuple, 0, len(tops)/2) + hits := make(map[string]int) + for _, t := range tops { + numA, _ := hits[t.A.Symbol] + numB, _ := hits[t.B.Symbol] + if max(numA, numB) < itemMax { + hits[t.A.Symbol] = numA + 1 + hits[t.B.Symbol] = numB + 1 + res = append(res, t) + } + } + return res, nil +} diff --git a/arb/stat_group.go b/arb/stat_group.go new file mode 100644 index 0000000..f59b228 --- /dev/null +++ b/arb/stat_group.go @@ -0,0 +1,52 @@ +package arb + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/orm" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + "go.uber.org/zap" + "sort" +) + +func StatGroupPairs() *errs.Error { + exgId := "binance" + endMS := config.TimeRange.EndMS + // 生成待订阅标的列表 + exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear) + limit := 720 + exsList, barList, _, err := getExsBars(exg.Default, exsMap, "1h", 0, endMS, limit) + if err != nil { + return err + } + arr := make([]*core.StrFloat, 0, len(exsList)) + for i, exs := range exsList { + var sumVal float64 + bars := barList[i] + if float64(len(bars)) < float64(limit)*0.9 { + continue + } + for _, b := range bars { + sumVal += b.Volume * b.Close + } + arr = append(arr, &core.StrFloat{ + Str: exs.Symbol, + Val: sumVal, + }) + } + // 按成交量降序,取前30% + sort.Slice(arr, func(i, j int) bool { + return arr[i].Val > arr[j].Val + }) + cutNum := int(float64(len(arr)) * 0.2) + cutPairs := make([]string, 0, cutNum) + for i := 0; i < cutNum; i++ { + base, _, _, _ := core.SplitSymbol(arr[i].Str) + cutPairs = append(cutPairs, base) + } + log.Info("cut 20% of pairs", zap.Int("num", cutNum), zap.Int("total", len(arr)), zap.Strings("items", cutPairs)) + return nil +} diff --git a/arb/stat_job.go b/arb/stat_job.go new file mode 100644 index 0000000..8ec1da0 --- /dev/null +++ b/arb/stat_job.go @@ -0,0 +1,271 @@ +package arb + +import ( + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "github.com/banbox/banexg/utils" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "maps" + "math" + "math/rand" + "path/filepath" +) + +func init() { + strat.AddStratGroup("arb", map[string]strat.FuncMakeStrat{ + "gp": ArbGroupJob, + }) +} + +/* +strengths widRatios +rate: 0 -0.002491 0.859934 +rate: 14234 -0.000915 0.946673 +rate: 28468 -0.000089 0.997275 +rate: 42702 0.000818 1.048731 +rate: 56936 0.002571 1.166192 +rate: 71170 0.007989 1.619902 +*/ + +func ArbGroupJob(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenFast := 6 + lenSlow := 20 + lenMid := 96 + lenStd := 96 + bollWid := 0.0043 + lastSplitMS := int64(0) + tfMSecs := int64(60000) + dayMS := int64(utils.TFToSecs("1d") * 1000) + env, _ := ta.NewBarEnv(core.ExgName, core.Market, "", "1m") + var rate *ta.Series + groups := make(map[*strat.StratJob]float64) + hits := make(map[string]float64) + warmNum := 120 + var widths []float64 + var maxRate float64 + var position float64 + var posTag string + var rates []*TimeFlt + dumpDir := filepath.Join(config.GetDataDir(), "dump") + return &strat.TradeStrat{ + WarmupNum: warmNum, + RunTimeFrames: []string{"1m"}, + BatchInOut: true, + OnBar: func(s *strat.StratJob) { + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + var curMS int64 + newNum := 0 + hitDup := maps.Clone(hits) + for _, j := range jobs { + curMS = max(curMS, j.Env.TimeStop) + if _, ok := hitDup[j.Symbol.Symbol]; ok { + delete(hitDup, j.Symbol.Symbol) + } else { + newNum += 1 + } + } + alignDayMS := utils.AlignTfMSecs(curMS, dayMS) + if alignDayMS > lastSplitMS || newNum > 0 || len(hitDup) > 0 { + // 每天刷新品种分组 + for j := range groups { + j.CloseOrders(&strat.ExitReq{Tag: "switch", Log: true}) + } + offsetMS := alignDayMS - dayMS + dateStr := btime.ToDateStr(offsetMS, "20060102") + dumpPath := filepath.Join(dumpDir, dateStr+".html") + if len(rates) > 1000 { + log.Info("try dump rates", zap.String("date", dateStr), zap.Int("num", len(rates))) + err := dumpRateChart(rates, "1m", dumpPath, dateStr) + if err != nil { + log.Error("dumpRateChart fail", zap.Error(err)) + } + } + rates = nil + rand.Shuffle(len(jobs), func(i, j int) { + jobs[i], jobs[j] = jobs[j], jobs[i] + }) + env.Reset() + rate = env.NewSeries(nil) + groups = make(map[*strat.StratJob]float64) + hits = make(map[string]float64) + for i, j := range jobs { + if i%2 == 0 { + groups[j] = -1 + } else { + groups[j] = 1 + } + hits[j.Symbol.Symbol] = j.Env.Close.Get(lenMid) + } + env.TimeStop = curMS - int64(lenMid)*tfMSecs + env.TimeStart = env.TimeStop - tfMSecs + for i := warmNum; i >= 0; i-- { + var price1, price2 float64 + var num1, num2 float64 + for j, tag := range groups { + base := hits[j.Symbol.Symbol] + ratec := j.Env.Close.Get(i) / base + if tag < 0 { + price1 += ratec + num1 += 1 + } else { + price2 += ratec + num2 += 1 + } + } + price1 /= num1 + price2 /= num2 + env.TimeStart += tfMSecs + env.TimeStop += tfMSecs + rate.Append(price1 / price2) + _ = ta.EMA(rate, lenFast) + _ = ta.EMA(rate, lenSlow) + maMid := ta.EMA(rate, lenMid).Get(0) + dev := ta.StdDev(rate, lenStd) + stdDev := ta.SMA(dev, lenStd).Get(0) * 1.7 + bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5 + widths = append(widths, bollWidAvg) + } + lastSplitMS = alignDayMS + } + var price1, price2 float64 + var num1, num2 float64 + for j, tag := range groups { + base := hits[j.Symbol.Symbol] + ratec := j.Env.Close.Get(0) / base + if tag < 0 { + price1 += ratec + num1 += 1 + } else { + price2 += ratec + num2 += 1 + } + } + price1 /= num1 + price2 /= num2 + curRate := price1 / price2 + env.TimeStart += tfMSecs + env.TimeStop += tfMSecs + rate.Append(curRate) + rates = append(rates, &TimeFlt{ + Stamp: curMS, + Val: curRate, + }) + maFast := ta.EMA(rate, lenFast) + maSlow := ta.EMA(rate, lenSlow) //66 + maMidS := ta.EMA(rate, lenMid) + maMid := maMidS.Get(0) + dev := ta.StdDev(rate, lenStd) + stdDev := ta.SMA(dev, lenStd).Get(0) * 1.7 + bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5 + widths = append(widths, bollWidAvg) + zScore := (curRate - maMid) / bollWidAvg + strength := maMid/maMidS.Get(warmNum) - 1 + widDelta := bollWidAvg - widths[len(widths)-60] + widRatio := bollWidAvg / widths[len(widths)-warmNum] + fastXSlow := maFast.Cross(maSlow) + fastXMid := maFast.Cross(maMidS) + + absStrength := math.Abs(strength) + mkt := "" + if absStrength > 0.01 || math.Abs(zScore) > 4 { + // D: 结构性跳变,暂停交易 + mkt = "D" + } else if widRatio > 1.5 && widDelta > 0 || bollWidAvg >= 0.008 { + // C: 高波动突破 + mkt = "C" + } else if absStrength >= 0.003 || widRatio > 1.3 && widDelta > 0 { + // B: 趋势市场 + mkt = "B" + } else if absStrength < 0.002 && widRatio < 1.2 && widDelta <= 0 { + // A: 低波动区间 + mkt = "A" + } else { + //if absStrength >= 0.001 && absStrength < 0.002 && widRatio < 1.3 + // E: 低频弱趋势,中性,混合策略 + mkt = "E" + } + dirt := -1.0 + if strength > 0 { + dirt = 1 + } + if math.Abs(zScore) > maxRate*dirt { + maxRate = zScore + } + allow1, allow2 := false, false + if mkt == "A" { + allow1 = true + } else if mkt == "B" || mkt == "C" { + // B明显趋势 或 C波动扩大方向明确 + allow2 = true + } else if mkt == "E" { + allow1, allow2 = true, true + } + openDirt := 0.0 + openTag := "" + dateStr := btime.ToDateStr(curMS, core.DefaultDateFmt) + if allow1 { + expPft := math.Abs(curRate - maMid) + if math.Abs(maxRate) >= 1.5 && expPft > 0.002 && float64(fastXSlow)*dirt == -1 { + openDirt = -dirt + openTag = "rev" + log.Info("open rev", zap.String("date", dateStr), zap.Float64("z", zScore), + zap.Float64("width", bollWidAvg), zap.Float64("widDelta", widDelta)) + } + } + if allow2 && float64(fastXMid)*dirt == 1 { + if absStrength >= 0.003 || widRatio > 1.3 && widDelta > 0 { + openDirt = dirt + openTag = "follow" + log.Info("open trend", zap.String("date", dateStr), zap.Float64("z", zScore), + zap.Float64("width", bollWidAvg), zap.Float64("widDelta", widDelta), + zap.Float64("strength", strength), zap.Float64("widRatio", widRatio)) + } + } + if position != openDirt && openDirt != 0 { + if position != 0 { + log.Info("close pos", zap.String("date", dateStr), zap.String("tag", "reverse"), zap.Float64("dirt", position)) + for j := range groups { + j.CloseOrders(&strat.ExitReq{Tag: "reverse"}) + } + } + for j, tag := range groups { + j.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: tag != openDirt, + }) + } + log.Info("open pos", zap.String("date", dateStr), zap.String("tag", openTag), zap.Float64("dirt", openDirt)) + position = openDirt + posTag = openTag + } else if position != 0 { + exitTag := "" + if posTag == "rev" { + if position*dirt > 0 { + exitTag = "tpRev" + } else if math.Abs(zScore) >= 3 { + exitTag = "slRev" + } + } else if posTag == "follow" { + if position*dirt < 0 && (math.Abs(zScore) > 0.5 || widDelta < 0) { + exitTag = "slFol" + } else if position*dirt > 0 && math.Abs(zScore) >= 1 { + exitTag = "tpFol" + } + } + if exitTag != "" { + log.Info("close pos", zap.String("date", dateStr), zap.String("tag", exitTag), zap.Float64("dirt", position)) + position = 0 + posTag = "" + for j := range groups { + j.CloseOrders(&strat.ExitReq{Tag: exitTag}) + } + } + } + }, + } +} diff --git a/arb/stat_mean.go b/arb/stat_mean.go new file mode 100644 index 0000000..272013d --- /dev/null +++ b/arb/stat_mean.go @@ -0,0 +1,373 @@ +package arb + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "github.com/banbox/banta" + "go.uber.org/zap" + "math" + "sort" + "strconv" + "sync" +) + +/* +统计套利:对两个交易对的价格比率下注 +价格比率较稳定的: + 快速穿越大趋势跟随开单 + 超出上下轨,回弹交叉开单 +价格比率明显单边趋势: + 在标准差30%以内交叉,跟随大趋势方向开单 + +强劲单边跟随: + 平时价格比率小幅上下波动,但偶尔有强劲单边价格剧烈运动:GLM/USDT:USDT, XEM/USDT:USDT 20241001 + +瞬时极端波动: + 某段时间,价格比率快速上下极端波动,GLM/USDT:USDT, XEM/USDT:USDT 20241025 + +*/ + +func MeanRegressStat(p *config.RunPolicyConfig) *strat.TradeStrat { + var sta *StatTwo + var jobs = make(map[string]*strat.StratJob) + return &strat.TradeStrat{ + WarmupNum: 1000, + BatchInOut: true, + OnStartUp: func(s *strat.StratJob) { + jobs[s.Symbol.Symbol] = s + }, + OnBar: func(s *strat.StratJob) { + if sta == nil { + sta = newStatTwo(jobs, nil) + } + e := s.Env + if e.TimeStart > sta.barMS { + sta.barMS = e.TimeStart + } + sta.OnPrice(s.Symbol.Symbol, e.Close.Get(0)) + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + sta.onStampDone() + }, + } +} + +const ( + slopeDist = 150 + maSlopeDist = 500 +) + +type StatTwo struct { + barMS int64 // 收到最新bar的时间戳 + prices map[string]float64 // 当前时间戳下,所有品种的价格 + jobs map[string]*strat.StratJob // 所有的任务 + cacheSize int // Tuple中保留历史数据的最小长度 + lock sync.Mutex + Tuples map[string]map[string]*Tuple +} + +type Tuple struct { + A *strat.StratJob + B *strat.StratJob + env *banta.BarEnv + raw *banta.Series + maFast *banta.Series + maSlow *banta.Series + maBig *banta.Series + rawSbBig *banta.Series // sma(raw-maBig, 500) + faXSl int // fast slow交叉:1快线上穿慢线;-1快线下穿慢线 + rawXBig int // raw big交叉 + maSlope float64 // ma斜率(和500个之前) + slope float64 // 价格比率斜率(和150个之前) + sort float64 // 排序分数,降序 + stdDev float64 // 标准差 + allowType int // 允许开单类型: 1突破布林带回弹;2快速穿越大趋势线;3中心交叉跟随趋势 + allowDirt int // 允许开单方向: 1做多A做空B; -1做空A做多B + openType int // 开单类型:从allowType赋值 + openRate float64 // 开单的价格比率,0表示未开单 + openAt int64 // 开单的时间戳 +} + +func newStatTwo(jobs map[string]*strat.StratJob, tuples [][]string) *StatTwo { + tupleMap := make(map[string]map[string]*Tuple) + res := &StatTwo{ + cacheSize: 2000, + jobs: jobs, + Tuples: tupleMap, + } + if len(tuples) == 0 { + codes := make([]string, 0, len(jobs)) + for key := range jobs { + codes = append(codes, key) + } + for i := 0; i < len(codes); i++ { + for j := i + 1; j < len(codes); j++ { + tuples = append(tuples, []string{codes[i], codes[j]}) + } + } + } + if len(tuples) > 7000 { + log.Warn("too many tuples may cause OOM!", zap.Int("num", len(tuples))) + } + for _, va := range tuples { + a, b := va[0], va[1] + if a > b { + a, b = b, a + } + subMap, ok := tupleMap[a] + if !ok { + subMap = make(map[string]*Tuple) + tupleMap[a] = subMap + } + tup, ok := subMap[b] + if !ok { + baseA, _, _, _ := core.SplitSymbol(a) + baseB, _, _, _ := core.SplitSymbol(b) + symbol := fmt.Sprintf("%s/%s", baseA, baseB) + env, _ := banta.NewBarEnv(core.ExgName, core.Market, symbol, "1m") + env.MaxCache = res.cacheSize + raw := env.NewSeries(nil) + A, _ := jobs[a] + B, _ := jobs[b] + tup = &Tuple{ + A: A, + B: B, + raw: raw, + env: env, + } + subMap[b] = tup + } + } + return res +} + +func (s *StatTwo) OnPrice(symbol string, price float64) { + s.lock.Lock() + s.prices[symbol] = price + s.lock.Unlock() +} + +func (s *StatTwo) popTuples() []*Tuple { + s.lock.Lock() + tasks := make(map[string]map[string]bool) + for codeA := range s.prices { + subMap := make(map[string]bool) + for codeB := range s.prices { + if codeA >= codeB { + continue + } + subMap[codeB] = true + } + tasks[codeA] = subMap + } + var res = make([]*Tuple, 0, len(s.prices)*2) + for codeA, subMap := range tasks { + for codeB := range subMap { + ratio := s.prices[codeA] / s.prices[codeB] + t := s.tuple(codeA, codeB, ratio) + if t == nil { + continue + } + res = append(res, t) + } + } + s.prices = make(map[string]float64) + s.lock.Unlock() + return res +} + +func (s *StatTwo) onStampDone() { + tuples := s.popTuples() + // 筛选两种开单类型 + size := len(tuples) / 3 + list1 := make([]*Tuple, 0, size) + list2 := make([]*Tuple, 0, size) + list3 := make([]*Tuple, 0, size) + for _, t := range tuples { + t.calcInOuts() + if t.allowType == 1 { + list1 = append(list1, t) + } else if t.allowType == 2 { + list2 = append(list2, t) + } else if t.allowType == 3 { + list3 = append(list3, t) + } + } + var list = list1 + if len(list) == 0 { + if len(list2) == 0 { + if len(list3) == 0 { + return + } + list = list3 + } else { + list = list2 + } + } + // 按分数降序,取前三个中没有开单的 + var it *Tuple + sort.Slice(list, func(i, j int) bool { + return list[i].sort > list[j].sort + }) + gap := len(list) / 2 + list = append(list[gap:], list[:gap]...) + for i, v := range list { + if v.openType == 0 && v.A.OrderNum == 0 && v.B.OrderNum == 0 { + it = v + break + } + if i == 2 { + break + } + } + if it == nil { + return + } + // 下单 + shortA := it.allowDirt < 0 + errA := it.A.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(it.allowType), + Short: shortA, + }) + if errA == nil { + errB := it.B.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(it.allowType), + Short: !shortA, + }) + if errB != nil { + it.A.Entrys = nil + } else { + it.openType = it.allowType + it.openRate = it.raw.Get(0) + it.openAt = it.env.TimeStop + } + } + //dateStr := btime.ToDateStr(s.barMS, core.DefaultDateFmt) + //fmt.Printf("%s %s - %s type=%v sort=%.3f slope=%.3f bigSlope=%.3f\n", dateStr, it.A.Symbol.Symbol, + // it.B.Symbol.Symbol, it.allowType, it.sort, it.slope, it.maSlope) +} + +func (s *StatTwo) tuple(a, b string, ratio float64) *Tuple { + if a > b { + a, b = b, a + ratio = 1 / ratio + } + subMap, ok := s.Tuples[a] + if !ok { + return nil + } + res, ok := subMap[b] + if ok { + res.update(s.barMS, ratio) + return res + } + return nil +} + +func (t *Tuple) update(stamp int64, ratio float64) { + t.env.TimeStart = stamp + t.env.TimeStop = stamp + t.env.TFMSecs + t.env.BarNum += 1 + t.raw.Append(ratio) + t.maFast = banta.EMA(t.raw, 24) + t.maSlow = banta.EMA(t.raw, 66) + t.maBig = banta.EMA(t.raw, 1333) + t.rawSbBig = banta.SMA(t.raw.Sub(t.maBig), 500) + t.faXSl = t.maFast.Cross(t.maSlow) + t.rawXBig = t.raw.Cross(t.maBig) + sdev := banta.StdDev(t.raw, 1333) + t.stdDev = banta.SMA(sdev, 800).Get(0) * 1.7 + if t.raw.Len() > slopeDist { + // 计算近期斜率,优先用基于历史150偏移计算 + // 当基于均线历史150偏移的绝对变动区间更小时,基于均线计算 + old := t.raw.Get(slopeDist) + oldMa := t.maBig.Get(slopeDist) + slope := ratio/old - 1 + slopeMa := ratio/oldMa - 1 + if math.Abs(slopeMa) < math.Abs(slope) { + slope = slopeMa + } + t.slope = slope + } + if t.maBig.Len() > maSlopeDist { + t.allowType = 0 + t.sort = 0 + t.maSlope = t.maBig.Get(0)/t.maBig.Get(maSlopeDist) - 1 + } +} + +func (t *Tuple) calcInOuts() { + if t.maBig.Len() < maSlopeDist { + return + } + //fastVal, fastVal30 := t.maFast.Get(0), t.maFast.Get(30) + //if t.rawXBig == -1 || t.rawXBig == 1 { + // // 快速穿越大趋势,开单 + // fastSlope := math.Abs(fastVal/fastVal30 - 1) + // distRate := t.rawSbBig.Get(0) / stdDev + // if fastSlope > 0.01 && distRate*float64(t.rawXBig) < 0 && math.Abs(distRate) > 0.7 { + // // 要求穿过大趋势线时,具备较高的斜率,且过去500个平均与大周期的差超过70%标准差 + // t.allowType = 2 + // t.allowDirt = t.rawXBig + // t.sort = math.Abs(distRate) + // } + //} + maSlopeAbs := math.Abs(t.maSlope) + //diff := math.Abs(t.slope - t.maSlope) + //if maSlopeAbs < min(math.Abs(t.slope), 0.02) && diff > 0.015 { + // // 突破布林带上下轨,回弹时开单 + // bigVal := t.maBig.Get(0) + // slowPrev := t.maSlow.Get(60) + // // 按maSlope进行惩罚,极端0.02时削减64% + // sortV := diff * math.Pow(1-maSlopeAbs*20, 2) + // if t.faXSl == -1 && max(fastVal, fastVal30) > bigVal+stdDev { + // // 均线上方,突破上界,快线下穿慢线 + // if fastVal > slowPrev { + // t.allowType = 1 + // t.allowDirt = t.faXSl + // t.sort = sortV + // } + // } else if t.faXSl == 1 && min(fastVal, fastVal30) < bigVal-stdDev { + // // 均线下方,突破下界,快线上穿慢线 + // if fastVal < slowPrev { + // t.allowType = 1 + // t.allowDirt = t.faXSl + // t.sort = sortV + // } + // } + //} + nearToCenter := math.Abs(t.raw.Get(0)-t.maBig.Get(0)) < t.stdDev*0.3 + if maSlopeAbs > 0.02 && nearToCenter && (t.faXSl == 1 || t.faXSl == -1) && t.maSlope*float64(t.faXSl) > 0 { + // 明显大趋势,在中心附近交叉,按大趋势方向开单 + t.allowType = 3 + t.allowDirt = t.faXSl + t.sort = maSlopeAbs + } + closeOd := false + if t.openType == 2 { + // 根据穿越大趋势开单:回穿大趋势平仓、快线慢线交叉平仓 + if t.faXSl*t.allowDirt == -1 || t.rawXBig*t.allowDirt == -1 { + closeOd = true + } + } else if t.openType == 1 && t.faXSl*t.allowDirt == -1 { + // 根据突破上下轨回弹开单,(盈利达到标准差30% || 持有时间超过700个bar):快线慢线交叉平仓 + pft30 := (t.raw.Get(0)-t.openRate)*float64(t.allowDirt) > t.stdDev*0.3 + //costBarNum := int((t.env.TimeStop - t.openAt) / t.env.TFMSecs) + if pft30 { //|| costBarNum > 1000 { + closeOd = true + } + } else if t.openType == 3 && t.faXSl*t.allowDirt == -1 { + // 中心附近交叉,跟随趋势的,再次交叉时平仓 + closeOd = true + } + if closeOd { + t.A.CloseOrders(&strat.ExitReq{Tag: "exit"}) + t.B.CloseOrders(&strat.ExitReq{Tag: "exit"}) + t.openType = 0 + t.openRate = 0 + t.openAt = 0 + } +} diff --git a/arb/stat_prof.go b/arb/stat_prof.go new file mode 100644 index 0000000..4a11d3e --- /dev/null +++ b/arb/stat_prof.go @@ -0,0 +1,348 @@ +package arb + +import ( + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/orm" + "github.com/banbox/banbot/strat" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + "github.com/banbox/banstagy/com" + "go.uber.org/zap" + "gonum.org/v1/gonum/floats" + "math" + "sort" + "strconv" + "strings" +) + +type IJV struct { + I int + J int + V float64 +} + +func ProfTuples() *errs.Error { + exgId := "binance" + tf := "1m" + endMS := config.TimeRange.EndMS + // 生成待订阅标的列表 + jobs := make(map[string]*strat.StratJob) + exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear) + exsList, barList, jobs, err := getExsBars(exg.Default, exsMap, "1h", 0, endMS, 720) + if err != nil { + return err + } + // 先从1h级别过滤,筛选比率较稳定的,取前10%,最多2k个 + maLen := 17 + stepMa := 8 //对应480分钟 + ts := make([]*IJV, 0, len(exsList)*2) + for i := 0; i < len(exsList); i++ { + barsA := barList[i] + for j := i + 1; j < len(exsList); j++ { + barsB := barList[j] + barsRes := getAlignBars(barsA, barsB) + if len(barsRes) == 0 { + continue + } + a, b := barsRes[0], barsRes[1] + if len(a) <= 100 { + continue + } + slopes := make([]float64, 0, 90) + sum := float64(0) + avgs := make([]float64, 0, len(a)) + for m := 0; m < len(a); m++ { + sum += a[m].Close / b[m].Close + if m >= maLen { + sum -= a[m-maLen].Close / b[m-maLen].Close + avg := sum / float64(maLen) + avgs = append(avgs, avg) + if len(avgs)%stepMa == 1 && len(avgs) > stepMa { + slopes = append(slopes, math.Abs(avg/avgs[len(avgs)-stepMa-1]-1)*100) + } + } + } + if len(slopes) < 20 { + continue + } + avgSlope := floats.Sum(slopes) / float64(len(slopes)) + if avgSlope < 0.05 { + continue + } + ts = append(ts, &IJV{I: i, J: j, V: avgSlope}) + } + } + sort.Slice(ts, func(i, j int) bool { + return ts[i].V < ts[j].V + }) + tops := ts[:min(2000, int(float64(len(ts))*0.1))] + log.Info("cut top tuples", zap.Int("num", len(tops)), zap.Int("all", len(ts))) + var tuples = make([][]string, 0, len(tops)) + its := make([]*com.CodeVal, 0, len(tops)) + for _, t := range tops { + ca, cb := exsList[t.I].Symbol, exsList[t.J].Symbol + tuples = append(tuples, []string{ca, cb}) + codeA, _, _, _ := core.SplitSymbol(ca) + codeB, _, _, _ := core.SplitSymbol(cb) + if codeA > codeB { + codeA, codeB = codeB, codeA + } + its = append(its, &com.CodeVal{Code: codeA + "/" + codeB, Val: t.V}) + } + printCodeVals(its) + sta := newStatTwo(jobs, tuples) + barNum := 0 + // 输出csv信息 + var datas = make(map[string][]float64) + // 每行表示一对;每列表示一个时间帧 + var rows = make(map[string][]float64) + colCount := 0 // 列数量,即时间采样数量 + // 进度条 + totalSecs := (endMS - config.TimeRange.StartMS) / 1000 + pbar := utils.NewPrgBar(int(totalSecs), "Run") + defer pbar.Close() + onStaBatch := func(arr []*Tuple) { + if barNum%500 != 0 && barNum < 1800 { + return + } + colCount += 1 + for _, it := range arr { + code := it.env.Symbol + if math.IsNaN(it.maSlope) { + continue + } + absVal := math.Abs(it.maSlope * 100) + + old, _ := datas[code] + datas[code] = append(old, absVal) + + col, _ := rows[code] + if len(col) < colCount { + col = append(col, make([]float64, colCount-len(col))...) + } + rows[code] = append(col, absVal) + } + } + // Kline回调 + onBar := func(b *orm.InfoKline, nexts []*orm.InfoKline) { + if b.Time > sta.barMS { + if !b.IsWarmUp && sta.barMS > 0 { + pbar.Add(int((b.Time - sta.barMS) / 1000)) + } + barNum += 1 + sta.barMS = b.Time + arr := sta.popTuples() + if !b.IsWarmUp { + onStaBatch(arr) + } + } + sta.OnPrice(b.Symbol, b.Close) + } + var replayErr *errs.Error + verCh := make(chan int, 1) + onData := func(evt *orm.DataSeries, _ []*orm.DataSeries) { + bar, err_ := orm.AsKline(evt) + if err_ != nil { + replayErr = errs.New(errs.CodeRunTime, err_) + select { + case verCh <- -1: + default: + } + return + } + onBar(bar, nil) + } + err = biz.RunHistSeries(&biz.RunHistSeriesArgs{ + ExsList: exsList, + Start: config.TimeRange.StartMS, + End: config.TimeRange.EndMS, + VerCh: verCh, + OnData: onData, + TfWarms: map[string]int{tf: 0}, // 无需预热,必须所有bar一同运行 + }) + if err != nil { + return err + } + if replayErr != nil { + return replayErr + } + // 计算平均大趋势斜率 + var items = make([]*com.CodeVal, 0, len(datas)) + var avgs = make(map[string]float64) + for code, arr := range datas { + avgVal := floats.Sum(arr) / float64(len(arr)) + items = append(items, &com.CodeVal{Code: code, Val: avgVal}) + avgs[code] = avgVal + } + printCodeVals(items) + // 生成csv文件 + outRows := make([][]string, 0) + for key, row := range rows { + if len(row) < colCount { + row = append(row, make([]float64, colCount-len(row))...) + rows[key] = row + } + out := make([]string, 0, colCount+2) + out = append(out, key, strconv.FormatFloat(avgs[key], 'f', 2, 64)) + for _, v := range row { + out = append(out, strconv.FormatFloat(v, 'f', 2, 64)) + } + outRows = append(outRows, out) + } + return utils.WriteCsvFile("D:/slopes.csv", outRows, false) +} + +func getExsBars(exchange banexg.BanExchange, exsMap map[int32]*orm.ExSymbol, tf string, startMS, endMS int64, limit int) ([]*orm.ExSymbol, [][]*banexg.Kline, map[string]*strat.StratJob, *errs.Error) { + _, err := orm.LoadMarkets(exg.Default, false) + if err != nil { + return nil, nil, nil, err + } + err = orm.BulkDownOHLCV(exchange, exsMap, tf, startMS, endMS, limit, nil) + if err != nil { + return nil, nil, nil, err + } + exsList := make([]*orm.ExSymbol, 0, len(exsMap)) + barList := make([][]*banexg.Kline, 0, len(exsMap)) + jobs := make(map[string]*strat.StratJob) + for _, exs := range exsMap { + _, quote, _, ident := core.SplitSymbol(exs.Symbol) + if quote != "USDT" || ident != "" { + continue + } + _, bars, err := orm.GetOHLCV(exs, tf, startMS, endMS, limit, false) + if err != nil { + return nil, nil, nil, err + } + exsList = append(exsList, exs) + barList = append(barList, bars) + jobs[exs.Symbol] = &strat.StratJob{Symbol: exs} + } + return exsList, barList, jobs, nil +} + +func getAlignBars(items ...[]*banexg.Kline) [][]*banexg.Kline { + if len(items) == 0 { + return nil + } + if len(items) == 1 { + return items + } + first := items[0] + if len(first) == 0 { + return nil + } + headLen, headStart, headStop := len(first), first[0].Time, first[len(first)-1].Time + allAlign, minLen := true, headLen + for _, a := range items { + if len(a) == 0 { + return nil + } + if len(a) != headLen || a[0].Time != headStart || a[len(a)-1].Time != headStop { + allAlign = false + } + minLen = min(minLen, len(a)) + } + if allAlign { + return items + } + var resList = make([][]*banexg.Kline, len(items)) + var indexs = make([]int, len(items)) + started := false + reachEnd := false + for { + stampCounts := make(map[int64]int) + maxNum, mostTime, maxTime := 0, int64(0), int64(0) + for i, idx := range indexs { + b := items[i][idx] + num, _ := stampCounts[b.Time] + stampCounts[b.Time] = num + 1 + if num >= maxNum { + maxNum, mostTime = num+1, b.Time + } + maxTime = max(maxTime, b.Time) + } + idxChange := false + if len(stampCounts) > 1 { + // 至少2个不同时间戳,将较旧的移动 + if maxNum <= len(indexs)/2 { + mostTime = maxTime + } + for i, idx := range indexs { + arr := items[i] + for arr[idx].Time < mostTime { + idx += 1 + if idx >= minLen { + reachEnd = true + break + } + } + if idx > indexs[i] { + idxChange = true + indexs[i] = idx + } + } + } else if !started { + started = true + } + if started && maxNum >= len(indexs)/2 { + // 已开始,maxNum超过一半时有效 + for i, idx := range indexs { + b := items[i][idx] + res := resList[i] + if b.Time > mostTime { + b = res[len(res)-1] + } else if b.Time < mostTime { + panic("bad logic") + } else { + indexs[i] = idx + 1 + idxChange = true + if idx+1 >= minLen { + reachEnd = true + } + } + resList[i] = append(res, b) + } + } + if !idxChange { + for i, idx := range indexs { + arr := items[i] + if arr[idx].Time <= mostTime { + idx += 1 + indexs[i] = idx + if idx >= minLen { + reachEnd = true + break + } + } + } + } + if reachEnd { + break + } + } + if len(resList[0]) == 0 { + return nil + } + return resList +} + +func printCodeVals(items []*com.CodeVal) { + sort.Slice(items, func(i, j int) bool { + return items[i].Val < items[j].Val + }) + var b strings.Builder + for _, it := range items { + b.WriteString(it.Code) + b.WriteString(": ") + b.WriteString(strconv.FormatFloat(it.Val, 'f', 2, 64)) + b.WriteString(", ") + } + fmt.Printf(b.String()) + fmt.Printf("\n") +} diff --git a/arb/stat_showTwo.go b/arb/stat_showTwo.go new file mode 100644 index 0000000..9dbd9ba --- /dev/null +++ b/arb/stat_showTwo.go @@ -0,0 +1,203 @@ +package arb + +import ( + _ "embed" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/opt" + "github.com/banbox/banbot/orm" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/utils" + "github.com/banbox/banta" + "math" + "strings" +) + +func GenTwoPairRatio() *errs.Error { + //LineExamples() + //return nil + //groups := []string{"1000PEPE/USDT:USDT", "1000BONK/USDT:USDT"} + groups := []string{ + "LPT,ATH,MOODENG,RNDR,APT,TRUMP,DOT,HYPE,BAKE,SPX,BOME,TUT,PUMP,BERA,OG,EOS,MERL,TAO,NMR,FET,TIA,FHE,ZORA,LINK,POL,TRX,1000PEPE,ETHFI,PROVE,TREE", + "RED,SUI,VIRTUAL,NEIRO,GALA,TON,PAXG,XNY,CRV,NAORIS,XRP,NEAR,AVAX,ETC,XLM,MATIC,AAVE,SEI,1000SHIB,DAM,SPK,MEME,OP,FIL,KAITO,FARTCOIN,EIGEN,BCH,IP,XPL", + } + //startMS := config.TimeRange.StartMS + endMS, _ := btime.ParseTimeMS("20250921") + tf := "15m" + core.Market = "linear" + tfMSecs := int64(utils.TFToSecs(tf) * 1000) + barLimit := 30000 + startMS := endMS - tfMSecs*int64(barLimit) + barsArr := make([][]*TimeFlt, 0, len(groups)) + allPairs := strings.Split(strings.Join(groups, ","), ",") + allPairs, err := config.ParsePairs(allPairs...) + if err != nil { + return err + } + exsMap := make(map[int32]*orm.ExSymbol) + for _, pair := range allPairs { + exs, err := orm.GetExSymbolCur(pair) + if err != nil { + return err + } + exsMap[exs.ID] = exs + } + err = orm.BulkDownOHLCV(exg.Default, exsMap, tf, startMS, endMS, 0, nil) + if err != nil { + return err + } + for _, group := range groups { + pairs := strings.Split(group, ",") + prices, err := GetGroupPrices(pairs, tf, startMS, endMS) + if err != nil { + return err + } + barsArr = append(barsArr, prices) + } + rateArr := make([]*TimeFlt, len(barsArr[0])) + for i, va := range barsArr[0] { + rateArr[i] = &TimeFlt{ + Stamp: va.Stamp, + Val: va.Val / barsArr[1][i].Val, + } + } + path := "D:/ratios.html" + title := "price ratio" + return dumpRateChart(rateArr, tf, path, title) +} + +// GetGroupPrices 计算分组内多个品种的平均价格 +func GetGroupPrices(pairs []string, tf string, startMS, endMS int64) ([]*TimeFlt, *errs.Error) { + var groupBar [][]*banexg.Kline + pairs, err := config.ParsePairs(pairs...) + if err != nil { + return nil, err + } + tfMSecs := int64(utils.TFToSecs(tf) * 1000) + barLimit := (endMS - startMS) / tfMSecs + for _, pair := range pairs { + exs, err := orm.GetExSymbolCur(pair) + if err != nil { + return nil, err + } + _, bars, err := orm.GetOHLCV(exs, tf, startMS, endMS, 0, false) + if err != nil { + return nil, err + } + if float64(len(bars)) > float64(barLimit)*0.8 { + groupBar = append(groupBar, bars) + } + } + // 对分组内品种计算价格归一化均值 + groupBar = getAlignBars(groupBar...) + if len(groupBar) == 0 { + return nil, errs.NewMsg(errs.CodeParamInvalid, "align bars fail for %v", pairs) + } + for _, gp := range groupBar { + base := gp[0].Close + gp[0].Close = 1 + for i := 1; i < len(gp); i++ { + gp[i].Close /= base + } + } + barNum := len(groupBar[0]) + bars := make([]*TimeFlt, 0, barNum) + for j := 0; j < barNum; j++ { + var sumVal float64 + for _, gp := range groupBar { + sumVal += gp[j].Close + } + bars = append(bars, &TimeFlt{ + Stamp: groupBar[0][j].Time, + Val: sumVal / float64(len(groupBar)), + }) + } + return bars, nil +} + +type TimeFlt struct { + Stamp int64 + Val float64 +} + +func dumpRateChart(rates []*TimeFlt, tf string, path, title string) *errs.Error { + if len(rates) == 0 { + return nil + } + var labels []string + var arrRaw []float64 + var arrFast []float64 + var arrSlow []float64 + var arrMdMa []float64 + var arrBigMa []float64 + var arrBigUp []float64 + var arrBigDn []float64 + var arrHH, arrLL []float64 + + bollWid := 0.0043 + showNum := 1440 + stepFlt := float64(len(rates)) / float64(showNum) + stepSize := int(math.Round(stepFlt)) + tfMSecs := int64(utils.TFToSecs(tf) * 1000) + + lenFast := int(stepFlt * 5.77) + lenSlow := int(stepFlt * 31.25) + lenMid := int(stepFlt * 96.5) + lenBig := int(stepFlt * 320.2) + lenStd := int(stepFlt * 96.3) + lenHL := int(stepFlt * 14.42) + + var idx = 0 + env, _ := banta.NewBarEnv(core.ExgName, core.Market, "", tf) + env.TimeStart = rates[idx].Stamp + env.TimeStop = rates[idx].Stamp + tfMSecs + rate := env.NewSeries(nil) + rowId := 0 + dateFmt := "01-02 15:04" + for idx < len(rates) { + item := rates[idx] + env.TimeStart += tfMSecs + env.TimeStop += tfMSecs + env.BarNum += 1 + rowId += 1 + ratio := item.Val + rate.Append(ratio) + maFast := banta.EMA(rate, lenFast) + maSlow := banta.EMA(rate, lenSlow) //66 + maMid := banta.EMA(rate, lenMid).Get(0) + maBig := banta.EMA(rate, lenBig) + dev := banta.StdDev(rate, lenStd) + stdDev := banta.SMA(dev, lenStd).Get(0) * 1.7 + bollWidAvg := bollWid*maMid*0.5 + stdDev*0.5 + hh := banta.Highest(rate, lenHL).Get(0) + ll := banta.Lowest(rate, lenHL).Get(0) + bigVal := maBig.Get(0) + if rowId%stepSize == 0 && !math.IsNaN(maMid) { + labels = append(labels, btime.ToDateStr(item.Stamp, dateFmt)) + arrRaw = append(arrRaw, ratio) + arrFast = append(arrFast, maFast.Get(0)) + arrSlow = append(arrSlow, maSlow.Get(0)) + arrMdMa = append(arrMdMa, maMid) + arrBigMa = append(arrBigMa, bigVal) + arrBigUp = append(arrBigUp, maMid+bollWidAvg) + arrBigDn = append(arrBigDn, maMid-bollWidAvg) + arrHH = append(arrHH, hh) + arrLL = append(arrLL, ll) + } + idx += 1 + } + return opt.DumpChart(path, title, labels, 5, nil, []*opt.ChartDs{ + {Label: "raw", Data: arrRaw}, + {Label: "ma5", Data: arrFast, Hidden: true}, + {Label: "ma10", Data: arrSlow, Hidden: true}, + {Label: "mid", Data: arrMdMa}, + {Label: "big", Data: arrBigMa, Hidden: true}, + {Label: "upp", Data: arrBigUp, Hidden: false}, + {Label: "dnn", Data: arrBigDn, Hidden: false}, + //{Label: "hh", Data: arrHH, Hidden: true}, + //{Label: "ll", Data: arrLL, Hidden: true}, + }) +} diff --git a/arb/stat_spike.go b/arb/stat_spike.go new file mode 100644 index 0000000..a8dd650 --- /dev/null +++ b/arb/stat_spike.go @@ -0,0 +1,31 @@ +package arb + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banexg/errs" +) + +/* +捕捉价格比率的瞬时剧烈波动 +*/ + +func FindTupleSpikes() *errs.Error { + tf := "1m" + endMS := config.TimeRange.EndMS + items, err := findSpikeTuples(endMS, tf, 2000, 5, 5) + if err != nil { + return err + } + fmt.Println("tuple,score,slope,pftRate,rawXBig") + for _, t := range items { + codeA, _, _, _ := core.SplitSymbol(t.A.Symbol) + codeB, _, _, _ := core.SplitSymbol(t.B.Symbol) + if codeA > codeB { + codeA, codeB = codeB, codeA + } + fmt.Printf("%s/%s,%.2f,%.4f,%.3f,%v\n", codeA, codeB, t.Score, t.BigSlope, t.PftRate, t.RawXBig) + } + return nil +} diff --git a/arb/stat_test.go b/arb/stat_test.go new file mode 100644 index 0000000..16384d6 --- /dev/null +++ b/arb/stat_test.go @@ -0,0 +1,391 @@ +package arb + +import ( + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/data" + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/goods" + "github.com/banbox/banbot/orm" + utils2 "github.com/banbox/banbot/utils" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + "github.com/banbox/banexg/utils" + "github.com/sasha-s/go-deadlock" + "go.uber.org/zap" + "math/rand" + "path/filepath" + "sort" + "strings" + "testing" + "time" +) + +func TestPairPick(t *testing.T) { + all := "ETH,BTC,SOL,DOGE,XRP,WLFI,MYX,ENA,BNB,1000PEPE,LINK,SUI,AVAX,ADA,PUMP,WLD,SOMI,HYPE,BIO,FARTCOIN,NMR,M,PENGU,PYTH,AAVE,LTC,HIFI,TA,WIF,ARB,RNDR,TRUMP,1000BONK,BCH,DOT,UNI,IP,BAKE,MATIC,FIL,EOS,TRX,NEAR,ONDO,XPL,1000SHIB,DOLO,PROMPT,CRV,OP,MEME,ETHFI,HBAR,XLM,ETC,TIA,LDO,SEI,TREE,EIGEN,TAO,XNY,SPK,APT,DAM,W,NAORIS,INJ,SAPIEN,JELLYJELLY,MOODENG,VIRTUAL,RED,TST,QTUM,MERL,TON,F,TUT,PNUT,LPT,SOLV,PENDLE,CFX,POL,KAITO,BERA,ZORA,GALA,FHE,PAXG,ONT,JUP,OG,FET,PUMPBTC,SKL,GAL,ATOM,NEIRO,BOME,MAV,PROVE,ATH,SPX" + pickA, pickB := 30, 30 + coins := strings.Split(all, ",") + rand.Shuffle(len(coins), func(i, j int) { coins[i], coins[j] = coins[j], coins[i] }) + groupA := coins[:pickA] + groupB := coins[pickA : pickA+pickB] + fmt.Println(strings.Join(groupA, ",")) + fmt.Println(strings.Join(groupB, ",")) +} + +// 从高频逐笔数据中提取每隔5秒的价格 +func TestExtractPrices(t *testing.T) { + core.SetRunMode(core.RunModeBackTest) + err := biz.SetupComsExg(&config.CmdArgs{}) + if err != nil { + panic(err) + } + loader, err := data.NewWsDataLoader() + if err != nil { + panic(err) + } + dataDir := config.GetDataDir() + outPath := filepath.Join(dataDir, "dump", "group1.gob") + group1 := "LPT,ATH,MOODENG,RNDR,APT,TRUMP,DOT,HYPE,BAKE,SPX,BOME,TUT,PUMP,BERA,OG,EOS,MERL,TAO,NMR,FET,TIA,FHE,ZORA,LINK,POL,TRX,1000PEPE,ETHFI,PROVE,TREE" + //group2 := "RED,SUI,VIRTUAL,NEIRO,GALA,TON,PAXG,XNY,CRV,NAORIS,XRP,NEAR,AVAX,ETC,XLM,MATIC,AAVE,SEI,1000SHIB,DAM,SPK,MEME,OP,FIL,KAITO,FARTCOIN,EIGEN,BCH,IP,XPL" + pairs := strings.Split(group1, ",") + pairs, err = config.ParsePairs(pairs...) + if err != nil { + panic(err) + } + result := make(map[string][]float64) + if utils2.Exists(outPath) { + err = utils2.DecodeGobFile(outPath, &result) + if err != nil { + panic(err) + } + cache := make(map[string]int) + num := 0 + for k, v := range result { + cache[k] = len(v) + if len(v) > 0 { + num += 1 + } + } + log.Info("load cache", zap.Int("num", num), zap.Any("keys", cache)) + } + lock := deadlock.Mutex{} + year, mon, day := time.Now().Add(-time.Hour * 24).Date() + endTime := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC) + year, mon, day = endTime.AddDate(0, 0, -15).Date() + startTime := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC) + gapMS := int64(5000) // 采样间隔ms + startMS := startTime.UnixMilli() + endMS := endTime.UnixMilli() + size := int((endMS - startMS) / gapMS) + pBar := utils2.NewPrgBar(len(pairs)*size, "run") + defer pBar.Close() + err = utils2.ParallelRun(pairs, 4, func(i int, pair string) *errs.Error { + lock.Lock() + arr, ok := result[pair] + lock.Unlock() + if ok && len(arr) > 0 { + pBar.Add(size) + return nil + } + arr = make([]float64, size+10) + lastIdx := 0 + saveNum := 0 + curTime := startTime + success := true + for curTime.Before(endTime) { + trades, err := loader.LoadTrades(&data.WsSymbol{ + ExgId: "binance", + Market: banexg.MarketLinear, + WsType: core.WsSubTrade, + Symbol: pair, + Date: curTime.Format(core.DateFmt), + Hour: curTime.Hour(), + }) + if err != nil { + log.Error("down trades fail", zap.Error(err)) + success = false + break + } + curTime = curTime.Add(time.Hour) + for _, td := range trades { + curIdx := int((td.Timestamp - startMS) / gapMS) + if curIdx > lastIdx { + prevVal := arr[lastIdx] + for lastIdx+1 < curIdx { + lastIdx += 1 + arr[lastIdx] = prevVal + } + lastIdx = curIdx + arr[curIdx] = td.Price + } + } + if lastIdx > saveNum { + pBar.Add(lastIdx - saveNum) + saveNum = lastIdx + } + } + if success { + lock.Lock() + result[pair] = arr + lock.Unlock() + } + return nil + }) + err = utils2.EncodeGob(outPath, result) + if err != nil { + panic(err) + } +} + +// 测试随机两组品种的平均价格比率(逐秒级别) +func TestDayBatchRates(t *testing.T) { + // 读取两组30个交易品种的每隔5s的价格序列,计算平均价比率,研究套利空间 + tf := "5s" + dataDir := config.GetDataDir() + outPath := filepath.Join(dataDir, "dump", "group1.gob") + group1 := make(map[string][]float64) + err := utils2.DecodeGobFile(outPath, &group1) + if err != nil { + panic(err) + } + outPath = filepath.Join(dataDir, "dump", "group2.gob") + group2 := make(map[string][]float64) + err = utils2.DecodeGobFile(outPath, &group2) + if err != nil { + panic(err) + } + tfSecs := utils.TFToSecs(tf) + daySize := utils.TFToSecs("1d") / tfSecs + dataLen := 0 + for _, arr := range group1 { + dataLen = len(arr) + break + } + tfMSecs := int64(tfSecs * 1000) + year, mon, day := time.Now().Add(-time.Hour * 24).Date() + endMS := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC).UnixMilli() + startMS := endMS - int64(dataLen)*tfMSecs + dumpDir := filepath.Join(config.GetDataDir(), "dump") + for offset := 0; offset+daySize < dataLen; offset += daySize { + offsetMS := startMS + int64(offset)*tfMSecs + batchA := normInRange(group1, offset, offset+daySize, offsetMS, tfMSecs) + batchB := normInRange(group2, offset, offset+daySize, offsetMS, tfMSecs) + rateArr := make([]*TimeFlt, len(batchA)) + for i, va := range batchA { + rateArr[i] = &TimeFlt{ + Stamp: va.Stamp, + Val: va.Val / batchB[i].Val, + } + } + dateStr := btime.ToDateStr(offsetMS, "20060102") + dumpPath := filepath.Join(dumpDir, dateStr+".html") + log.Info("dump rate chart", zap.String("title", dateStr)) + err = dumpRateChart(rateArr, tf, dumpPath, dateStr) + if err != nil { + panic(err) + } + } +} + +func normInRange(dataMap map[string][]float64, start, end int, offsetMS, stepMS int64) []*TimeFlt { + batchA := make(map[string][]float64) + minLen := -1 + // 对每个品种的价格序列进行归一化 + for key, arr := range dataMap { + vals := arr[start:end] + res := make([]float64, len(vals)) + first := float64(0) + for i, v := range vals { + if v > 0 { + if first > 0 { + res[i] = v / first + } else { + res[i] = 1 + first = v + } + } + } + batchA[key] = res + if minLen < 0 { + minLen = len(res) + } else { + minLen = min(minLen, len(res)) + } + } + // 直接对所有品种价格序列取平均值 + res := make([]*TimeFlt, minLen) + for i := 0; i < minLen; i++ { + sumVal := float64(0) + validNum := 0 + for _, arr := range batchA { + val := arr[i] + if val > 0 { + sumVal += val + validNum += 1 + } + } + item := &TimeFlt{ + Stamp: offsetMS + int64(i)*stepMS, + } + if validNum > 0 { + item.Val = sumVal / float64(validNum) + } + res[i] = item + } + return res +} + +/* +每7日取成交量最高top60 +随机分为两组计算平均价格比率 + +获取7日成交量倒序前30%的品种列表 +每天计算: + + 计算最近14天1h的相关性矩阵 + 按相关性从高到低取top60 + 奇数位和偶数位分为两组 + +对比了按相关性从高到低取top60和随机分配top60,差别不大,简单起见,使用随机方法 +*/ +func TestDailyGroupArb(t *testing.T) { + core.SetRunMode(core.RunModeBackTest) + err := biz.SetupComsExg(&config.CmdArgs{}) + if err != nil { + panic(err) + } + groupSize := 30 + dayTf := "1d" + dayMSecs := int64(utils.TFToSecs("1d") * 1000) + year, mon, day := time.Now().Add(-time.Hour * 24).Date() + endMS := time.Date(year, mon, day, 0, 0, 0, 0, time.UTC).UnixMilli() + startMS := endMS - dayMSecs*28 + minListMS := endMS - dayMSecs*180 + curMS := startMS + exsMap := orm.GetExSymbols(core.ExgName, core.Market) + pairs := make([]string, 0, len(exsMap)) + for _, exs := range exsMap { + if exs.ListMs > minListMS { + // 忽略上市不久的品种 + continue + } + pairs = append(pairs, exs.Symbol) + } + err = orm.BulkDownOHLCV(exg.Default, exsMap, "1h", startMS, endMS, 0, nil) + if err != nil { + panic(err) + } + var pre30Pairs []string + var pre30Ms int64 + dumpDir := filepath.Join(config.GetDataDir(), "dump") + for curMS < endMS { + if len(pre30Pairs) == 0 || (curMS-pre30Ms)/dayMSecs >= 7 { + // 获取14日成交量倒序前30%的品种列表 + pre30Pairs = nil + symbolVols, err := goods.GetSymbolVols(pairs, dayTf, 14, curMS, false) + if err != nil { + panic(err) + } + sort.Slice(symbolVols, func(i, j int) bool { + return symbolVols[i].Vol > symbolVols[j].Vol + }) + cutNum := int(float64(len(symbolVols)) * 0.23) + for i := 0; i < cutNum; i++ { + pre30Pairs = append(pre30Pairs, symbolVols[i].Symbol) + } + pre30Ms = curMS + dateStr := btime.ToDateStr(curMS, core.DateFmt) + log.Info("cut pre 30% pairs by vols", zap.Int("num", len(pre30Pairs)), zap.String("date", dateStr)) + } + // 按成交量取前面的,然后随机挑选。 + coins := pre30Pairs[:groupSize*2] + rand.Shuffle(len(coins), func(i, j int) { coins[i], coins[j] = coins[j], coins[i] }) + var pairsA, pairsB []string + downMap := make(map[int32]*orm.ExSymbol) + for i := 0; i < groupSize*2; i++ { + pair := coins[i] + if i%2 == 0 { + pairsA = append(pairsA, pair) + } else { + pairsB = append(pairsB, pair) + } + exs, _ := orm.GetExSymbolCur(pair) + downMap[exs.ID] = exs + } + // 对A和B两组计算1m加权价格 + curEndMS := curMS + dayMSecs + err = orm.BulkDownOHLCV(exg.Default, downMap, "1m", curMS, curEndMS, 0, nil) + if err != nil { + panic(err) + } + pricesA, err := GetGroupPrices(pairsA, "1m", curMS, curEndMS) + if err != nil { + panic(err) + } + pricesB, err := GetGroupPrices(pairsB, "1m", curMS, curEndMS) + if err != nil { + panic(err) + } + rateArr := make([]*TimeFlt, len(pricesA)) + for i, va := range pricesA { + rateArr[i] = &TimeFlt{ + Stamp: va.Stamp, + Val: va.Val / pricesB[i].Val, + } + } + dateStr := btime.ToDateStr(curMS, "20060102") + dumpPath := filepath.Join(dumpDir, dateStr+"_r2.html") + log.Info("dump rate chart", zap.String("title", dateStr)) + err = dumpRateChart(rateArr, "1m", dumpPath, dateStr) + if err != nil { + panic(err) + } + curMS += dayMSecs + } +} + +func getPairCorrs(curMS int64, pairs []string) []string { + // 计算最近14天1h的相关性矩阵 + dayMSecs := int64(utils.TFToSecs("1d") * 1000) + hourMSecs := int64(utils.TFToSecs("1h") * 1000) + pre14MS := curMS - dayMSecs*14 + names := make([]string, 0, len(pairs)) + dataArr := make([][]float64, 0, len(pairs)) + klineNum := int((curMS - pre14MS) / hourMSecs) + err := orm.FastBulkOHLCV(exg.Default, pairs, "1h", pre14MS, curMS, 0, func(s string, s2 string, klines []*banexg.Kline, infos []*orm.AdjInfo) { + if len(klines) == klineNum { + prices := make([]float64, 0, len(klines)) + for _, b := range klines { + prices = append(prices, b.Close) + } + names = append(names, s) + dataArr = append(dataArr, prices[:klineNum]) + } + }) + if err != nil { + panic(err) + } + _, avgs, err_ := utils2.CalcCorrMat(klineNum, dataArr, true) + if err_ != nil { + panic(err_) + } + // 基于平均相似度降序 + arr := make([]*goods.IdVal, 0, len(avgs)) + lefts := make(map[int]bool) + for i, avg := range avgs { + arr = append(arr, &goods.IdVal{Id: i, Val: avg}) + lefts[i] = true + } + sort.Slice(arr, func(i, j int) bool { + return arr[i].Val < arr[j].Val + }) + var resPairs []string + for _, v := range arr { + resPairs = append(resPairs, names[v.Id]) + } + return resPairs +} diff --git a/com/common.go b/com/common.go new file mode 100644 index 0000000..1180a56 --- /dev/null +++ b/com/common.go @@ -0,0 +1,91 @@ +package com + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/utils" + ta "github.com/banbox/banta" + "os" + "path/filepath" + "slices" +) + +const ( + MaSMA = "sma" + MaEMA = "ema" + MaRMA = "rma" + MaWMA = "wma" + MaVWMA = "vwma" + MaHMA = "hma" + MaKAMA = "kama" + MaALMA = "alma" +) + +var MATypes = []string{MaSMA, MaEMA, MaRMA, MaWMA, MaVWMA, MaHMA, MaKAMA, MaALMA} + +var maMap = make(map[string]bool) + +func init() { + for _, name := range MATypes { + maMap[name] = true + } +} + +func DefMaType(p *config.RunPolicyConfig, key, defVal string, allows ...string) string { + if len(allows) == 0 { + allows = MATypes + } else { + for _, k := range allows { + if _, ok := maMap[k]; !ok { + panic(fmt.Sprintf("unsupport maType: %s", k)) + } + } + } + if _, ok := maMap[defVal]; !ok { + panic(fmt.Sprintf("unsupport maType: %s", defVal)) + } + defValInt := slices.Index(allows, defVal) + if defValInt < 0 { + defValInt = 0 + allows = append([]string{defVal}, allows...) + } + pVal := p.DefInt(key, defValInt, core.PUniform(0, float64(len(allows)-1))) + return allows[pVal] +} + +func GetMA(price, vol *ta.Series, maType string, maLen int) *ta.Series { + switch maType { + case MaSMA: + return ta.SMA(price, maLen) + case MaEMA: + return ta.EMA(price, maLen) + case MaRMA: + return ta.RMA(price, maLen) + case MaWMA: + return ta.WMA(price, maLen) + case MaVWMA: + return ta.VWMA(price, vol, maLen) + case MaHMA: + return ta.HMA(price, maLen) + case MaKAMA: + return ta.KAMA(price, maLen) + case MaALMA: + return ta.ALMA(price, maLen, 6, 0.85) + default: + panic(fmt.Errorf("unknown maType: " + maType)) + } +} + +func GetTmpPath(name string) (string, error) { + path, err := os.Getwd() + if err != nil { + return "", err + } + tmpDir := filepath.Join(path, "tmp") + err = utils.EnsureDir(tmpDir, 0644) + if err != nil { + return "", err + } + return filepath.Join(tmpDir, name), nil +} diff --git a/com/cross.go b/com/cross.go new file mode 100644 index 0000000..401c236 --- /dev/null +++ b/com/cross.go @@ -0,0 +1,38 @@ +package com + +import "math" + +func NewCrossLog() *CrossLog { + return &CrossLog{} +} + +type CrossLog struct { + PrevVal float64 + Sign int // 最近一次交叉符号:1上穿,-1下穿 + XAt int // 最近一次交叉序号 + Num int // 当前序号 +} + +func (l *CrossLog) Update(sub float64) { + l.Num += 1 + if sub == 0 { + return + } + if math.IsNaN(l.PrevVal) || l.PrevVal == 0 { + l.PrevVal = sub + } else { + if factor := l.PrevVal * sub; factor < 0 { + l.PrevVal = sub + l.XAt = l.Num + if sub > 0 { + l.Sign = 1 + } else { + l.Sign = -1 + } + } + } +} + +func (l *CrossLog) Get() int { + return l.Sign * (l.Num - l.XAt + 1) +} diff --git a/com/eff_trail_stop.go b/com/eff_trail_stop.go new file mode 100644 index 0000000..90ec398 --- /dev/null +++ b/com/eff_trail_stop.go @@ -0,0 +1,44 @@ +package com + +import ( + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/utils" + "math" +) + +type EFTRS struct { + SLLong float64 + SLShort float64 + + EFLen int +} + +func NewEFTRS(efLen int) *EFTRS { + return &EFTRS{EFLen: efLen} +} + +func (t *EFTRS) Calc(s *strat.StratJob, atr float64) (float64, float64) { + e := s.Env + c, c1 := e.Close.Get(0), e.Close.Get(1) + totSum := utils.ArrSum(e.Close.Sub(e.Close.Back(1)).Abs().Range(0, t.EFLen)) + effRate := math.Abs(c-e.Close.Get(t.EFLen)) / totSum + slLong := e.Low.Get(0) - atr + slShort := e.High.Get(0) + atr + if len(s.LongOrders) > 0 { + if c > c1 { + slLong = t.SLLong + effRate*(c-c1) + } else { + slLong = t.SLLong + } + } + if len(s.ShortOrders) > 0 { + if c < c1 { + slShort = t.SLShort - effRate*(c1-c) + } else { + slShort = t.SLShort + } + } + t.SLLong = slLong + t.SLShort = slShort + return slLong, slShort +} diff --git a/com/ema.go b/com/ema.go new file mode 100644 index 0000000..655e46a --- /dev/null +++ b/com/ema.go @@ -0,0 +1,28 @@ +package com + +import "math" + +type Ema struct { + Alpha float64 + Val float64 +} + +func NewEMA(alpha float64) *Ema { + return &Ema{Alpha: alpha} +} + +func (e *Ema) Update(val float64) float64 { + if math.IsNaN(val) || math.IsInf(val, 0) { + return math.NaN() + } + if e.Val == 0 { + e.Val = val + } else { + e.Val = e.Val*(1-e.Alpha) + val*e.Alpha + } + return e.Val +} + +func (e *Ema) Reset() { + e.Val = 0 +} diff --git a/com/exm.go b/com/exm.go new file mode 100644 index 0000000..dc1ae71 --- /dev/null +++ b/com/exm.go @@ -0,0 +1,135 @@ +package com + +import ( + "math" + "slices" +) + +type ExmItem struct { + BarNum int + TimeMS int64 + Value float64 + Valid bool +} + +type ExmTrace struct { + Hist []*ExmItem + Expect int // 1寻找极大值,-1寻找极小值 + PrevVals []float64 // 前一个值 + PrevDist float64 // 前两个极值的距离 + distList []float64 // 前面极值距离,保留最近3个 +} + +/* +Update +val: 当前指标值 +pNum: 前一个索引 +ptime:前一个时间戳 +*/ +func (m *ExmTrace) Update(val float64, pNum int, ptime int64) bool { + if math.IsNaN(val) { + return false + } + if len(m.PrevVals) < 2 { + // 至少积累2个 + if len(m.PrevVals) > 0 && m.PrevVals[len(m.PrevVals)-1] == val { + // 和前一个值相同,跳过 + return false + } + m.PrevVals = append(m.PrevVals, val) + return false + } + // 前1个,前2个 + p1, p2 := m.PrevVals[1], m.PrevVals[0] + if p1 == val { + // 当前值无效 + return false + } + // 更新 + m.PrevVals = append(m.PrevVals, val) + m.PrevVals = m.PrevVals[len(m.PrevVals)-2:] + pSub := p1 - p2 + sub := p1 - val + var last *ExmItem + if len(m.Hist) > 0 { + last = m.Hist[len(m.Hist)-1] + } + getMinDist := func() float64 { + if last == nil { + return math.Abs(p1) * 0.3 + } + if m.PrevDist != 0 { + return m.PrevDist * 0.3 + } + return max(math.Abs(last.Value), math.Abs(p1)) * 0.3 + } + if pSub*sub <= 0 { + // 非极大值或极小值 + if last != nil && !last.Valid && (val-last.Value)*float64(m.Expect) > getMinDist() { + // 检查最后一个是否有效 + last.Valid = true + if len(m.Hist) > 1 { + dist := math.Abs(m.Hist[len(m.Hist)-2].Value - m.Hist[len(m.Hist)-1].Value) + m.distList = append(m.distList, dist) + if len(m.distList) > 3 { + m.distList = m.distList[len(m.distList)-3:] + } + m.PrevDist = slices.Max(m.distList) + } + return true + } + return false + } + valid := 0 + if sub > 0 { + if m.Expect == 0 || m.Expect == 1 { + // 新增极大值 + if last == nil || p1-last.Value >= getMinDist() { + valid = -1 + } + } else if p1 > last.Value { + // 更新极大值 + valid = -1 + } + } else if sub < 0 { + if m.Expect == 0 || m.Expect == -1 { + // 新增极小值 + if last == nil || last.Value-p1 >= getMinDist() { + valid = 1 + } + } else if p1 < last.Value { + // 更新极小值 + valid = 1 + } + } + if valid != 0 { + exm := &ExmItem{ + TimeMS: ptime, + BarNum: pNum, + Value: p1, + } + if m.Expect != valid { + m.Hist = append(m.Hist, exm) + //log.Info("add exm", zap.String("date", btime.ToDateStr(ptime, "")), + // zap.Float64("val", p1)) + } else { + m.Hist[len(m.Hist)-1] = exm + //log.Info("update exm", zap.String("date", btime.ToDateStr(ptime, "")), + // zap.Float64("val", p1)) + } + if len(m.Hist) > 2 { + dist := math.Abs(m.Hist[len(m.Hist)-3].Value - m.Hist[len(m.Hist)-2].Value) + if len(m.distList) == 0 || dist != m.distList[len(m.distList)-1] { + m.distList = append(m.distList, dist) + if len(m.distList) > 3 { + m.distList = m.distList[len(m.distList)-3:] + } + m.PrevDist = slices.Max(m.distList) + } + } + m.Expect = valid + m.PrevVals = nil + return true + } + return false +} diff --git a/com/inds.go b/com/inds.go new file mode 100644 index 0000000..bacd64c --- /dev/null +++ b/com/inds.go @@ -0,0 +1,41 @@ +package com + +import "github.com/banbox/banta" + +/* +IchimokuCloud + +sml: 9, mid: 26, big: 52, disp: 26 +return [smlMa, midMa, spanA, spanB] +*/ +func IchimokuCloud(e *banta.BarEnv, sml, mid, big, disp int) *banta.Series { + res := e.Close.To("_ichimokuCld", big*100000+mid*1000+disp*10+sml) + if res.Cached() { + return res + } + // earlier + h := e.High.Back(disp - 1) + l := e.Low.Back(disp - 1) + hhSml := banta.Highest(h, sml).Get(0) + llSml := banta.Lowest(l, sml).Get(0) + hhMid := banta.Highest(h, mid).Get(0) + llMid := banta.Lowest(l, mid).Get(0) + hhBig := banta.Highest(h, big).Get(0) + llBig := banta.Lowest(l, big).Get(0) + + smlMa := (hhSml + llSml) / 2 + midMa := (hhMid + llMid) / 2 + bigMa := (hhBig + llBig) / 2 + spanA := (smlMa + midMa) / 2 + + // Current + h, l = e.High, e.Low + hhSml = banta.Highest(h, sml).Get(0) + llSml = banta.Lowest(l, sml).Get(0) + hhMid = banta.Highest(h, mid).Get(0) + llMid = banta.Lowest(l, mid).Get(0) + + smlMa2 := (hhSml + llSml) / 2 + midMa2 := (hhMid + llMid) / 2 + return res.Append([]float64{smlMa2, midMa2, spanA, bigMa}) +} diff --git a/com/slop.go b/com/slop.go new file mode 100644 index 0000000..cb5ba86 --- /dev/null +++ b/com/slop.go @@ -0,0 +1,65 @@ +package com + +import ( + "math" + "slices" +) + +type Slop struct { + lastCalc int + sampleNum int + Dirt int // 大周期斜率方向:1向上,0不明确,-1向下 + rate float64 // 斜率阈值,推荐0.25 + Data []float64 + Top float64 + Btm float64 +} + +func NewSlop(sampleNum int, rate float64) *Slop { + return &Slop{ + Dirt: 0, + sampleNum: sampleNum, + rate: rate, + Top: math.NaN(), + Btm: math.NaN(), + } +} + +func (s *Slop) Update(barNum int, slop float64) { + s.Data = append(s.Data, math.Abs(slop)) + if len(s.Data) < s.sampleNum { + return + } + if !math.IsNaN(s.Top) { + s.Dirt = 0 + if slop >= s.Top { + s.Dirt = 1 + } else if slop <= s.Btm { + s.Dirt = -1 + } + } + if barNum-s.lastCalc < s.sampleNum/2 { + return + } + if len(s.Data) > s.sampleNum { + s.Data = s.Data[len(s.Data)-s.sampleNum:] + } + thres := CutScore(s.Data, s.rate) + s.Top = thres + s.Btm = -thres + s.lastCalc = barNum + s.Dirt = 0 + if slop >= s.Top { + s.Dirt = 1 + } else if slop <= s.Btm { + s.Dirt = -1 + } +} + +func CutScore(vals []float64, rate float64) float64 { + arr := make([]float64, len(vals)) + copy(arr, vals) + slices.Sort(arr) + idx := int(float64(len(vals)) * rate) + return arr[idx] +} diff --git a/com/trailing_stop.go b/com/trailing_stop.go new file mode 100644 index 0000000..c8884c3 --- /dev/null +++ b/com/trailing_stop.go @@ -0,0 +1,85 @@ +package com + +import ( + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +type TRS struct { + MaxLow float64 + MinHigh float64 + SLLong float64 // 止损的tr倍率,有持仓时,逐渐减小 + SLShort float64 // 止损的tr倍率,有持仓时,逐渐减小 + InitSLTR float64 + minSLTR float64 + stepSLTR float64 +} + +func NewTRS(initSLTR, minRate, costStep float64) *TRS { + var minSLTR = initSLTR * minRate + var stepSLTR = (initSLTR - minSLTR) / costStep + return &TRS{ + InitSLTR: initSLTR, + minSLTR: minSLTR, + stepSLTR: stepSLTR, + } +} + +func (m *TRS) TrialLong(price float64) { + m.SLLong = m.InitSLTR + m.MaxLow = price +} + +func (m *TRS) TrialShort(price float64) { + m.SLShort = m.InitSLTR + m.MinHigh = price +} + +func (m *TRS) setTRS(e *ta.BarEnv, longNum, shortNum int) { + h, l := e.High.Get(0), e.Low.Get(0) + if longNum == 0 { + if m.SLLong != m.InitSLTR { + m.MaxLow = 0 + } + } else { + m.MaxLow = max(m.MaxLow, l) + if m.SLLong > m.minSLTR+0.01 { + m.SLLong -= m.stepSLTR + } + } + if shortNum == 0 { + if m.SLShort != m.InitSLTR { + m.MinHigh = 0 + } + } else { + m.MinHigh = min(m.MinHigh, h) + if m.SLShort > m.minSLTR+0.01 { + m.SLShort -= m.stepSLTR + } + } +} + +func (m *TRS) Calc(s *strat.StratJob, atr float64) (float64, float64) { + m.setTRS(s.Env, len(s.LongOrders), len(s.ShortOrders)) + var stopLong, stopShort float64 + if m.MaxLow > 0 { + stopLong = m.MaxLow - atr*m.SLLong + } + if m.MinHigh > 0 { + stopShort = m.MinHigh + atr*m.SLShort + } + return stopLong, stopShort +} + +func (m *TRS) CalcSL(s *strat.StratJob, atr *ta.Series, lenAtr, lenAtrB int, pow, rate float64) (float64, float64) { + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), lenAtrB).Get(0) + atrRate := math.Pow(atr.Get(0)/atrBase, pow) + return m.Calc(s, atrBase*atrRate*rate) +} + +func (m *TRS) CalcSL2(s *strat.StratJob, lenAtr, lenAtrB int, pow, rate float64) (float64, float64) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + return m.CalcSL(s, atr, lenAtr, lenAtrB, pow, rate) +} diff --git a/com/types.go b/com/types.go new file mode 100644 index 0000000..e54d2f9 --- /dev/null +++ b/com/types.go @@ -0,0 +1,6 @@ +package com + +type CodeVal struct { + Code string + Val float64 +} diff --git a/com/utils.go b/com/utils.go new file mode 100644 index 0000000..3d2d025 --- /dev/null +++ b/com/utils.go @@ -0,0 +1,51 @@ +package com + +import ( + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "go.uber.org/zap" +) + +func FastOpen(s *strat.StratJob, dirt, slLong, slShort, limit float64) { + openTag := "long" + stopLoss := slLong + if dirt < 0 { + openTag = "short" + stopLoss = slShort + } + if !s.CanOpen(dirt < 0) { + return + } + err := s.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: dirt < 0, + Limit: limit, + StopLoss: stopLoss, + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } +} + +func MakePairInfos(warmNum int) func(s *strat.StratJob) []*strat.PairSub { + return func(s *strat.StratJob) []*strat.PairSub { + var bigTF string + switch s.TimeFrame { + case "1d": + bigTF = "1w" + case "4h": + bigTF = "1d" + case "1h": + bigTF = "8h" + case "15m": + bigTF = "2h" + case "5m": + bigTF = "1h" + case "1m": + bigTF = "15m" + } + return []*strat.PairSub{ + {Pair: "_cur_", TimeFrame: bigTF, WarmupNum: warmNum}, + } + } +} diff --git a/demo/bar_agg.go b/demo/bar_agg.go new file mode 100644 index 0000000..2a7dd55 --- /dev/null +++ b/demo/bar_agg.go @@ -0,0 +1,79 @@ +package demo + +import ( + "fmt" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "path/filepath" + "strings" +) + +type AggMore struct { + bars []*banexg.Kline + barsBig []*banexg.Kline +} + +func getBar(e *ta.BarEnv) *banexg.Kline { + return &banexg.Kline{ + Time: e.TimeStart, + Open: e.Open.Get(0), + High: e.High.Get(0), + Low: e.Low.Get(0), + Close: e.Close.Get(0), + Volume: e.Volume.Get(0), + } +} + +func dumpKline(bars []*banexg.Kline, name string) *errs.Error { + if len(bars) == 0 { + return nil + } + outDir := config.GetDataDir() + rows := utils.KlineToStr(bars, btime.UTCLocale) + path := filepath.Join(outDir, "tmp", fmt.Sprintf("%s.csv", name)) + return utils.WriteCsvFile(path, rows, false) +} + +func BarAgg(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 1, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 1}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &AggMore{} + }, + OnBar: func(s *strat.StratJob) { + b := getBar(s.Env) + m, _ := s.More.(*AggMore) + m.bars = append(m.bars, b) + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + b := getBar(e) + m, _ := s.More.(*AggMore) + m.barsBig = append(m.barsBig, b) + }, + OnShutDown: func(s *strat.StratJob) { + m, _ := s.More.(*AggMore) + code := strings.ReplaceAll(s.Symbol.Symbol, "/", "_") + code = strings.ReplaceAll(code, ":", "_") + err := dumpKline(m.bars, fmt.Sprintf("%s_%s", code, s.TimeFrame)) + if err != nil { + log.Error("dump klines fail", zap.Error(err)) + } + err = dumpKline(m.barsBig, fmt.Sprintf("%s_1h", code)) + if err != nil { + log.Error("dump big klines fail", zap.Error(err)) + } + }, + } +} diff --git a/demo/bar_stat.go b/demo/bar_stat.go new file mode 100644 index 0000000..150e196 --- /dev/null +++ b/demo/bar_stat.go @@ -0,0 +1,40 @@ +package demo + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "go.uber.org/zap" + "slices" +) + +type Stat struct { + rates []float64 +} + +func BarStat(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 50, + OnStartUp: func(s *strat.StratJob) { + s.More = &Stat{} + }, + OnBar: func(s *strat.StratJob) { + m, _ := s.More.(*Stat) + // rate := genCurFutProbs(s.Env)[0] + m.rates = append(m.rates, 0) + }, + OnShutDown: func(s *strat.StratJob) { + m, _ := s.More.(*Stat) + slices.Sort(m.rates) + var sum float64 + var outs []string + for _, v := range m.rates { + sum += v + outs = append(outs, fmt.Sprintf("%.2f", v*100)) + } + log.Info("", zap.Float64("avg", sum/float64(len(m.rates))), + zap.Strings("arr", outs)) + }, + } +} diff --git a/demo/dca.go b/demo/dca.go new file mode 100644 index 0000000..00dc4f0 --- /dev/null +++ b/demo/dca.go @@ -0,0 +1,48 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" +) + +type DCAState struct { + LastInvestTime int64 // 上次定投时间 +} + +func DCA(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + intvDays := pol.DefInt("intvDays", 7, core.PNorm(1, 365)) + intvMSecs := int64(intvDays) * 86400000 + + return &strat.TradeStrat{ + WarmupNum: 1, // 定投策略不需要太多预热数据 + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的状态 + s.More = &DCAState{ + LastInvestTime: 0, + } + }, + + OnBar: func(s *strat.StratJob) { + state, _ := s.More.(*DCAState) + currentTime := s.Env.TimeStop + + // 检查是否到了定投时间间隔 + if state.LastInvestTime > 0 && currentTime-state.LastInvestTime < intvMSecs { + return // 还未到定投时间 + } + + // 执行定投买入 + err := s.OpenOrder(&strat.EnterReq{ + Tag: "buy", + Log: true, + }) + + if err == nil { + // 更新上次定投时间 + state.LastInvestTime = currentTime + } + }, + } +} diff --git a/demo/demo.go b/demo/demo.go new file mode 100644 index 0000000..9667c3d --- /dev/null +++ b/demo/demo.go @@ -0,0 +1,71 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func Demo(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastPeriod := pol.DefInt("fastPeriod", 5, core.PNorm(3, 20)) + maRate := pol.Def("maRate", 4.0, core.PNorm(1.5, 9.0)) + + // 计算慢速均线周期 + slowPeriod := int(float64(fastPeriod) * maRate) + + return &strat.TradeStrat{ + WarmupNum: slowPeriod + 10, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + maFast := ta.SMA(e.Close, fastPeriod) // 快速移动平均线 + maSlow := ta.SMA(e.Close, slowPeriod) // 慢速移动平均线 + maCrossState := maFast.Cross(maSlow) // 判断均线交叉状态 + + // 金叉:快线上穿慢线 + if maCrossState == 1 { + // 先平掉空头持仓 + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "golden_cross_close_short", + Dirt: core.OdDirtShort, + }) + } + + // 再开多头仓位 + if len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "golden_cross_long", + Short: false, + }) + } + } + + // 死叉:快线下穿慢线 + if maCrossState == -1 { + // 先平掉多头持仓 + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "death_cross_close_long", + Dirt: core.OdDirtLong, + }) + } + + // 再开空头仓位 + if len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "death_cross_short", + Short: true, + }) + } + } + }, + } +} + +func init() { + strat.StratMake["demo"] = Demo +} diff --git a/demo/freqtrade/Strategy004.go b/demo/freqtrade/Strategy004.go new file mode 100644 index 0000000..c8f673f --- /dev/null +++ b/demo/freqtrade/Strategy004.go @@ -0,0 +1,43 @@ +package freqtrade + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +Strategy 004 +author@: Gerald Lonlas +github@: https://github.com/freqtrade/freqtrade-strategies/blob/main/user_data/strategies/Strategy004.py +*/ +func Strategy004(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + adxSlow := ta.ADX(e.High, e.Low, e.Close, 35).Get(0) + cci := ta.CCI(e.Close, 14).Get(0) + fastD, _, fastK := ta.KDJBy(e.High, e.Low, e.Close, 5, 3, 3, "sma") + fastDBig, _, fastKBig := ta.KDJBy(e.High, e.Low, e.Close, 50, 3, 3, "sma") + ema := ta.EMA(e.Close, 5).Get(0) + KDX := fastK.Cross(fastD) + + adxOk := adx > 50 || adxSlow > 26 + fastKDOk := fastK.Get(1) < 20 && fastD.Get(1) < 20 + fastKDBigOk := fastKBig.Get(1) < 30 && fastDBig.Get(1) < 30 + + fastKDgt70 := fastK.Get(0) > 70 || fastD.Get(0) > 70 + if adxOk && cci < -100 && fastKDOk && fastKDBigOk && KDX == 1 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: ema * 0.1, + }) + } else if adxSlow < 25 && fastKDgt70 && fastK.Get(1) < fastD.Get(1) && e.Close.Get(0) > ema { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + }, + } +} diff --git a/demo/freqtrade/Strategy005.go b/demo/freqtrade/Strategy005.go new file mode 100644 index 0000000..53a28c3 --- /dev/null +++ b/demo/freqtrade/Strategy005.go @@ -0,0 +1,26 @@ +package freqtrade + +/* +Strategy 005 +author@: Gerald Lonlas +github@: https://github.com/freqtrade/freqtrade-strategies/blob/main/user_data/strategies/Strategy005.py +*/ + +//func Strategy005(p *config.RunPolicyConfig) *strat.TradeStrat { +// buyVolAvg := int(p.Def("buyVolAvg", 150, core.PNorm(50, 300))) +// buyRsi := p.Def("buyRsi", 26, core.PNorm(1, 100)) +// buyFastD := p.Def("buyFastD", 1, core.PNorm(1, 100)) +// buyFishNo := p.Def("buyFishNo", 5, core.PNorm(1, 100)) +// sellRsi := p.Def("sellRsi", 74, core.PNorm(1, 100)) +// sellMiDi := p.Def("sellMiDi", 4, core.PNorm(1, 100)) +// sellFiRsiN := p.Def("sellFiRsiN", 30, core.PNorm(1, 100)) +// return &strat.TradeStrat{ +// WarmupNum: 100, +// OnBar: func(s *strat.StratJob) { +// e := s.Env +// macdCols := ta.MACD(e.Close, 12, 26, 9).Cols +// macd, macdSig := macdCols[0], macdCols[1] +// +// }, +// } +//} diff --git a/demo/freqtrade/common.go b/demo/freqtrade/common.go new file mode 100644 index 0000000..9d26546 --- /dev/null +++ b/demo/freqtrade/common.go @@ -0,0 +1,11 @@ +package freqtrade + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("freqtrade", map[string]strat.FuncMakeStrat{ + "Strategy001": Strategy001, + "Strategy004": Strategy004, + + }) +} diff --git a/demo/freqtrade/strategy001.go b/demo/freqtrade/strategy001.go new file mode 100644 index 0000000..0d7739f --- /dev/null +++ b/demo/freqtrade/strategy001.go @@ -0,0 +1,46 @@ +package freqtrade + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* + Strategy 001 +author@: Gerald Lonlas +github@: https://github.com/freqtrade/freqtrade-strategies/blob/main/user_data/strategies/Strategy001.py +*/ + +func Strategy001(p *config.RunPolicyConfig) *strat.TradeStrat { + lenSml := int(p.Def("lenSml", 20, core.PNorm(10, 30))) + midRate := p.Def("midRate", 2.5, core.PNorm(1.5, 5)) + bigRate := p.Def("bigRate", 2, core.PNorm(1.5, 3)) + lenMid := int(float64(lenSml) * midRate) + lenBig := int(float64(lenMid) * bigRate) + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + maSml := ta.EMA(e.Close, lenSml) + maMid := ta.EMA(e.Close, lenMid) + maBig := ta.EMA(e.Close, lenBig) + haO, _, _, haC := ta.HeikinAshi(e) + haOpen := haO.Get(0) + haClose := haC.Get(0) + smlXmid := maSml.Cross(maMid) + midXBig := maMid.Cross(maBig) + + if smlXmid == 1 && haClose > maSml.Get(0) && haOpen < haClose { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: haClose * 0.1, + }) + } else if midXBig == 1 && haClose < maSml.Get(0) && haOpen > haClose { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + }, + } +} diff --git a/demo/here.java b/demo/here.java new file mode 100644 index 0000000..5ccc75c --- /dev/null +++ b/demo/here.java @@ -0,0 +1,3 @@ +public static void main (String[] args) { + System.out.println("Hello World2!"); +} \ No newline at end of file diff --git a/demo/hft.go b/demo/hft.go new file mode 100644 index 0000000..282a512 --- /dev/null +++ b/demo/hft.go @@ -0,0 +1,43 @@ +package demo + +import ( + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg" + "github.com/banbox/banexg/log" + "go.uber.org/zap" +) + +func hft(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WsSubs: map[string]string{ + core.WsSubDepth: "", + core.WsSubTrade: "", + core.WsSubKLine: "", + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + log.Info(fmt.Sprintf("OnBar %v: %v", e.TimeStop, e.Close.Get(0))) + }, + OnWsKline: func(s *strat.StratJob, pair string, k *banexg.Kline) { + log.Info(fmt.Sprintf("OnWsKline %v: %v", k.Time, k.Close)) + s.OpenOrder(&strat.EnterReq{Tag: "long"}) + _, _, err := biz.GetOdMgr(s.Account).ProcessOrders(s) + if err != nil { + log.Error("process order fail", zap.Error(err)) + } + }, + OnWsTrades: func(s *strat.StratJob, pair string, trades []*banexg.Trade) { + last := trades[len(trades)-1] + log.Info(fmt.Sprintf("OnWsTrades %v %v, %v", last.Timestamp, last.Price, last.Amount)) + }, + OnWsDepth: func(s *strat.StratJob, dep *banexg.OrderBook) { + bp1, bm1 := dep.Bids.Price[0], dep.Bids.Size[0] + ap1, am1 := dep.Asks.Price[0], dep.Asks.Size[0] + log.Info(fmt.Sprintf("OnWsDepth %v %v, %v,, %v, %v", dep.TimeStamp, bp1, bm1, ap1, am1)) + }, + } +} diff --git a/demo/hold.go b/demo/hold.go new file mode 100644 index 0000000..1ef5a83 --- /dev/null +++ b/demo/hold.go @@ -0,0 +1,19 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" +) + +func Hold(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + if len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "hold", + }) + } + }, + } +} diff --git a/demo/holdSl.go b/demo/holdSl.go new file mode 100644 index 0000000..c70c55f --- /dev/null +++ b/demo/holdSl.go @@ -0,0 +1,37 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func HoldSL(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 20, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.VWMA(e.Close, e.Volume, 5) + ma20 := ta.VWMA(e.Close, e.Volume, 10) + maCrx := ma5.Cross(ma20) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + + for _, od := range s.LongOrders { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: e.Close.Get(0) - atr*0.5, + }) + } + if maCrx == -1 && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "exit", Dirt: core.OdDirtLong}) + } else if maCrx == 1 || len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: atr * 0.5, + }) + } + }, + } +} diff --git a/demo/limits.go b/demo/limits.go new file mode 100644 index 0000000..d93712d --- /dev/null +++ b/demo/limits.go @@ -0,0 +1,48 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "go.uber.org/zap" +) + +func DemoV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + dirt := -1.0 + limitRate := 0.001 + slRate := 0.005 + return &strat.TradeStrat{ + WarmupNum: 30, + OnBar: func(s *strat.StratJob) { + if s.OrderNum == 0 { + price := s.Env.Close.Get(0) + limit := price * (1 - dirt*limitRate) + stopLoss := price * (1 - dirt*slRate) + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + Limit: limit, + Short: dirt < 0, + StopLoss: stopLoss, + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } else { + log.Info("new order", zap.Float64("limit", limit), zap.Float64("stopLoss", stopLoss)) + } + } + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + price := s.Env.Close.Get(0) + stopLoss := price * (1 - dirt*slRate) + sl := od.GetStopLoss() + if sl != nil && (stopLoss-sl.Price)*dirt > 0 { + // 有盈利,调整跟踪止损 + log.Info("set stoploss", zap.Float64("val", stopLoss)) + sl.Price = stopLoss + od.DirtyInfo = true + } + return nil + }, + } +} diff --git a/demo/ma_er.go b/demo/ma_er.go new file mode 100644 index 0000000..fbbb1ce --- /dev/null +++ b/demo/ma_er.go @@ -0,0 +1,31 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func DemoER(pol *config.RunPolicyConfig) *strat.TradeStrat { + smlLen := int(pol.Def("smlLen", 9, core.PNorm(3, 10))) + bigLen := int(pol.Def("bigLen", 23, core.PNorm(10, 40))) + erUpp := pol.Def("erUpp", 0.13, core.PNorm(0.1, 0.7)) + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.SMA(e.Close, smlLen) + ma20 := ta.SMA(e.Close, bigLen) + maCrx := ma5.Cross(ma20) + + er := ta.ER(e.Close, 50).Get(0) + + if maCrx == 1 && er < erUpp { + s.OpenOrder(&strat.EnterReq{Tag: "open"}) + } else if maCrx == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + }, + } +} diff --git a/demo/made.go b/demo/made.go new file mode 100644 index 0000000..b36c27e --- /dev/null +++ b/demo/made.go @@ -0,0 +1,86 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func Made(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastMAPeriod := pol.DefInt("fast_ma_period", 5, core.PUniform(3, 10)) + slowMAPeriod := pol.DefInt("slow_ma_period", 20, core.PUniform(10, 40)) + atrPeriod := pol.DefInt("atr_period", 14, core.PUniform(7, 21)) + slRate := pol.Def("sl_rate", 2.0, core.PUniform(1.0, 4.0)) + + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + EachMaxShort: 1, + DrawDownExit: true, // 启用回撤止盈 + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + maFast := ta.SMA(e.Close, fastMAPeriod) + maSlow := ta.SMA(e.Close, slowMAPeriod) + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + + // 计算均线交叉信号 + maCrossOver := maFast.Cross(maSlow) // 1 表示上穿 + maCrossUnder := maSlow.Cross(maFast) // 1 表示下穿 + + // 获取当前价格和ATR值 + currentPrice := e.Close.Get(0) + currentATR := atr.Get(0) + + // 多头入场信号:快线上穿慢线(金叉) + if maCrossOver == 1 { + if s.GetOrderNum(core.OdDirtShort) > 0 { + s.CloseOrders(&strat.ExitReq{Dirt: core.OdDirtShort, Tag: "exit_short"}) + } + if s.GetOrderNum(core.OdDirtLong) == 0 { + stopLossPrice := currentPrice - currentATR*slRate + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLoss: stopLossPrice, + StopLossTag: "atr_sl_long", + }) + } + } + + // 空头入场信号:快线下穿慢线(死叉) + if maCrossUnder == 1 { + if s.GetOrderNum(core.OdDirtLong) > 0 { + s.CloseOrders(&strat.ExitReq{Dirt: core.OdDirtLong, Tag: "exit_long"}) + } + if s.GetOrderNum(core.OdDirtShort) == 0 { + stopLossPrice := currentPrice + currentATR*slRate + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + StopLoss: stopLossPrice, + StopLossTag: "atr_sl_short", + }) + } + } + + // 多头出场信号:快线下穿慢线(死叉) + if maCrossUnder == 1 && s.GetOrderNum(core.OdDirtLong) > 0 { + s.CloseOrders(&strat.ExitReq{ + Dirt: core.OdDirtLong, + Tag: "exit_long", + }) + } + + // 空头出场信号:快线上穿慢线(金叉) + if maCrossOver == 1 && s.GetOrderNum(core.OdDirtShort) > 0 { + s.CloseOrders(&strat.ExitReq{ + Dirt: core.OdDirtShort, + Tag: "exit_short", + }) + } + }, + } +} diff --git a/demo/main.go b/demo/main.go new file mode 100644 index 0000000..4de7a14 --- /dev/null +++ b/demo/main.go @@ -0,0 +1,26 @@ +package demo + +import ( + "github.com/banbox/banbot/strat" +) + +func init() { + strat.AddStratGroup("demo", map[string]strat.FuncMakeStrat{ + "dca": DCA, + "tmp": tmp, + "01": DemoV1, + "agg": BarAgg, + "stat": BarStat, + "demo": Demo, + "demo_er": DemoER, + "riskRw": fixRiskReward, + "hold": Hold, + "holdSL": HoldSL, + "show": Show, + "tmp1": Tmp1, + "made": Made, + "test": Test, + "hft": hft, + "TrailStop": TrailStop, + }) +} diff --git a/demo/pairs_test.go b/demo/pairs_test.go new file mode 100644 index 0000000..bfd190b --- /dev/null +++ b/demo/pairs_test.go @@ -0,0 +1,97 @@ +package demo + +import ( + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/goods" + "github.com/banbox/banbot/orm" + "github.com/banbox/banexg/log" + "go.uber.org/zap" + "math/rand" + "slices" + "sort" + "testing" + "time" +) + +func genVolPairs(timeMS int64) []string { + err := biz.SetupComs(&config.CmdArgs{}) + if err != nil { + panic(err) + } + _, err = orm.LoadMarkets(exg.Default, false) + if err != nil { + panic(err) + } + ft := goods.VolumePairFilter{ + BaseFilter: goods.BaseFilter{Name: "VolumePairFilter"}, + BackPeriod: "3d", + } + core.SetRunMode(core.RunModeBackTest) + core.SetRunEnv(core.RunEnvProd) + btime.CurTimeMS = timeMS + config.PairMgr.Cron = "" + pairs, err := ft.GenSymbols(btime.UTCStamp()) + if err != nil { + panic(err) + } + res := make([]string, 0, len(pairs)) + for _, pair := range pairs { + _, quote, _, _ := core.SplitSymbol(pair) + if _, ok := config.StakeCurrencyMap[quote]; ok { + res = append(res, pair) + } + } + return res +} + +func TestRandPairs(t *testing.T) { + timeMS, _ := btime.ParseTimeMS("2021-11-01 00:00") + pairs := genVolPairs(timeMS) + total := len(pairs) + half := pairs[:total/3] + for i := range half { + j := rand.Intn(i + 1) + if i != j { + half[i], half[j] = half[j], half[i] + } + } + cutNum := 50 + if len(half) > cutNum { + half = half[:cutNum] + } + log.Info("pairs", zap.Int("total", total), zap.Strings("arr", half)) +} + +func TestPairsLocs(t *testing.T) { + pairs := genVolPairs(time.Now().UnixMilli()) + //pairs48 := []string{"1000FLOKI/USDT:USDT", "1000LUNC/USDT:USDT", "1000PEPE/USDT:USDT", "1000SHIB/USDT:USDT", "1000XEC/USDT:USDT", "ADA/USDT:USDT", "APT/USDT:USDT", "ARB/USDT:USDT", "AVAX/USDT:USDT", "BADGER/USDT:USDT", "BCH/USDT:USDT", "BNB/USDT:USDT", "BOME/USDT:USDT", "BTC/USDT:USDT", "CFX/USDT:USDT", "DOGE/USDT:USDT", "DOT/USDT:USDT", "ETC/USDT:USDT", "ETH/USDT:USDT", "ETHFI/USDT:USDT", "FET/USDT:USDT", "FIL/USDT:USDT", "FTM/USDT:USDT", "GALA/USDT:USDT", "HIFI/USDT:USDT", "ICP/USDT:USDT", "ID/USDT:USDT", "JUP/USDT:USDT", "LINK/USDT:USDT", "LTC/USDT:USDT", "LUNA2/USDT:USDT", "MATIC/USDT:USDT", "NEAR/USDT:USDT", "ONDO/USDT:USDT", "OP/USDT:USDT", "ORDI/USDT:USDT", "PENDLE/USDT:USDT", "PIXEL/USDT:USDT", "POLYX/USDT:USDT", "RSR/USDT:USDT", "SAND/USDT:USDT", "SOL/USDT:USDT", "STX/USDT:USDT", "SUI/USDT:USDT", "TOKEN/USDT:USDT", "TON/USDT:USDT", "WIF/USDT:USDT", "WLD/USDT:USDT", "XRP/USDT:USDT", "YGG/USDT:USDT"} + pairs170 := []string{"1000BONK/USDT:USDT", "1000FLOKI/USDT:USDT", "1000LUNC/USDT:USDT", "1000PEPE/USDT:USDT", "1000RATS/USDT:USDT", "1000XEC/USDT:USDT", "1INCH/USDT:USDT", "AAVE/USDT:USDT", "ACH/USDT:USDT", "ADA/USDT:USDT", "AGIX/USDT:USDT", "ALGO/USDT:USDT", "ANKR/USDT:USDT", "APE/USDT:USDT", "API3/USDT:USDT", "APT/USDT:USDT", "AR/USDT:USDT", "ARB/USDT:USDT", "ARPA/USDT:USDT", "ASTR/USDT:USDT", "ATOM/USDT:USDT", "AUCTION/USDT:USDT", "AVAX/USDT:USDT", "AXL/USDT:USDT", "AXS/USDT:USDT", "BADGER/USDT:USDT", "BAKE/USDT:USDT", "BCH/USDT:USDT", "BEAMX/USDT:USDT", "BEL/USDT:USDT", "BICO/USDT:USDT", "BLUR/USDT:USDT", "BNB/USDT:USDT", "BNX/USDT:USDT", "BOND/USDT:USDT", "BSV/USDT:USDT", "BTC/USDT:USDT", "CAKE/USDT:USDT", "CELO/USDT:USDT", "CFX/USDT:USDT", "CHR/USDT:USDT", "CHZ/USDT:USDT", "COMP/USDT:USDT", "CYBER/USDT:USDT", "DOGE/USDT:USDT", "DOT/USDT:USDT", "DUSK/USDT:USDT", "DYM/USDT:USDT", "EGLD/USDT:USDT", "ENA/USDT:USDT", "ENJ/USDT:USDT", "EOS/USDT:USDT", "ETC/USDT:USDT", "ETH/USDT:USDT", "ETHW/USDT:USDT", "FET/USDT:USDT", "FIL/USDT:USDT", "FLM/USDT:USDT", "FLOW/USDT:USDT", "FRONT/USDT:USDT", "FTM/USDT:USDT", "GAL/USDT:USDT", "GALA/USDT:USDT", "GAS/USDT:USDT", "GLM/USDT:USDT", "GMT/USDT:USDT", "GMX/USDT:USDT", "GRT/USDT:USDT", "HBAR/USDT:USDT", "HIFI/USDT:USDT", "HOOK/USDT:USDT", "HOT/USDT:USDT", "ICP/USDT:USDT", "ID/USDT:USDT", "ILV/USDT:USDT", "IMX/USDT:USDT", "INJ/USDT:USDT", "IOTA/USDT:USDT", "IOTX/USDT:USDT", "JASMY/USDT:USDT", "JOE/USDT:USDT", "JTO/USDT:USDT", "JUP/USDT:USDT", "KAS/USDT:USDT", "KAVA/USDT:USDT", "KEY/USDT:USDT", "KNC/USDT:USDT", "KSM/USDT:USDT", "LDO/USDT:USDT", "LEVER/USDT:USDT", "LINA/USDT:USDT", "LINK/USDT:USDT", "LPT/USDT:USDT", "LQTY/USDT:USDT", "LRC/USDT:USDT", "LTC/USDT:USDT", "LUNA2/USDT:USDT", "MAGIC/USDT:USDT", "MANA/USDT:USDT", "MANTA/USDT:USDT", "MASK/USDT:USDT", "MATIC/USDT:USDT", "MAV/USDT:USDT", "MEME/USDT:USDT", "METIS/USDT:USDT", "MINA/USDT:USDT", "MKR/USDT:USDT", "MTL/USDT:USDT", "MYRO/USDT:USDT", "NEO/USDT:USDT", "NKN/USDT:USDT", "OCEAN/USDT:USDT", "OM/USDT:USDT", "ONDO/USDT:USDT", "ONE/USDT:USDT", "OP/USDT:USDT", "ORDI/USDT:USDT", "PENDLE/USDT:USDT", "PEOPLE/USDT:USDT", "POLYX/USDT:USDT", "PYTH/USDT:USDT", "QNT/USDT:USDT", "QTUM/USDT:USDT", "RDNT/USDT:USDT", "REN/USDT:USDT", "RIF/USDT:USDT", "RLC/USDT:USDT", "RSR/USDT:USDT", "RUNE/USDT:USDT", "RVN/USDT:USDT", "SEI/USDT:USDT", "SKL/USDT:USDT", "SNX/USDT:USDT", "SPELL/USDT:USDT", "SSV/USDT:USDT", "STG/USDT:USDT", "STMX/USDT:USDT", "STORJ/USDT:USDT", "STX/USDT:USDT", "SUI/USDT:USDT", "SXP/USDT:USDT", "T/USDT:USDT", "THETA/USDT:USDT", "TIA/USDT:USDT", "TOKEN/USDT:USDT", "TON/USDT:USDT", "TRU/USDT:USDT", "TRX/USDT:USDT", "UMA/USDT:USDT", "VET/USDT:USDT", "WAVES/USDT:USDT", "WAXP/USDT:USDT", "WIF/USDT:USDT", "WOO/USDT:USDT", "XAI/USDT:USDT", "XLM/USDT:USDT", "XRP/USDT:USDT", "XTZ/USDT:USDT", "XVG/USDT:USDT", "YFI/USDT:USDT", "YGG/USDT:USDT", "ZEC/USDT:USDT", "ZEN/USDT:USDT", "ZETA/USDT:USDT", "ZIL/USDT:USDT", "ZRX/USDT:USDT"} + fails := make([]string, 0, 10) + idArr := make([]int, 0, 100) + for _, p := range pairs170 { + idx := slices.Index(pairs, p) + if idx < 0 { + fails = append(fails, p) + } else { + idArr = append(idArr, idx) + } + } + log.Warn("pairs", zap.Int("total", len(pairs)), zap.Strings("unknown", fails)) + sort.Slice(idArr, func(i, j int) bool { + return idArr[i] < idArr[j] + }) + step := 10 + end, num := step, 0 + for _, v := range idArr { + if v >= end { + log.Info("range", zap.Int("to", end), zap.Int("num", num)) + num = 0 + end += step + } + num += 1 + } +} diff --git a/demo/risk_reward.go b/demo/risk_reward.go new file mode 100644 index 0000000..0d292d2 --- /dev/null +++ b/demo/risk_reward.go @@ -0,0 +1,63 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func fixRiskReward(p *config.RunPolicyConfig) *strat.TradeStrat { + rewardRate := 4.0 + return &strat.TradeStrat{ + EachMaxLong: 1, + WarmupNum: 30, + OnBar: func(s *strat.StratJob) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + rsi := ta.RSI50(e.Close, 14).Get(0) + ma := ta.SMA(e.Close, 5).Get(0) + maMid := ta.SMA(e.Close, 10).Get(0) + maBig := ta.SMA(e.Close, 40).Get(0) + c := e.Close.Get(0) + if s.OrderNum == 0 && maMid > maBig && rsi > 15 && c < ma { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: atr, + StopLossTag: "init", + }) + } + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + sl := od.GetStopLoss() + if sl.Tag == "take" { + // 已设置止盈 + return nil + } + if sl.Tag == "init" { + // 检查是否回本,如果是,设置止损到成本线 + if od.ProfitRate > 0 { + // 保存初始止损空间 + od.SetInfo("initSL", sl.Range) + feeRate := od.Enter.FeeQuote * 2 / od.EnterCost() + slPrice := od.Enter.Average * (1 + feeRate) + od.SetStopLoss(&ormo.ExitTrigger{ + Price: slPrice, + Tag: "break_even", + }) + } + } else { + //已设置成本止损,检查是否触发止盈 + slRange := od.GetInfoFloat64("initSL") + if od.Profit > slRange*rewardRate { + // 触发止盈线,设置止盈线止损 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: od.Enter.Average + slRange*rewardRate, + Tag: "take", + }) + } + } + return nil + }, + } +} diff --git a/demo/sample.go b/demo/sample.go new file mode 100644 index 0000000..2bf729d --- /dev/null +++ b/demo/sample.go @@ -0,0 +1,25 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func Sample(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.SMA(e.Close, 5) + ma20 := ta.SMA(e.Close, 20) + maCrx := ma5.Cross(ma20) + + if maCrx == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "open"}) + } else if maCrx == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + }, + } +} diff --git a/demo/show.go b/demo/show.go new file mode 100644 index 0000000..15e5135 --- /dev/null +++ b/demo/show.go @@ -0,0 +1,45 @@ +package demo + +import ( + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "go.uber.org/zap" + "strings" + "sync" +) + +func Show(pol *config.RunPolicyConfig) *strat.TradeStrat { + pairMap := make(map[string]int) + var copied []string + var lastMS int64 + var lockPair sync.Mutex + return &strat.TradeStrat{ + EachMaxLong: 1, + OnStartUp: func(s *strat.StratJob) { + lockPair.Lock() + base, _, _, _ := core.SplitSymbol(s.Symbol.Symbol) + pairMap[base] = 0 + lockPair.Unlock() + }, + OnBar: func(s *strat.StratJob) { + curMS := s.Env.TimeStop + if curMS > lastMS { + if len(copied) > 0 { + date := btime.ToDateStr(lastMS, core.DefaultDateFmt) + lockPair.Lock() + totalNum := len(pairMap) + lockPair.Unlock() + log.Info("OnBar", zap.String("date", date), zap.Int("got", len(copied)), + zap.Int("total", totalNum), zap.String("copied", strings.Join(copied, ","))) + } + lastMS = curMS + copied = nil + } + base, _, _, _ := core.SplitSymbol(s.Symbol.Symbol) + copied = append(copied, base) + }, + } +} diff --git a/demo/test.go b/demo/test.go new file mode 100644 index 0000000..fb66eb8 --- /dev/null +++ b/demo/test.go @@ -0,0 +1,45 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func Test(pol *config.RunPolicyConfig) *strat.TradeStrat { + //- 超参(极简) + highestDay := pol.DefInt("highest day", 20, core.PNorm(10, 40)) + atrN := pol.DefInt("atrn", 14, core.PNorm(10, 30)) + slNATR := pol.Def("sl natr", 2.0, core.PNorm(0.8, 4.0)) + tpNATR := pol.Def("tp natr", 2.5, core.PNorm(1.0, 5.0)) + //预热:技术指标所需长度的最大值 + 余量 + warm := max(highestDay+1, atrN+1, 20) + 2 + return &strat.TradeStrat{ + WarmupNum: warm, + RunTimeFrames: []string{"15m"}, + HedgeOff: true, + OnBar: func(s *strat.StratJob) { + e := s.Env + if e.Close.Len() <= warm { + return + } + last := e.Close.Get(0) + hhPrev := ta.Highest(e.High, highestDay).Get(1) + atr := ta.ATR(e.High, e.Low, e.Close, atrN).Get(0) + //仅在没有任何多单(含未成交挂单)时考虑新挂单倮京沋塑 + if len(s.LongOrders) == 0 { + //只在价格位于前高下方时挂“突破买入"的 stop 触发单 + if last < hhPrev && atr > 0 { + entry := hhPrev + _ = s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Stop: entry, + StopLossVal: slNATR * atr, + TakeProfitVal: tpNATR * atr, + }) + } + } + }, + } +} diff --git a/demo/tmp.go b/demo/tmp.go new file mode 100644 index 0000000..a933e67 --- /dev/null +++ b/demo/tmp.go @@ -0,0 +1,35 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "go.uber.org/zap" +) + +func tmp(p *config.RunPolicyConfig) *strat.TradeStrat { + price := 2.236 + return &strat.TradeStrat{ + OnBar: func(s *strat.StratJob) { + s.OpenOrder(&strat.EnterReq{ + Tag: "BUY", + OrderType: core.OrderTypeLimit, + Limit: price, + Amount: 4.65000, + TakeProfit: price * 1.01, + TakeProfitLimit: price * 1.012, + StopLoss: price * 0.99, + Log: true, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "all", + }) + ods, err := strat.GetHistOrders(ormo.GetHistOrdersArgs{ + Limit: 3, + }) + log.Info("get hist", zap.Int("num", len(ods)), zap.Error(err)) + }, + } +} diff --git a/demo/tmp1.go b/demo/tmp1.go new file mode 100644 index 0000000..78f6727 --- /dev/null +++ b/demo/tmp1.go @@ -0,0 +1,29 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "math" +) + +func Tmp1(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + flag := math.Round(math.Mod(float64(e.TimeStop/e.TFMSecs), 2)) + if flag == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "long"}) + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "exitS", Dirt: core.OdDirtShort}) + } + } else { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true}) + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "exitL", Dirt: core.OdDirtLong}) + } + } + }, + } +} diff --git a/demo/trailing_stop.go b/demo/trailing_stop.go new file mode 100644 index 0000000..bb76cae --- /dev/null +++ b/demo/trailing_stop.go @@ -0,0 +1,27 @@ +package demo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func TrailStop(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + TimeFrames: "1h", + OnBar: func(s *strat.StratJob) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + atrPct := atr * 100 / e.Close.Get(0) + k, d, _ := ta.KDJ(e.High, e.Low, e.Close, 9, 3, 3) + kdCross := k.Cross(d) + if len(s.LongOrders) == 0 && kdCross == 1 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + CallbackPct: min(10, 5*atrPct), + }) + } + }, + } +} diff --git a/ent/fund_rate.go b/ent/fund_rate.go new file mode 100644 index 0000000..10d70bf --- /dev/null +++ b/ent/fund_rate.go @@ -0,0 +1,124 @@ +package ent + +import ( + "fmt" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/orm" + utils2 "github.com/banbox/banbot/utils" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/utils" + "github.com/banbox/banstagy/com" + "math" + "path/filepath" + "sort" + "strconv" +) + +/* +永续合约资金费率结算后一段时间,如果资金费率<-0.0035,未来5分钟有较大概率下降 +*/ + +type FRate struct { + Time int64 + Data []*com.CodeVal +} + +func runFundEnter() *errs.Error { + exgId := "binance" + exchange, err := exg.GetWith(exgId, banexg.MarketLinear, banexg.MarketSwap) + if err != nil { + return err + } + _, err = orm.LoadMarkets(exchange, false) + if err != nil { + return err + } + dumpDir := filepath.Join(config.GetDataDir(), "dump") + err_ := utils2.EnsureDir(dumpDir, 0755) + if err_ != nil { + return errs.New(errs.CodeIOWriteFail, err_) + } + dumpPath := filepath.Join(dumpDir, exgId+"_fundingRate.json") + his := make(map[string]map[int64]float64) + err_ = utils.ReadJsonFile(dumpPath, &his, utils.JsonNumDefault) + if err_ != nil { + return errs.New(errs.CodeIOReadFail, err_) + } + dates := make(map[int64]*FRate) + for code, timeMap := range his { + for dateMS, val := range timeMap { + sta, ok := dates[dateMS] + if !ok { + sta = &FRate{Time: dateMS} + dates[dateMS] = sta + } + sta.Data = append(sta.Data, &com.CodeVal{ + Code: code, + Val: val, + }) + } + } + arr := make([]*FRate, 0, len(dates)) + for _, it := range dates { + arr = append(arr, it) + } + sort.Slice(arr, func(i, j int) bool { + return arr[i].Time < arr[j].Time + }) + // 按时间戳,对资金费率绝对值较大的,筛选K线并记录价格 + rows := make([][]string, 0) + barNum := 20 + backOff := 5 + head := []string{"time", "code", "rate", "price"} + for i := 0; i < barNum; i++ { + sid := i - backOff + if sid >= 0 { + sid += 1 + } + head = append(head, "p"+strconv.Itoa(sid)) + } + rows = append(rows, head) + for _, dt := range arr { + sort.Slice(dt.Data, func(i, j int) bool { + return math.Abs(dt.Data[i].Val) > math.Abs(dt.Data[j].Val) + }) + startMS := dt.Time - int64(backOff)*60000 + for _, it := range dt.Data { + if math.Abs(it.Val) < 0.0005 { + break + } + exs, err := orm.GetExSymbol(exchange, it.Code) + if err != nil { + return err + } + _, bars, err := orm.GetOHLCV(exs, "1m", startMS, 0, barNum, false) + if err != nil { + return err + } + if len(bars) < barNum { + continue + } + row := make([]string, 0, 4+barNum) + initPrice := bars[backOff].Open + row = append(row, btime.ToDateStr(dt.Time, core.DefaultDateFmt), it.Code, + strconv.FormatFloat(it.Val, 'f', 6, 64), + fmt.Sprintf("%.4e", initPrice)) + var pct float64 + for i, b := range bars { + if i < backOff { + pct = (1 - b.Open/initPrice) * 100 + } else { + pct = (b.Close/initPrice - 1) * 100 + } + row = append(row, strconv.FormatFloat(pct, 'f', 4, 64)) + } + rows = append(rows, row) + } + } + outPath := filepath.Join(dumpDir, exgId+"_fr.csv") + return utils2.WriteCsvFile(outPath, rows, false) +} diff --git a/ent/his_sharpe.go b/ent/his_sharpe.go new file mode 100644 index 0000000..5ede0b4 --- /dev/null +++ b/ent/his_sharpe.go @@ -0,0 +1,137 @@ +package ent + +import ( + "errors" + "flag" + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/opt" + "github.com/banbox/banbot/orm" + "github.com/banbox/banbot/orm/ormo" + utils2 "github.com/banbox/banexg/utils" + "strconv" +) + +func hisSharpe(args []string) error { + var configPaths config.ArrString + var inPath string + var sub = flag.NewFlagSet("fac", flag.ExitOnError) + sub.Var(&configPaths, "config", "config path to use, Multiple -config options may be used") + sub.StringVar(&inPath, "in", "", "path for all pairs orders e.g.: orders.gob") + err_ := sub.Parse(args) + if err_ != nil { + return err_ + } + core.SetRunMode(core.RunModeBackTest) + err := biz.SetupComsExg(&config.CmdArgs{ + Configs: configPaths, + }) + if err != nil { + return err + } + var startMs = config.TimeRange.StartMS + var endMS = config.TimeRange.EndMS + if len(config.StakeCurrency) == 0 { + return errors.New("`stake_currency` in yml is required") + } + var tfSecs int + var orders []*ormo.InOutOrder + if inPath == "" { + return errors.New("-in is required") + } else { + inPath = config.ParsePath(inPath) + } + orders, err = ormo.LoadOrdersGob(inPath) + if err != nil { + return err + } + var exsMap = make(map[int32]*orm.ExSymbol) + var pairMap = make(map[string]int32) + var totalCost float64 + var validOdNum int + if len(orders) > 0 { + startMs = orders[0].RealEnterMS() + endMS = orders[0].RealExitMS() + for _, od := range orders { + curStart := od.RealEnterMS() + if curStart < startMs && curStart > 0 { + startMs = curStart + } + curEnd := od.RealExitMS() + if curEnd > endMS { + endMS = curEnd + } + tfSecs = max(tfSecs, utils2.TFToSecs(od.Timeframe)) + curCost := od.EnterCost() + if curCost > 0 { + totalCost += curCost + validOdNum += 1 + } + if _, ok := pairMap[od.Symbol]; !ok { + exs, err := orm.GetExSymbolCur(od.Symbol) + if err != nil { + return err + } + exsMap[exs.ID] = exs + pairMap[od.Symbol] = exs.ID + } + } + } else { + return errors.New("orders is empty") + } + if validOdNum == 0 { + return errors.New("no valid orders") + } + if tfSecs == 0 { + tfSecs = utils2.TFToSecs("1d") + } + tf := "1d" + // 滚动测试因子 + rangeStart, _ := btime.ParseTimeMS("2022-11-19") + rangeEnd, _ := btime.ParseTimeMS("2024-11-18") + pairOrders, err := opt.CutOrdersInRange(orders, rangeStart, rangeEnd) + if err != nil { + return err + } + pairOrders = map[string][]*ormo.InOutOrder{ + "WIF/USDT:USDT": pairOrders["WIF/USDT:USDT"], + } + pairInfos, err := opt.CalcPairStats(pairOrders, rangeStart, rangeEnd, tf) + if err != nil { + return err + } + initFund := config.WalletAmounts["USDT"] + rangeSecs := (rangeEnd - rangeStart) / 1000 + for _, sta := range pairInfos { + reals := make([]float64, 0, len(sta.Returns)) + curFund := initFund + for _, v := range sta.Returns { + curFund += v + reals = append(reals, curFund) + } + sharp, _, err := opt.CalcMeasuresByReal(reals, rangeSecs, "", 0, 0) + if err != nil { + return err + } + labels := make([]string, len(reals)) + for i := range reals { + labels[i] = strconv.Itoa(i) + } + g := opt.Chart{ + Labels: labels, + Datasets: []*opt.ChartDs{ + { + Label: "pow(x, 1.5)", + Data: reals, + BorderColor: "rgb(75, 192, 192)", + }, + }, + } + g.DumpFile("D:/dump_pairs2.html") + fmt.Printf("%s sharpe: %v", sta.Symbol, sharp) + } + return nil +} diff --git a/ent/main.go b/ent/main.go new file mode 100644 index 0000000..6f0d2d8 --- /dev/null +++ b/ent/main.go @@ -0,0 +1,76 @@ +package ent + +import ( + "flag" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/entry" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + "github.com/banbox/banstagy/arb" + "github.com/spf13/cobra" + "os" +) + +func init() { + entry.AddCommand("tool", &cobra.Command{ + Use: "hisSharpe", + DisableFlagParsing: true, + RunE: func(_ *cobra.Command, args []string) error { + return hisSharpe(args) + }, + }) +} + +func RunMyEntry(args []string) *errs.Error { + if len(args) == 0 { + log.Warn("sub command is require for `my`") + return nil + } + var ag = &config.CmdArgs{} + var f = flag.NewFlagSet("web", flag.ExitOnError) + f.StringVar(&ag.LogLevel, "level", "info", "log level") + f.StringVar(&ag.TimeZone, "tz", "", "timezone") + f.StringVar(&ag.DataDir, "datadir", "", "Path to data dir.") + f.StringVar(&ag.ConfigData, "config-data", "", "yaml config string") + f.Var(&ag.Configs, "config", "config path to use, Multiple -config options may be used") + if args == nil { + args = os.Args + } + err_ := f.Parse(args[1:]) + if err_ != nil { + return errs.New(errs.CodeRunTime, err_) + } + + err := biz.SetupComsExg(&config.CmdArgs{ + LogLevel: ag.LogLevel, + DataDir: ag.DataDir, + TimeZone: ag.TimeZone, + Configs: ag.Configs, + ConfigData: ag.ConfigData, + }) + if err != nil { + return err + } + switch args[0] { + case "fundRate": + err = runFundEnter() + case "moneyFlow": + err = maxVolTest() + case "maxChg": + err = maxChgTest() + case "twoPairRatio": + err = arb.GenTwoPairRatio() + case "profTuple": + err = arb.ProfTuples() + case "tupleSpike": + err = arb.FindTupleSpikes() + case "statGroup": + err = arb.StatGroupPairs() + case "balance": + err = showBalance() + default: + log.Warn("unknown sub command for `my`") + } + return err +} diff --git a/ent/max_chg.go b/ent/max_chg.go new file mode 100644 index 0000000..912ed2a --- /dev/null +++ b/ent/max_chg.go @@ -0,0 +1,195 @@ +package ent + +import ( + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + utils2 "github.com/banbox/banexg/utils" + "github.com/banbox/banstagy/com" + "github.com/banbox/banta" + "math" + "path/filepath" + "sort" + "strconv" +) + +type chgSta struct { + env *banta.BarEnv + volMa *com.Ema + chg float64 // 变动百分比 + stability float64 // 稳定性分数 + adx float64 + rsi float64 + er float64 + price float64 + weight float64 + nexts []*orm.InfoKline + num int +} + +func maxChgTest() *errs.Error { + exgId := "binance" + tf := "1m" + maAlpha := 0.03 + viewNextNum := 5 + tfMSecs := int64(utils2.TFToSecs(tf) * 1000) + exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear) + exsList := make([]*orm.ExSymbol, 0, len(exsMap)) + envs := make(map[string]*chgSta) + staList := make([]*chgSta, 0, len(exsMap)) + topStas := make([]*chgSta, 0, len(exsMap)/4) + for _, exs := range exsMap { + _, quote, _, _ := core.SplitSymbol(exs.Symbol) + if quote != "USDT" { + continue + } + exsList = append(exsList, exs) + env := &chgSta{ + env: &banta.BarEnv{ + TimeFrame: tf, + TFMSecs: tfMSecs, + Exchange: exs.Exchange, + MarketType: exs.Market, + Symbol: exs.Symbol, + MaxCache: 1000, + }, + volMa: com.NewEMA(maAlpha), + } + envs[exs.Symbol] = env + staList = append(staList, env) + } + barNum := 0 + barTimeMS := int64(0) + var rows [][]string + var head = []string{"time", "code", "chg", "weight", "price", "adx", "rsi", "er"} + for i := 0; i < viewNextNum; i++ { + head = append(head, "p"+strconv.Itoa(i+1)) + } + rows = append(rows, head) + updateWeights := func() { + total := float64(0) + for _, e := range envs { + total += e.volMa.Val + } + if math.IsNaN(total) || total == 0 { + return + } + for _, e := range envs { + e.weight = (e.volMa.Val / total) * 100 + } + sort.Slice(staList, func(i, j int) bool { + return staList[i].weight > staList[j].weight + }) + topStas = staList[:150] + } + checkBatch := func() { + sort.Slice(topStas, func(i, j int) bool { + return math.Abs(topStas[i].chg) > math.Abs(topStas[j].chg) + }) + for i := 0; i < 3; i++ { + e := topStas[i] + row := []string{ + btime.ToDateStr(barTimeMS, core.DefaultDateFmt), + e.env.Symbol, + strconv.FormatFloat(e.chg, 'f', 2, 64), + strconv.FormatFloat(e.weight, 'f', 2, 64), + fmt.Sprintf("%.3e", e.price), + strconv.FormatFloat(e.adx, 'f', 2, 64), + strconv.FormatFloat(e.rsi, 'f', 2, 64), + strconv.FormatFloat(e.er, 'f', 2, 64), + } + for j := 0; j < viewNextNum; j++ { + rate := (e.nexts[j].Close/e.price - 1) * 100 + row = append(row, strconv.FormatFloat(rate, 'f', 2, 64)) + } + rows = append(rows, row) + } + } + totalSecs := (config.TimeRange.EndMS - config.TimeRange.StartMS) / 1000 + pbar := utils.NewPrgBar(int(totalSecs), "Run") + defer pbar.Close() + onBar := func(b *orm.InfoKline, nexts []*orm.InfoKline) { + if b.Time > barTimeMS { + if !b.IsWarmUp && barTimeMS > 0 { + pbar.Add(int((b.Time - barTimeMS) / 1000)) + } + barNum += 1 + barTimeMS = b.Time + if barNum%20 == 1 { + // 每隔20个bar,更新全局权重 + updateWeights() + } + if !b.IsWarmUp && barNum%5 == 0 { + // 每隔5个bar,计算一次变化 + checkBatch() + } + } + sta := envs[b.Symbol] + sta.num += 1 + e := sta.env + e.OnBar(b.Time, b.Open, b.High, b.Low, b.Close, b.Volume, + b.Quote, b.BuyVolume, b.TradeNum) + avgPrice := (b.High + b.Low + b.Close) / 3 + sta.volMa.Update(b.Volume * avgPrice) + sta.chg = (b.Close/b.Open - 1) * 100 + sta.adx = banta.ADX(e.High, e.Low, e.Close, 3).Get(0) + sta.rsi = banta.RSI(e.Close, 3).Get(0) + sta.er = banta.ER(e.Close, 3).Get(0) + if b.IsWarmUp { + return + } + sta.nexts = nexts + sta.price = b.Close + } + var replayErr *errs.Error + verCh := make(chan int, 1) + onData := func(evt *orm.DataSeries, nexts []*orm.DataSeries) { + bar, err_ := orm.AsKline(evt) + if err_ != nil { + replayErr = errs.New(errs.CodeRunTime, err_) + select { + case verCh <- -1: + default: + } + return + } + futureBars := make([]*orm.InfoKline, 0, len(nexts)) + for _, next := range nexts { + futureBar, convErr := orm.AsKline(next) + if convErr != nil { + replayErr = errs.New(errs.CodeRunTime, convErr) + select { + case verCh <- -1: + default: + } + return + } + futureBars = append(futureBars, futureBar) + } + onBar(bar, futureBars) + } + err := biz.RunHistSeries(&biz.RunHistSeriesArgs{ + ExsList: exsList, + Start: config.TimeRange.StartMS, + End: config.TimeRange.EndMS, + ViewNextNum: viewNextNum, + TfWarms: map[string]int{tf: 100}, + VerCh: verCh, + OnData: onData, + }) + if err == nil && replayErr != nil { + err = replayErr + } + outPath := filepath.Join(config.GetDataDir(), "dump", "maxChg.csv") + err2 := utils.WriteCsvFile(outPath, rows, false) + if err == nil { + err = err2 + } + return err +} diff --git a/ent/max_vol.go b/ent/max_vol.go new file mode 100644 index 0000000..652c1bf --- /dev/null +++ b/ent/max_vol.go @@ -0,0 +1,182 @@ +package ent + +import ( + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banstagy/com" + "math" + "path/filepath" + "sort" + "strconv" +) + +type volSta struct { + code string + longVol *com.Ema + shortVol *com.Ema + longRate float64 + shortRate float64 + weight float64 + price float64 + nexts []*orm.InfoKline + num int +} + +func maxVolTest() *errs.Error { + exgId := "binance" + tf := "1h" + maAlpha := 0.03 + viewNextNum := 3 + exsMap := orm.GetExSymbols(exgId, banexg.MarketLinear) + exsList := make([]*orm.ExSymbol, 0, len(exsMap)) + envs := make(map[string]*volSta) + envList := make([]*volSta, 0, len(exsMap)) + for _, exs := range exsMap { + _, quote, _, _ := core.SplitSymbol(exs.Symbol) + if quote != "USDT" { + continue + } + exsList = append(exsList, exs) + env := &volSta{ + code: exs.Symbol, + longVol: com.NewEMA(maAlpha), + shortVol: com.NewEMA(maAlpha), + } + envs[exs.Symbol] = env + envList = append(envList, env) + } + barNum := 0 + barTimeMS := int64(0) + updateWeights := func() { + total := float64(0) + for _, e := range envs { + total += e.longVol.Val + e.shortVol.Val + } + if math.IsNaN(total) { + return + } + for _, e := range envs { + wei := (e.longVol.Val + e.shortVol.Val) / total + e.weight = math.Pow(wei, 0.5) + } + } + var rows [][]string + var head = []string{"time", "code", "longRt", "shortRt"} + for i := 0; i < viewNextNum; i++ { + head = append(head, "p"+strconv.Itoa(i+1)) + } + rows = append(rows, head) + checkBatch := func() { + sort.Slice(envList, func(i, j int) bool { + return envList[i].longRate > envList[j].shortRate + }) + for i := 0; i < 3; i++ { + e := envList[i] + row := []string{ + btime.ToDateStr(barTimeMS, core.DefaultDateFmt), + e.code, + strconv.FormatFloat(e.longRate, 'f', 3, 64), + strconv.FormatFloat(e.shortRate, 'f', 3, 64), + } + for j := 0; j < viewNextNum; j++ { + rate := (e.nexts[j].Close/e.price - 1) * 100 + row = append(row, strconv.FormatFloat(rate, 'f', 2, 64)) + } + rows = append(rows, row) + } + } + totalSecs := (config.TimeRange.EndMS - config.TimeRange.StartMS) / 1000 + pbar := utils.NewPrgBar(int(totalSecs), "Run") + defer pbar.Close() + onBar := func(b *orm.InfoKline, nexts []*orm.InfoKline) { + if b.Time > barTimeMS { + if !b.IsWarmUp && barTimeMS > 0 { + pbar.Add(int((b.Time - barTimeMS) / 1000)) + } + barNum += 1 + barTimeMS = b.Time + if barNum%20 == 1 { + // 每隔20个bar,更新全局权重 + updateWeights() + } + if !b.IsWarmUp && barNum%5 == 0 { + // 每隔5个bar,计算一次变化 + checkBatch() + } + } + sta := envs[b.Symbol] + sta.num += 1 + lineTot := (b.High - b.Low) - math.Abs(b.Open-b.Close) + total := (b.High - b.Low) + lineTot + core.AmtDust + longTot := lineTot + if b.Open < b.Close { + longTot += math.Abs(b.Open - b.Close) + } + avgPrice := (b.High + b.Low + b.Close) / 3 + quoteVol := avgPrice * b.Volume + longRt := longTot / total + longVol := quoteVol * longRt + shortVol := quoteVol - longVol + sta.longVol.Update(longVol) + sta.shortVol.Update(shortVol) + if b.IsWarmUp { + return + } + // 暂时不考虑多空直接比例 + sta.longRate = longVol / sta.longVol.Val * sta.weight * math.Pow(longRt, 2) + sta.shortRate = shortVol / sta.shortVol.Val * sta.weight * math.Pow(1-longRt, 2) + sta.nexts = nexts + sta.price = b.Close + } + var replayErr *errs.Error + verCh := make(chan int, 1) + onData := func(evt *orm.DataSeries, nexts []*orm.DataSeries) { + bar, err_ := orm.AsKline(evt) + if err_ != nil { + replayErr = errs.New(errs.CodeRunTime, err_) + select { + case verCh <- -1: + default: + } + return + } + futureBars := make([]*orm.InfoKline, 0, len(nexts)) + for _, next := range nexts { + futureBar, convErr := orm.AsKline(next) + if convErr != nil { + replayErr = errs.New(errs.CodeRunTime, convErr) + select { + case verCh <- -1: + default: + } + return + } + futureBars = append(futureBars, futureBar) + } + onBar(bar, futureBars) + } + err := biz.RunHistSeries(&biz.RunHistSeriesArgs{ + ExsList: exsList, + Start: config.TimeRange.StartMS, + End: config.TimeRange.EndMS, + ViewNextNum: viewNextNum, + TfWarms: map[string]int{tf: 100}, + VerCh: verCh, + OnData: onData, + }) + if err == nil && replayErr != nil { + err = replayErr + } + outPath := filepath.Join(config.GetDataDir(), "dump", "moneyFlow.csv") + err2 := utils.WriteCsvFile(outPath, rows, false) + if err == nil { + err = err2 + } + return err +} diff --git a/ent/tools.go b/ent/tools.go new file mode 100644 index 0000000..884d529 --- /dev/null +++ b/ent/tools.go @@ -0,0 +1,31 @@ +package ent + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/exg" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/utils" +) + +func showBalance() *errs.Error { + var account string + for acc := range config.Accounts { + account = acc + break + } + exchange := exg.Default + res, err := exchange.FetchBalance(map[string]interface{}{ + banexg.ParamAccount: account, + }) + if err != nil { + return err + } + resstr, err_ := utils.MarshalString(res.Info) + if err_ != nil { + return errs.New(errs.CodeMarshalFail, err_) + } + fmt.Println(resstr) + return nil +} diff --git a/fac/best.go b/fac/best.go new file mode 100644 index 0000000..15ba045 --- /dev/null +++ b/fac/best.go @@ -0,0 +1,172 @@ +package fac + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/opt" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg/log" + utils2 "github.com/banbox/banexg/utils" + "go.uber.org/zap" + "gonum.org/v1/gonum/floats" +) + +func init() { + opt.FactorMap["best"] = best + opt.FactorMap["best3"] = best3 + opt.FactorMap["bigVol"] = bigVols + opt.FactorMap["sharpe"] = sharpe +} + +func bigVols(arg opt.FacArgs) ([]string, error) { + allNum := len(arg.Pairs) + cutNum := min(int(float64(allNum)/10), 30) + // 只取成交量高的一半品种 + volPeriodMS := int64(utils2.TFToSecs("30d") * 1000) + volBarNum := int(volPeriodMS / arg.TFMSecs) + vols := make([]float64, 0, allNum) + for _, sta := range arg.Pairs { + start := max(0, len(sta.KLines)-volBarNum) + bars := sta.KLines[start:] + volSum := float64(0) + for _, b := range bars { + volSum += b.Volume * b.Close + } + vols = append(vols, volSum) + } + volIdxArr := utils.ArgSortDesc(vols)[:allNum/2] + var result = make([]string, 0, cutNum) + for _, idx := range volIdxArr { + result = append(result, arg.Pairs[idx].Symbol) + if len(result) >= cutNum { + break + } + } + return result, nil +} + +func best(arg opt.FacArgs) ([]string, error) { + return bestOf(arg, 0) +} + +func best3(arg opt.FacArgs) ([]string, error) { + return bestOf(arg, 0.25) +} + +func bestOf(arg opt.FacArgs, offRate float64) ([]string, error) { + allNum := len(arg.Pairs) + initFund := float64(0) + for _, v := range config.WalletAmounts { + initFund = v + break + } + cutNum := min(int(float64(allNum)/10), 30) + // 只取成交量高的一半品种 + volPeriodMS := int64(utils2.TFToSecs("30d") * 1000) + volBarNum := int(volPeriodMS / arg.TFMSecs) + vols := make([]float64, 0, allNum) + scores := make([]float64, 0, allNum) + for _, sta := range arg.Pairs { + start := max(0, len(sta.KLines)-volBarNum) + bars := sta.KLines[start:] + volSum := float64(0) + for _, b := range bars { + volSum += b.Volume * b.Close + } + vols = append(vols, volSum) + profitPct := floats.Sum(sta.Returns) * 100 / initFund + reals := make([]float64, 0, len(sta.Returns)) + curFund := initFund + for _, v := range sta.Returns { + curFund += v + reals = append(reals, curFund) + } + drawRate, _ := utils.CalcDrawDown(reals, opt.ShowNum) + score := opt.CalcBtScore(profitPct, drawRate*100) + scores = append(scores, score) + } + volIdxArr := utils.ArgSortDesc(vols)[:allNum/2] + scoreIdxArr := utils.ArgSortDesc(scores) + volIdMap := utils.ArrToMap(volIdxArr, func(t2 int) int { + return t2 + }) + offset := int(float64(len(scoreIdxArr)) * offRate) + var result = make([]string, 0, cutNum) + for _, idx := range scoreIdxArr[offset:] { + if _, ok := volIdMap[idx]; ok { + result = append(result, arg.Pairs[idx].Symbol) + if len(result) >= cutNum { + break + } + } + } + return result, nil +} + +func sharpe(arg opt.FacArgs) ([]string, error) { + allNum := len(arg.Pairs) + initFund := float64(0) + for _, v := range config.WalletAmounts { + initFund = v + break + } + cutNum := min(int(float64(allNum)/10), 30) + // 只取成交量高的一半品种 + volPeriodMS := int64(utils2.TFToSecs("30d") * 1000) + volBarNum := int(volPeriodMS / arg.TFMSecs) + vols := make([]float64, 0, allNum) + scores := make([]float64, 0, allNum) + totOdNum := 0 + rangeSecs := (arg.EndMS - arg.StartMS) / 1000 + for _, sta := range arg.Pairs { + if len(sta.KLines) == 0 || sta.KLines[0].Time > arg.StartMS { + // 跳过回顾周期内数据不全的品种 + vols = append(vols, 0) + scores = append(scores, -99) + continue + } + start := max(0, len(sta.KLines)-volBarNum) + bars := sta.KLines[start:] + volSum := float64(0) + for _, b := range bars { + volSum += b.Volume * b.Close + } + vols = append(vols, volSum) + reals := make([]float64, 0, len(sta.Returns)) + curFund := initFund + for _, v := range sta.Returns { + curFund += v + reals = append(reals, curFund) + } + sharp, _, err := opt.CalcMeasuresByReal(reals, rangeSecs, "", 0, 0) + if err != nil { + return nil, err + } + scores = append(scores, sharp) + totOdNum += len(sta.Orders) + } + validNum := len(vols) + volIdxArr := utils.ArgSortDesc(vols)[:validNum/2] + scoreIdxArr := utils.ArgSortDesc(scores) + volIdMap := utils.ArrToMap(volIdxArr, func(t2 int) int { + return t2 + }) + avgOdNum := totOdNum / len(arg.Pairs) + var result = make([]string, 0, cutNum) + var raw = make([]string, 0, allNum) + for _, idx := range scoreIdxArr { + //raw[arg.Pairs[idx].Symbol] = scores[idx] + raw = append(raw, arg.Pairs[idx].Symbol) + if _, ok := volIdMap[idx]; ok { + sta := arg.Pairs[idx] + if len(sta.Orders)*4 < avgOdNum { + continue + } + result = append(result, sta.Symbol) + if len(result) >= cutNum { + break + } + } + } + log.Info("by sharpe", zap.Strings("pairs", raw)) + return result, nil +} diff --git a/filters/exp_pos.go b/filters/exp_pos.go new file mode 100644 index 0000000..68d38b2 --- /dev/null +++ b/filters/exp_pos.go @@ -0,0 +1,88 @@ +package filters + +import ( + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/goods" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + "go.uber.org/zap" + "math" + "strings" +) + +func init() { + goods.RegisterFilter("ExpPosFilter", func(base goods.BaseFilter) goods.IFilter { + return &ExpPosFilter{ + BaseFilter: base, + Exp: 3, + CutRate: 1, + } + }) +} + +type ExpPosFilter struct { + goods.BaseFilter + Rate float64 `yaml:"rate" mapstructure:"rate,omitempty"` + // 输出的数量 + Limit int `yaml:"limit" mapstructure:"limit,omitempty"` + // 偏斜因子 (k)。> 1.0 的值会使分布偏向较小的索引。默认2 + Exp float64 `yaml:"exp" mapstructure:"exp,omitempty"` + // 允许的最大索引占总数的百分比 (例如 0.5 表示 50%)默认1 + CutRate float64 `yaml:"cut_rate" mapstructure:"cut_rate,omitempty"` +} + +func (e *ExpPosFilter) Filter(pairs []string, timeMS int64) ([]string, *errs.Error) { + outNum := float64(len(pairs)) + if e.Rate > 0 { + outNum *= e.Rate + } + outNumV := int(outNum) + if e.Limit > 0 && e.Limit < outNumV { + outNumV = e.Limit + } + maxId := float64(len(pairs)) * e.CutRate + outIds := calcRankings(int(maxId), outNumV, e.Exp) + outPairs := make([]string, 0, len(outIds)) + hit := make(map[int]bool) + codes := make([]string, 0, len(outIds)) + for _, id := range outIds { + if _, ok := hit[id]; ok { + continue + } + hit[id] = true + outPairs = append(outPairs, pairs[id]) + base, _, _, _ := core.SplitSymbol(pairs[id]) + codes = append(codes, base) + } + dateStr := btime.ToDateStr(btime.TimeMS(), core.DefaultDateFmt) + log.Info("ExpPosFilter", zap.String("date", dateStr), zap.Int("inNum", len(pairs)), zap.Ints("ids", outIds), + zap.String("code", strings.Join(codes, ","))) + return outPairs, nil +} + +func calcRankings(totalNum int, pickNum int, skewFactor float64) []int { + if pickNum <= 1 { + return []int{0} + } + + rankings := make([]int, pickNum) + + for i := 1; i <= pickNum; i++ { + // 2. 将源索引 (i) 归一化到 [0.0, 1.0] 范围 + proportion := float64(i) / float64(pickNum) + + // 3. 应用幂函数 (k) 来“偏斜”分布 + // 这使得 0.1 -> 0.01, 0.5 -> 0.25 (当 k=2.0 时) + // 导致更多早期项目聚集在较低的索引上 + skewedProportion := math.Pow(proportion, skewFactor) + + // 4. 将偏斜后的比例 [0.0, 1.0] 映射到最终的索引范围 [0, totalNum-1] + finalRank := skewedProportion * float64(totalNum-1) + + // 转换为整数索引 + rankings[i-1] = int(math.Round(finalRank)) + } + + return rankings +} diff --git a/flux/grid/common.go b/flux/grid/common.go new file mode 100644 index 0000000..22d6018 --- /dev/null +++ b/flux/grid/common.go @@ -0,0 +1,302 @@ +package grid + +import ( + "fmt" + "math" + "slices" + "strconv" + "strings" + + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" +) + +/* +CheckPos在每个bar的close上检查位置,是否需要补仓或止盈。 +bar中间的high/low忽略。所以尽量在1m周期执行,避免一个bar的high/low突破网格,close又收回来导致未执行的问题 +*/ + +func init() { + strat.AddStratGroup("grid", map[string]strat.FuncMakeStrat{ + "inv": InvGrid, + "long": LongGrid, + }) +} + +func NewGrid(initRate, maxAdd int, slRate float64, debug bool) *Grid { + return &Grid{ + InitRate: float64(initRate), + MaxAdd: maxAdd, + SLRate: slRate, + Debug: debug, + } +} + +type Grid struct { + InitRate float64 // 初次打开网格投入倍数 + MaxAdd int // 最大加仓次数 + SLRate float64 // 止损相对Unit的倍数 + Dirt float64 // 当前是做多网格还是做空;1做多,-1做空 + HoldSize float64 // 持有仓位倍率;相对单位开仓大小 + Unit float64 // 网格价格单位 + OneAmt float64 // 一份的数量,初次入场时计算 + EntPrice float64 // 入场价格 + Pos float64 // 网格位置,整数,-6/-5/-4/-3/-2/-1/0/1/2/3/4/5/6 + OldPos float64 // 上次网格位置 + openNeed bool // 是否需要打开加仓单 + takeNeed bool // 是否需要追加止盈单 + + Debug bool + barLogs []string + barMS int64 +} + +func (m *Grid) UpdatePos(e *ta.BarEnv, price float64) { + offRaw := (price - m.EntPrice) / m.Unit + rate := math.Floor(math.Abs(offRaw) + 0.1) + if offRaw < 0 { + rate = -rate + } + if rate != m.Pos { + m.Pos = rate + m.takeNeed = true + m.openNeed = true + if m.Debug { + base := m.EntPrice + m.Pos*m.Unit + m.log(e, "upper: %.2f lower: %.2f", base+m.Unit, base-m.Unit) + } + } +} + +func (m *Grid) Open(s *strat.StratJob) { + c := s.Env.Close.Get(0) + var tag = "short" + if m.Dirt > 0 { + tag = "long" + } + m.EntPrice = 0 + m.Pos = 0 + m.OldPos = 0 + m.HoldSize = 0 + m.OneAmt = s.Strat.GetStakeAmount(s) / c + m.openNeed = true + m.takeNeed = true + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + Amount: m.InitRate * m.OneAmt, + Short: m.Dirt < 0, + StopLossVal: m.Unit * m.SLRate, + }) + if m.Debug { + m.log(s.Env, "init open, dir: %v, size: %v, grid: %.3f, pct: %.1f", + m.Dirt, m.InitRate, m.Unit, m.Unit*100/c) + } +} + +func (m *Grid) CheckPos(s *strat.StratJob) { + e := s.Env + m.UpdatePos(e, e.Close.Get(0)) + if !m.openNeed && !m.takeNeed { + return + } + odList := s.GetOrders(m.Dirt) + // 取消未入场的触发订单 + cancels := make([]string, 0) + var holds = make(map[string]int) + for _, od := range odList { + if od.Status < ormo.InOutStatusPartEnter { + s.CloseOrders(&strat.ExitReq{Tag: "cancel", OrderID: od.ID, Force: true}) + if m.Debug { + cancels = append(cancels, strconv.FormatFloat(od.Enter.Price, 'f', 4, 64)) + } + } else if m.Debug { + num := int(math.Round(od.HoldAmount() / m.OneAmt)) + row := strconv.FormatFloat(od.Enter.Average, 'f', 2, 64) + old, _ := holds[row] + holds[row] = old + num + } + } + if m.HoldSize < 0.1 { + if m.Debug { + m.log(e, "hold reset\n") + } + s.CloseOrders(&strat.ExitReq{Tag: "clear", Force: true}) + return + } + posChg := (m.Pos - m.OldPos) * m.Dirt + m.OldPos = m.Pos + jumpNum := math.Abs(posChg) - 1 + var jumpStr string + if jumpNum > 0 { + // 市场剧烈变化,此时最新价格已偏移多个网格 + extAmt := m.OneAmt * jumpNum + tag := "jadd" + if posChg > 0 { + // 剧烈盈利,卖出额外持仓 + tag = "jexit" + s.CloseOrders(&strat.ExitReq{ + Tag: tag, + Amount: extAmt, + FilledOnly: true, + Force: true, + }) + } else { + // 剧烈亏损,买入额外持仓 + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + Amount: extAmt, + Short: m.Dirt < 0, + StopLoss: odList[0].GetStopLoss().Price, + }) + } + if m.Debug { + jumpStr = fmt.Sprintf("jump %v %.0f ", tag, jumpNum) + } + } + // 设置最新的止盈止损等待触发 + trigPrice := m.EntPrice + m.Pos*m.Unit + openPrice, closePrice := trigPrice-m.Unit, trigPrice+m.Unit + if m.Dirt < 0 { + openPrice, closePrice = closePrice, openPrice + } + var openInfo string + if m.openNeed { + if -m.Pos*m.Dirt >= float64(m.MaxAdd) { + // 达到最大加仓深度 + if m.Debug { + openInfo = "open=nil " + } + } else { + sl := odList[0].GetStopLoss() + stopLoss := e.Close.Get(0) - m.Dirt*m.Unit*m.SLRate + if sl != nil { + stopLoss = sl.Price + } else { + log.Info("hit sl nil") + } + s.OpenOrder(&strat.EnterReq{ + Tag: "add", + Limit: openPrice, + Amount: m.OneAmt, + Short: m.Dirt < 0, + StopLoss: stopLoss, + }) + if m.Debug { + openInfo = fmt.Sprintf("open=%.4f ", openPrice) + } + } + } + m.openNeed = false + if m.takeNeed { + s.SetAllTakeProfit(m.Dirt, &ormo.ExitTrigger{ + Price: closePrice, + Rate: 1 / m.HoldSize, + Tag: "exit", + }) + //s.CloseOrders(&strat.ExitReq{ + // Tag: "exit", + // Limit: closePrice, + // Amount: m.OneAmt, + // Force: true, + //}) + } + m.takeNeed = false + if m.Debug { + var takes = make(map[string]int) + for _, od := range odList { + tp := od.GetTakeProfit() + if tp != nil { + tpNum := math.Round(od.HoldAmount() / m.OneAmt) + if tp.Rate > 0 && tp.Rate < 1 { + tpNum *= tp.Rate + } + tpPrice := strconv.FormatFloat(tp.Price, 'f', 2, 64) + old, _ := takes[tpPrice] + takes[tpPrice] = old + int(math.Round(tpNum)) + } + } + var cancel string + if len(cancels) > 0 { + cancel = "cancel: [" + strings.Join(cancels, ", ") + "] " + } + hold := printAmts(holds, "hold: ") + take := printAmts(takes, "take: ") + m.log(e, cancel+jumpStr+openInfo+hold+take) + } +} + +func (m *Grid) OnOrderChange(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + if chgType == strat.OdChgEnterFill && od.Enter.Filled > core.AmtDust { + // 加仓入场完成事件 + e := s.Env + if m.EntPrice == 0 { + // 设置第一个下跌买入和第一个盈利卖出 + m.EntPrice = od.Enter.Average + sl := od.GetStopLoss() + stoploss := float64(0) + if sl != nil { + stoploss = sl.Price + } + upgaps := make([]string, 0, 5) + dngaps := make([]string, 0, 5) + for i := 0; i < 5; i++ { + dist := m.Unit * float64(i+1) + upgaps = append(upgaps, strconv.FormatFloat(m.EntPrice+dist, 'f', 4, 64)) + dngaps = append(dngaps, strconv.FormatFloat(m.EntPrice-dist, 'f', 4, 64)) + } + if m.Debug { + m.log(e, "p=%.4f sl=%.4f", m.EntPrice, stoploss) + m.log(e, "up: %v", strings.Join(upgaps, " ")) + m.log(e, "dn: %v", strings.Join(dngaps, " ")) + } + } + addNum := math.Round(od.Enter.Filled / m.OneAmt) + m.HoldSize += addNum + m.openNeed = addNum > 0 + if m.Debug { + m.log(e, "[enter] %.4f %.0f hold %.0f", + od.Enter.Average, addNum, m.HoldSize) + } + } else if chgType == strat.OdChgExitFill && od.Exit.Filled > core.AmtDust { + e := s.Env + exitNum := math.Round(od.Exit.Filled / m.OneAmt) + m.HoldSize -= exitNum + m.takeNeed = exitNum > 0 + if m.Debug { + m.log(e, "[exit] %v %.4f -> %.4f amt %.0f hold %.0f", + od.ExitTag, od.Enter.Average, od.Exit.Average, exitNum, m.HoldSize) + } + } +} + +func (m *Grid) log(e *ta.BarEnv, tpl string, args ...any) { + if e.TimeStart > m.barMS { + content := strings.Join(m.barLogs, "\n\t") + fmt.Printf(content) + fmt.Printf("\n") + date := btime.ToDateStr(e.TimeStart, core.DefaultDateFmt) + m.barLogs = []string{date + fmt.Sprintf(" h: %.2f l: %.2f c: %.2f", + e.High.Get(0), e.Low.Get(0), e.Close.Get(0))} + m.barMS = e.TimeStart + } + m.barLogs = append(m.barLogs, fmt.Sprintf(tpl, args...)) +} + +func printAmts(amts map[string]int, prefix string) string { + if len(amts) == 0 { + return "" + } + ents := utils.KeysOfMap(amts) + slices.Sort(ents) + for i, k := range ents { + if amts[k] > 1 { + ents[i] += " * " + strconv.Itoa(amts[k]) + } + } + return prefix + "[" + strings.Join(ents, ", ") + "] " +} diff --git a/flux/grid/inv.go b/flux/grid/inv.go new file mode 100644 index 0000000..dc09c3e --- /dev/null +++ b/flux/grid/inv.go @@ -0,0 +1,81 @@ +package grid + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +多头网格,下跌买入,上涨卖出。适合波动性较强,方向不明的市场。 + +以大周期atrBase的一定比例作为网格的单位大小 +初始倍率: InitRate + +初次开仓条件: + 大周期ER20 < 0.3 + 小周期MA5 < MA20 看多网格;否则看空网格 +持有仓位: + +*/ + +type GridV1 struct { + *Grid + bigER float64 // 大周期ER系数 +} + +func InvGrid(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := 20 + baseAtrLen := int(float64(lenAtr) * 4.3) + unitRate := pol.Def("unitRate", 2, core.PNorm(0.5, 5)) + initRate := int(pol.Def("initRate", 4, core.PNorm(0.7, 7.3))) + maxAdd := 5 + return &strat.TradeStrat{ + WarmupNum: 100, + StopEnterBars: 9999999, + TimeFrames: "1m", + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", 100}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &GridV1{Grid: NewGrid(initRate, maxAdd, 6, false)} + }, + OnBar: func(s *strat.StratJob) { + m, _ := s.More.(*GridV1) + if s.OrderNum == 0 { + if m.Unit == 0 || m.bigER >= 0.1 || s.IsWarmUp { + return + } + m.Open(s) + } else { + m.CheckPos(s) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*GridV1) + m.bigER = ta.ER(e.Close, 20).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + ma5 := ta.SMA(e.Close, 10).Get(0) + ma20 := ta.SMA(e.Close, 50).Get(0) + if s.OrderNum == 0 && !math.IsNaN(atrBase) { + // 未持仓时更新网格单元大小 + m.Unit = atrBase * unitRate + m.Dirt = core.OdDirtShort + if ma5 < ma20 { + m.Dirt = core.OdDirtLong + } + } + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + m, _ := s.More.(*GridV1) + m.OnOrderChange(s, od, chgType) + }, + } +} diff --git a/flux/grid/long.go b/flux/grid/long.go new file mode 100644 index 0000000..06e2bb0 --- /dev/null +++ b/flux/grid/long.go @@ -0,0 +1,83 @@ +package grid + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +多头网格,下跌买入,上涨卖出。适合波动性较强,方向不明的市场。 + +以大周期atrBase的一定比例作为网格的单位大小 +初始倍率: InitRate + +初次开仓条件: + 大周期ER20 < 0.3 + 小周期MA5 < MA20 看多网格;否则看空网格 +持有仓位: + +*/ + +type GridLong struct { + *Grid + bigER float64 // 大周期ER系数 +} + +func LongGrid(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := 20 + baseAtrLen := int(float64(lenAtr) * 4.3) + unitRate := pol.Def("unitRate", 2, core.PNorm(0.5, 5)) + initRate := int(pol.Def("initRate", 4, core.PNorm(0.7, 7.3))) + maxAdd := 5 + return &strat.TradeStrat{ + WarmupNum: 100, + StopEnterBars: 9999999, + TimeFrames: "1m", + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", 100}, + } + }, + OnStartUp: func(s *strat.StratJob) { + g := NewGrid(initRate, maxAdd, 6, false) + s.More = &GridLong{Grid: g} + }, + OnBar: func(s *strat.StratJob) { + m, _ := s.More.(*GridLong) + if s.OrderNum == 0 { + if m.Unit == 0 || s.IsWarmUp { + return + } + m.Open(s) + } else { + m.CheckPos(s) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*GridLong) + m.bigER = ta.ER(e.Close, 20).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + //ma5 := ta.SMA(e.Close, 10).Get(0) + //ma20 := ta.SMA(e.Close, 50).Get(0) + if s.OrderNum == 0 && !math.IsNaN(atrBase) { + // 未持仓时更新网格单元大小 + m.Unit = atrBase * unitRate + m.Dirt = core.OdDirtLong + //m.Dirt = core.OdDirtShort + //if ma5 < ma20 { + // m.Dirt = core.OdDirtLong + //} + } + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + m, _ := s.More.(*GridLong) + m.OnOrderChange(s, od, chgType) + }, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..971e684 --- /dev/null +++ b/go.mod @@ -0,0 +1,147 @@ +module github.com/banbox/banstagy + +go 1.24.0 + +toolchain go1.24.9 + +replace github.com/banbox/banbot => ../banbot + +replace github.com/banbox/banexg => ../banexg + +replace github.com/banbox/banta => ../banta + +require ( + github.com/banbox/banbot v0.2.31 + github.com/banbox/banexg v0.2.60 + github.com/banbox/banta v0.3.7 + github.com/sasha-s/go-deadlock v0.3.6 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/anyongjin/go-bayesopt v1.0.2 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/banbox/bntp v0.1.0 // indirect + github.com/banbox/cron/v3 v3.1.2 // indirect + github.com/beevik/ntp v1.4.3 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cloudwego/eino v0.6.0 // indirect + github.com/cloudwego/eino-ext/components/model/openai v0.1.1 // indirect + github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250918130948-16e3a249e721 // indirect + github.com/ebitengine/purego v0.9.0 // indirect + github.com/eino-contrib/jsonschema v1.0.2 // indirect + github.com/evanphx/json-patch v0.5.2 // indirect + github.com/fasthttp/websocket v1.5.12 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/fgprof v0.9.5 // indirect + github.com/flopp/go-findfont v0.1.0 // indirect + github.com/fogleman/gg v1.3.0 // indirect + github.com/getkin/kin-openapi v0.118.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.19.5 // indirect + github.com/go-telegram/bot v1.17.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gofiber/contrib/websocket v1.3.4 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nikolalohinski/gonja v1.5.3 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/perimeterx/marshmallow v1.1.4 // indirect + github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/tiendc/go-deepcopy v1.7.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/image v0.32.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.39.1 // indirect +) + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/c-bata/goptuna v0.9.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/gofiber/fiber/v2 v2.52.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + //github.com/pkujhd/goloader v0.0.0-20240113094056-ff3a1e01ffcb // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/schollz/progressbar/v3 v3.18.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.9 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.64.0 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/excelize/v2 v2.10.0 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + gonum.org/v1/gonum v0.16.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..678c524 --- /dev/null +++ b/go.sum @@ -0,0 +1,435 @@ +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/anyongjin/go-bayesopt v1.0.2 h1:845NHAdxk4x1FxeOS5McoaWO33ZfE2AR434j9lnEHAM= +github.com/anyongjin/go-bayesopt v1.0.2/go.mod h1:KB64n3O9sPBql6pPnOKh0w8bnOSG1TIMNb+9c/9iQs0= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/banbox/bntp v0.1.0 h1:kZROpbb3OME8XM8QeM+2SeQWQrHRDFN05okr8OITKAU= +github.com/banbox/bntp v0.1.0/go.mod h1:w0WP7GROKQo07wHFBm/FVgH4xlZuOYCoro0ky4iTC3M= +github.com/banbox/cron/v3 v3.1.2 h1:gBNnz/OBxIMU7iBCtObZKhb85rzbaXr8AZLZb40CgeM= +github.com/banbox/cron/v3 v3.1.2/go.mod h1:SZzzT03qsZ1y8/Pt2VqKb9Av/HfnuSargOQRmql7n18= +github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= +github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/mockey v1.2.14 h1:KZaFgPdiUwW+jOWFieo3Lr7INM1P+6adO3hxZhDswY8= +github.com/bytedance/mockey v1.2.14/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/c-bata/goptuna v0.9.0 h1:JUO13AVxM4YmO9/NMoaNWegfwaM4wAlkrQ+oN/fgfrg= +github.com/c-bata/goptuna v0.9.0/go.mod h1:X2UFBRhqjEI+x2xw/LbaGRaFa/TRBH9w8QjknAaEavo= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/eino v0.6.0 h1:pobGKMOfcQHVNhD9UT/HrvO0eYG6FC2ML/NKY2Eb9+Q= +github.com/cloudwego/eino v0.6.0/go.mod h1:JNapfU+QUrFFpboNDrNOFvmz0m9wjBFHHCr77RH6a50= +github.com/cloudwego/eino-ext/components/model/openai v0.1.1 h1:VRdUDcnfi/T8F0jcuovhdADU9Io/oMqiKpY2ZJTBc1o= +github.com/cloudwego/eino-ext/components/model/openai v0.1.1/go.mod h1:VwAXEY1ik2K9KFPZvymnkfBQQKgLHbpg90yg+7hrTt8= +github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250918130948-16e3a249e721 h1:5Hd8GxNEmu+ppTGCRBU6kLKfCQNXPMwi31xA83PzEqo= +github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250918130948-16e3a249e721/go.mod h1:fHn/6OqPPY1iLLx9wzz+MEVT5Dl9gwuZte1oLEnCoYw= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= +github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/eino-contrib/jsonschema v1.0.2 h1:HaxruBMUdnXa7Lg/lX8g0Hk71ZIfdTZXmBQz0e3esr8= +github.com/eino-contrib/jsonschema v1.0.2/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= +github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= +github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM= +github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-telegram/bot v1.17.0 h1:Hs0kGxSj97QFqOQP0zxduY/4tSx8QDzvNI9uVRS+zmY= +github.com/go-telegram/bot v1.17.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg= +github.com/gofiber/contrib/websocket v1.3.4/go.mod h1:kTFBPC6YENCnKfKx0BoOFjgXxdz7E85/STdkmZPEmPs= +github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= +github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0 h1:nIohpHs1ViKR0SVgW/cbBstHjmnqFZDM9RqgX9m9Xu8= +github.com/meguminnnnnnnnn/go-openai v0.0.0-20250821095446-07791bea23a0/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE= +github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw= +github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= +github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= +github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= +github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU= +github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= +github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og= +github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= +github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= +modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/llmjob/128.go b/llmjob/128.go new file mode 100644 index 0000000..a5ca5ec --- /dev/null +++ b/llmjob/128.go @@ -0,0 +1,189 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J128(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + longEntryThreshold := pol.Def("long_entry_threshold", 60.0, core.PNorm(50.0, 70.0)) + thresholdRange := pol.Def("threshold_range", 14.0, core.PNorm(10.0, 25.0)) + smaPeriod := pol.DefInt("sma_period", 3, core.PNorm(2, 8)) + atrPeriod := pol.DefInt("atr_period", 14, core.PNorm(7, 21)) + tp1ATR := pol.Def("tp1_atr", 3.0, core.PNorm(1.5, 5.0)) + tp2ATR := pol.Def("tp2_atr", 8.0, core.PNorm(5.0, 12.0)) + tp3ATR := pol.Def("tp3_atr", 14.0, core.PNorm(10.0, 20.0)) + tp1Percent := pol.Def("tp1_percent", 12.0, core.PNorm(5.0, 25.0)) + tp2Percent := pol.Def("tp2_percent", 12.0, core.PNorm(5.0, 25.0)) + tp3Percent := pol.Def("tp3_percent", 12.0, core.PNorm(5.0, 25.0)) + useTakeProfit := pol.Def("use_take_profit", 0.0, core.PNorm(0.0, 1.0)) // 0=false, 1=true + tradingDirection := pol.Def("trading_direction", 2.0, core.PNorm(0.0, 2.0)) // 0=Long, 1=Short, 2=Both + + // 计算衍生参数 + shortEntryThreshold := longEntryThreshold - thresholdRange + longExitThreshold := shortEntryThreshold + shortExitThreshold := longEntryThreshold + + // 确定最长预热周期 + warmupNum := 55 + smaPeriod + 10 + + return &strat.TradeStrat{ + Name: "FATR", + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, + EachMaxShort: 1, + RunTimeFrames: []string{"4h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算真实高低点和真实波动范围 + trueLow := e.Low.Min(e.Close.Back(1)) + trueHigh := e.High.Max(e.Close.Back(1)) + trueRange := trueHigh.Sub(trueLow) + + // 计算买入压力(BP) + bp := e.Close.Sub(trueLow) + + // 计算不同周期的压力比率 + calcRatio := func(len int) *ta.Series { + sumBP := ta.Sum(bp, len) + sumTR := ta.Sum(trueRange, len) + return sumBP.Div(sumTR).Mul(100) + } + + // 计算Fibonacci数列周期的压力比率 + ratio8 := calcRatio(8) + ratio13 := calcRatio(13) + ratio21 := calcRatio(21) + ratio34 := calcRatio(34) + ratio55 := calcRatio(55) + + // 计算加权平均值 + weightedAvg := ratio8.Mul(5).Add( + ratio13.Mul(4)).Add( + ratio21.Mul(3)).Add( + ratio34.Mul(2)).Add( + ratio55).Div(15) + + // 应用SMA平滑 + weightedAvgSMA := ta.SMA(weightedAvg, smaPeriod) + + // 计算ATR值 + atrValue := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + + // 获取当前值 + atrVal := atrValue.Get(0) + closePrice := e.Close.Get(0) + + // 生成交易信号 + longCondition := weightedAvgSMA.Cross(longEntryThreshold) == 1 + shortCondition := weightedAvgSMA.Cross(shortEntryThreshold) == -1 + longExit := weightedAvgSMA.Cross(longExitThreshold) == -1 + shortExit := weightedAvgSMA.Cross(shortExitThreshold) == 1 + + // 执行交易逻辑 + isLongEnabled := tradingDirection == 0 || tradingDirection == 2 + isShortEnabled := tradingDirection == 1 || tradingDirection == 2 + + // 多头交易逻辑 + if isLongEnabled { + if longCondition && len(s.LongOrders) == 0 { + // 开多单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + }) + + // 设置止盈订单 + if useTakeProfit > 0.5 { + tpPrice1 := closePrice + tp1ATR*atrVal + tpPrice2 := closePrice + tp2ATR*atrVal + tpPrice3 := closePrice + tp3ATR*atrVal + + // 设置多个止盈订单 + s.CloseOrders(&strat.ExitReq{ + Tag: "TP1_Long", + Dirt: core.OdDirtLong, + ExitRate: tp1Percent / 100, + Limit: tpPrice1, + }) + + s.CloseOrders(&strat.ExitReq{ + Tag: "TP2_Long", + Dirt: core.OdDirtLong, + ExitRate: tp2Percent / 100, + Limit: tpPrice2, + }) + + s.CloseOrders(&strat.ExitReq{ + Tag: "TP3_Long", + Dirt: core.OdDirtLong, + ExitRate: tp3Percent / 100, + Limit: tpPrice3, + }) + } + } + + if longExit && len(s.LongOrders) > 0 { + // 平多单 + s.CloseOrders(&strat.ExitReq{ + Tag: "Long_Exit", + Dirt: core.OdDirtLong, + }) + } + } + + // 空头交易逻辑 + if isShortEnabled { + if shortCondition && len(s.ShortOrders) == 0 { + // 开空单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + }) + + // 设置止盈订单 + if useTakeProfit > 0.5 { + tpPrice1 := closePrice - tp1ATR*atrVal + tpPrice2 := closePrice - tp2ATR*atrVal + tpPrice3 := closePrice - tp3ATR*atrVal + + // 设置多个止盈订单 + s.CloseOrders(&strat.ExitReq{ + Tag: "TP1_Short", + Dirt: core.OdDirtShort, + ExitRate: tp1Percent / 100, + Limit: tpPrice1, + }) + + s.CloseOrders(&strat.ExitReq{ + Tag: "TP2_Short", + Dirt: core.OdDirtShort, + ExitRate: tp2Percent / 100, + Limit: tpPrice2, + }) + + s.CloseOrders(&strat.ExitReq{ + Tag: "TP3_Short", + Dirt: core.OdDirtShort, + ExitRate: tp3Percent / 100, + Limit: tpPrice3, + }) + } + } + + if shortExit && len(s.ShortOrders) > 0 { + // 平空单 + s.CloseOrders(&strat.ExitReq{ + Tag: "Short_Exit", + Dirt: core.OdDirtShort, + }) + } + } + }, + } +} diff --git a/llmjob/1357.go b/llmjob/1357.go new file mode 100644 index 0000000..114d333 --- /dev/null +++ b/llmjob/1357.go @@ -0,0 +1,59 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J1357(pol *config.RunPolicyConfig) *strat.TradeStrat { + fastLength := pol.DefInt("fastLength", 16, core.PNorm(5, 20)) + slowLength := pol.DefInt("slowLength", 26, core.PNorm(15, 50)) + signalLength := pol.DefInt("signalLength", 9, core.PNorm(5, 15)) + + return &strat.TradeStrat{ + WarmupNum: slowLength + signalLength, + EachMaxLong: 3, + EachMaxShort: 3, + HedgeOff: true, + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算MACD指标 + macd, signal := ta.MACD(e.Close, fastLength, slowLength, signalLength) + + // 检测MACD线与信号线的交叉 + buySignal := macd.Cross(signal) + sellSignal := signal.Cross(macd) + + // 买入信号:MACD线上穿信号线 + if buySignal == 1 { + // 开多单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long Trigger", + }) + // 平空单 + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Short Trigger", + Dirt: core.OdDirtShort, + }) + } + + // 卖出信号:MACD线下穿信号线 + if sellSignal == 1 { + // 开空单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short Trigger", + Short: true, + }) + // 平多单 + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Long Trigger", + Dirt: core.OdDirtLong, + }) + } + }, + } +} diff --git a/llmjob/1525.go b/llmjob/1525.go new file mode 100644 index 0000000..ba7ec14 --- /dev/null +++ b/llmjob/1525.go @@ -0,0 +1,525 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "fmt" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "github.com/sasha-s/go-deadlock" + "go.uber.org/zap" + "math" +) + +/* +根据AtrBase基于不同品种权重,限制范围后更好 +从4h根据行情自动调整加仓阈值和止盈阈值后: +之前:夏普0.93,显示回撤35.8%,最大回撤95.4%,最大资金占用3.2W,总收益119.6% +之后:夏普1.38,显示回撤5.66%,最大回撤36.2%,最大资金占用1.64W,总收益79.4% + +尝试使用公式0.383*pow(1+target_qty, x)加仓,符合测试的数据,但效果更差 +*/ + +// State1525S 存储每个品种的策略状态 +type State1525S struct { + avgPrice float64 // 平均持仓成本 + positionQty float64 // 当前持仓数量 + tpLimit float64 + score float64 // 下跌行情力度分数,值域[0,6] +} + +func J1525(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + smaPeriod := pol.DefInt("sma_period", 18, core.PNorm(5, 50)) + targetPercRaw := pol.Def("target_perc", -0.14, core.PNorm(-0.3, -0.05)) + takeProfitRaw := pol.Def("take_profit", 0.09, core.PNorm(0.05, 0.3)) + targetQty := pol.Def("target_qty", 0.65, core.PNorm(0.2, 1.0)) + // 加仓次数,超过20后变差;低于19后有段时间不稳定 + maxAdd := pol.DefInt("max_add", 19, core.PNorm(1, 30)) + maxOpenPair := pol.DefInt("max_open_pair", 25, core.PNorm(1, 30)) // 最大同时交易品种 + + openPairs := make(map[*strat.StratJob]int) + pairLock := deadlock.Mutex{} + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + + targetPerc := targetPercRaw + takeProfit := takeProfitRaw + + printMS := int64(0) + maxPairs := 0 + // 加仓倍率,基于targetQty: 1, 0.6, 1 1.7 3 4.7 7.7 12.8 21.1 35 57.5 95 157 256 385 464 767 + return &strat.TradeStrat{ + WarmupNum: max(smaPeriod, baseAtrLen), + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"5m"}, + BatchInfo: true, + OrderOnRotation: "open", + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + s.More = &State1525S{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", 20}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*State1525S) + closePrice := e.Close.Get(0) + + // 不利行情下调整参数 + targetPercJob, takeProfitJob := targetPercRaw, takeProfitRaw + if state.score >= 5 { + targetPercJob = targetPercRaw * 2 + takeProfitJob = takeProfitRaw * 0.6 + } else if state.score >= 4 { + targetPercJob = targetPercRaw * 1.7 + takeProfitJob = takeProfitRaw * 0.7 + } else if state.score >= 3 { + targetPercJob = targetPercRaw * 1.3 + takeProfitJob = takeProfitRaw * 0.8 + } + targetPercCur := (targetPercJob + targetPerc) / 2 + takeProfitCur := (takeProfitJob + takeProfit) / 2 + + // 计算SMA + ma := ta.SMA(e.Close, smaPeriod) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + openRate := 0.006 * closePrice / atrBase + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + openRate = max(0.7, min(1.5, openRate)) + + // 计算当前持仓盈亏比例 + var pnl float64 + if state.positionQty > 0 && state.avgPrice > 0 { + pnl = (closePrice - state.avgPrice) / state.avgPrice + } + + // 计算止盈价格 + if state.positionQty > 0 && state.avgPrice > 0 { + curTakeProfit := state.avgPrice * (1 + takeProfitCur) + if curTakeProfit != state.tpLimit { + state.tpLimit = curTakeProfit + // 设置限价单止盈 + s.CloseOrders(&strat.ExitReq{ + Tag: "Take Profit", + Limit: state.tpLimit, + }) + } + } + + // 首次入场条件:价格上穿SMA且无持仓 + if e.Close.Cross(ma) == 1 && state.positionQty == 0 { + pairLock.Lock() + _, allow := openPairs[s] + if !allow && len(openPairs) < maxOpenPair { + openPairs[s] = 1 + maxPairs = max(maxPairs, len(openPairs)) + allow = true + } + pairLock.Unlock() + if allow { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + CostRate: openRate, + }) + } + } + + // 加仓条件:亏损达到目标比例且有持仓 + if state.positionQty > 0 && pnl <= targetPercCur && len(s.LongOrders) < maxAdd { + // 计算加仓数量,确保是整数 + odNum := len(s.LongOrders) + addQty := math.Floor(state.positionQty * targetQty) + pairLock.Lock() + openPairs[s] = odNum + pairLock.Unlock() + s.OpenOrder(&strat.EnterReq{ + Tag: fmt.Sprintf("%v", odNum), + Amount: addQty, + }) + if odNum > 5 { + printMS = e.TimeStop + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + score := float64(0) + if ta.RSI(e.Close, 14).Get(0) < 30 { + score += 1 + } + roc := ta.ROC(e.Close, 20).Get(0) + if roc < -20 { + score += 2 + } else if roc < -10 { + score += 1 + } + plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + if minusDi.Get(0) > plusDi.Get(0) { + if adx > 40 { + score += 2 + } else if adx > 25 { + score += 1 + } + } + ma25 := ta.SMA(e.Close, 25).Get(0) + ma99 := ta.SMA(e.Close, 99).Get(0) + if ma99 > ma25 { + score += 1 + } + state, _ := s.More.(*State1525S) + state.score = score + }, + OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) { + jobNum := len(jobs) + if jobNum < 3 { + return + } + downNum, downHeavyNum := float64(0), float64(0) + timeStop := int64(0) + var last *strat.JobEnv + for _, j := range jobs { + state, _ := j.Job.More.(*State1525S) + if state.score >= 5 { + downHeavyNum += 1 + } + if state.score >= 3 { + downNum += 1 + } + timeStop = j.Env.TimeStop + last = j + } + downRate := downNum / float64(len(jobs)) + downHeavyRate := downHeavyNum / float64(len(jobs)) + if printMS > 0 { + printMS = 0 + dateStr := btime.ToDateStr(timeStop, core.DefaultDateFmt) + fmt.Printf("%v downRate: %.3f downHeavyRate: %.3f\n", dateStr, downRate*100, downHeavyRate*100) + } + if downHeavyRate >= 0.8 { + // 极端下跌 + targetPerc = targetPercRaw * 2 + takeProfit = takeProfitRaw * 0.5 + } else if downRate >= 0.85 && downHeavyRate >= 0.6 { + // 严重下跌 + targetPerc = targetPercRaw * 1.8 + takeProfit = takeProfitRaw * 0.5 + } else if downRate >= 0.7 && downRate <= 0.85 { + // 市场中度下跌 + targetPerc = targetPercRaw * 1.6 + takeProfit = takeProfitRaw * 0.65 + } else if downRate >= 0.5 && downRate <= 0.7 { + // 轻度下跌 + targetPerc = targetPercRaw * 1.3 + takeProfit = takeProfitRaw * 0.8 + } else { + // 轻度或未下跌 + targetPerc = targetPercRaw + takeProfit = takeProfitRaw + } + pairLock.Lock() + totNum := 0 + for _, num := range openPairs { + totNum += num + } + pairLock.Unlock() + if totNum > 0 { + last.Job.Strat.WriteOutput(fmt.Sprintf("pairNum: %v", totNum), true) + } + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + state, _ := s.More.(*State1525S) + + // 当订单入场时更新持仓状态 + if chgType == strat.OdChgEnterFill && od.EnterTag != "" { + // 计算新的平均成本 + if state.positionQty == 0 { + // 首次开仓 + state.avgPrice = od.InitPrice + state.positionQty = od.HoldAmount() + } else { + // 加仓,更新平均成本 + oldQty := state.positionQty + oldAvgPrice := state.avgPrice + newQty := od.HoldAmount() + newAvgPrice := od.InitPrice + + // 计算新的平均成本 + totalQty := oldQty + newQty + totalCost := oldQty*oldAvgPrice + newQty*newAvgPrice + state.avgPrice = totalCost / totalQty + state.positionQty = totalQty + } + } + + // 当订单完全平仓时重置持仓状态 + if chgType == strat.OdChgExitFill && od.ExitTag != "" && od.Status == ormo.InOutStatusFullExit { + state.avgPrice = 0 + state.positionQty = 0 + state.tpLimit = 0 + pairLock.Lock() + delete(openPairs, s) + pairLock.Unlock() + } + }, + OnShutDown: func(s *strat.StratJob) { + if maxPairs == 0 { + return + } + log.Info("max open pairs", zap.Int("num", maxPairs)) + maxPairs = 0 + }, + } +} + +// State1525Sim 存储每个品种的策略状态 +type State1525Sim struct { + totAmount float64 + totCost float64 + tpLimit float64 // 止盈出场价格 + odNum int + upFlag float64 // 1: ma20 > ma99 && ma20 > ma20[2] + score float64 // 下跌行情力度分数,值域[0,6] +} + +func J1525Sim(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 最多30个品种,单笔30,初始3W + // 定义可调超参数 + smaPeriod := pol.DefInt("sma_period", 18, core.PNorm(5, 50)) + targetPercRaw := pol.Def("target_perc", -0.14, core.PNorm(-0.3, -0.05)) + takeProfitRaw := pol.Def("take_profit", 0.09, core.PNorm(0.05, 0.3)) + targetQty := pol.Def("target_qty", 0.6, core.PNorm(0.2, 1.0)) + // 加仓次数,超过20后变差;低于19后有段时间不稳定 + maxAdd := pol.DefInt("max_add", 19, core.PNorm(1, 30)) + startLvlRaw := pol.DefInt("start_lvl", 3, core.PNorm(1, 15)) + maxOpenPair := pol.DefInt("max_open_pair", 20, core.PNorm(1, 30)) // 最大同时交易品种 + + openPairs := make(map[*strat.StratJob]int) + pairLock := deadlock.Mutex{} + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + + targetPerc := targetPercRaw + takeProfit := takeProfitRaw + startLvl := startLvlRaw + + // 加仓倍率,基于targetQty: 1, 0.6, 1 1.7 3 4.7 7.7 12.8 21.1 35 57.5 95 157 256 385 464 767 + return &strat.TradeStrat{ + WarmupNum: max(smaPeriod, baseAtrLen), + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"5m"}, + BatchInfo: true, + StakeRate: 0.25, + OrderOnRotation: "open", // 品种切换时允许开单 + OnStartUp: func(s *strat.StratJob) { + s.More = &State1525Sim{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", 20}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*State1525Sim) + closePrice := e.Close.Get(0) + + // 不利行情下调整参数 + targetPercJob, takeProfitJob := targetPercRaw, takeProfitRaw + if state.score >= 5 { + targetPercJob = targetPercRaw * 2 + takeProfitJob = takeProfitRaw * 0.6 + } else if state.score >= 4 { + targetPercJob = targetPercRaw * 1.7 + takeProfitJob = takeProfitRaw * 0.7 + } else if state.score >= 3 { + targetPercJob = targetPercRaw * 1.3 + takeProfitJob = takeProfitRaw * 0.8 + } + targetPercCur := (targetPercJob + targetPerc) / 2 + takeProfitCur := (takeProfitJob + takeProfit) / 2 + + // 计算SMA + ma := ta.SMA(e.Close, smaPeriod) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + openRate := 0.006 * closePrice / atrBase + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + openRate = max(0.7, min(1.5, openRate)) + + // 检查止盈触发 + if state.tpLimit > 0 && state.tpLimit <= e.High.Get(0) { + state.totAmount = 0 + state.totCost = 0 + state.tpLimit = 0 + state.odNum = 0 + pairLock.Lock() + delete(openPairs, s) + pairLock.Unlock() + } + + // 计算当前持仓盈亏比例、止盈 + var pnl, avgPrice float64 + if state.totAmount > 0 { + avgPrice = state.totCost / state.totAmount + pnl = closePrice/avgPrice - 1 + curTP := avgPrice * (1 + takeProfitCur) + if curTP != state.tpLimit { + state.tpLimit = curTP + // 设置限价单止盈 + s.CloseOrders(&strat.ExitReq{ + Tag: "Take Profit", + Limit: curTP, + }) + } + } + + // 首次入场条件:价格上穿SMA且无持仓 + if e.Close.Cross(ma) == 1 && state.totAmount == 0 && openRate > 0 { + state.odNum = 1 + state.totAmount = openRate + state.totCost = closePrice * openRate + } + + // 加仓条件:亏损达到目标比例且有持仓 + if avgPrice > 0 && pnl <= targetPercCur && state.odNum < maxAdd { + amt := state.totAmount * targetQty + state.totAmount += amt + state.totCost += amt * closePrice + curStart := startLvl + if state.upFlag < 0 { + // 下降趋势上升3个阶梯,和全局阈值取平均 + curStart = (startLvl + startLvlRaw + 3) / 2 + } + if state.odNum >= curStart { + pairLock.Lock() + _, allow := openPairs[s] + if !allow && len(openPairs) < maxOpenPair { + allow = true + } + if allow { + openPairs[s] = len(s.LongOrders) + } + pairLock.Unlock() + if allow { + s.OpenOrder(&strat.EnterReq{ + Tag: fmt.Sprintf("%v", state.odNum), + CostRate: amt, + }) + } + } + state.odNum += 1 + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + score := float64(0) + if ta.RSI(e.Close, 14).Get(0) < 30 { + score += 1 + } + roc := ta.ROC(e.Close, 20).Get(0) + if roc < -20 { + score += 2 + } else if roc < -10 { + score += 1 + } + plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + if minusDi.Get(0) > plusDi.Get(0) { + if adx > 40 { + score += 2 + } else if adx > 25 { + score += 1 + } + } + ma7 := ta.SMA(e.Close, 7).Get(0) + ma20 := ta.SMA(e.Close, 20) + ma20V := ma20.Get(0) + ma99 := ta.SMA(e.Close, 99).Get(0) + if ma99 > ma20V { + score += 1 + } + state, _ := s.More.(*State1525Sim) + state.score = score + state.upFlag = 0 + if ma20V > ma99 && ma20V > ma20.Get(2) { + state.upFlag = 1 + } else if ma7 < min(ma20V, ma99) { + state.upFlag = -1 + } + }, + OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) { + jobNum := len(jobs) + if jobNum < 3 { + return + } + downNum, downHeavyNum, sumUp := float64(0), float64(0), float64(0) + var last *strat.JobEnv + for _, j := range jobs { + state, _ := j.Job.More.(*State1525Sim) + if state.score >= 5 { + downHeavyNum += 1 + } + if state.score >= 3 { + downNum += 1 + } + sumUp += max(0, state.upFlag) + last = j + } + downRate := downNum / float64(len(jobs)) + downHeavyRate := downHeavyNum / float64(len(jobs)) + sumUp /= float64(len(jobs)) + if downHeavyRate >= 0.8 { + // 极端下跌 + targetPerc = targetPercRaw * 2 + takeProfit = takeProfitRaw * 0.5 + } else if downRate >= 0.85 && downHeavyRate >= 0.6 { + // 严重下跌 + targetPerc = targetPercRaw * 1.8 + takeProfit = takeProfitRaw * 0.5 + } else if downRate >= 0.7 && downRate <= 0.85 { + // 市场中度下跌 + targetPerc = targetPercRaw * 1.6 + takeProfit = takeProfitRaw * 0.65 + } else if downRate >= 0.5 && downRate <= 0.7 { + // 轻度下跌 + targetPerc = targetPercRaw * 1.3 + takeProfit = takeProfitRaw * 0.8 + } else { + // 轻度或未下跌 + targetPerc = targetPercRaw + takeProfit = takeProfitRaw + } + // 调整最小开仓级别 + if sumUp > 0.7 { + startLvl = startLvlRaw + } else if sumUp > 0.5 && downRate <= 0.5 { + startLvl = startLvlRaw + 2 + } else if sumUp > 0.3 && downRate <= 0.7 { + startLvl = startLvlRaw + 4 + } else { + startLvl = startLvlRaw + 6 + } + pairLock.Lock() + totNum := 0 + for _, num := range openPairs { + totNum += num + } + pairLock.Unlock() + if totNum > 0 { + last.Job.Strat.WriteOutput(fmt.Sprintf("pairNum: %v", totNum), true) + } + }, + } +} diff --git a/llmjob/1569.go b/llmjob/1569.go new file mode 100644 index 0000000..b8674c9 --- /dev/null +++ b/llmjob/1569.go @@ -0,0 +1,105 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J1569(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastPeriod := pol.DefInt("fastPeriod", 10, core.PNorm(5, 20)) + smaRatio := pol.Def("smaRatio", 2.0, core.PNorm(1.5, 4.0)) + supportResistancePeriod := pol.DefInt("supportResistancePeriod", 12, core.PNorm(5, 20)) + confirmPeriod := pol.DefInt("confirmPeriod", 3, core.PNorm(2, 5)) + riskRewardRatio := pol.Def("riskRewardRatio", 2.2, core.PNorm(1.5, 3.0)) + + // 计算慢速SMA周期 + slowPeriod := int(float64(fastPeriod) * smaRatio) + + // 确保预热K线数量足够计算所有指标 + warmupNum := slowPeriod + if supportResistancePeriod > warmupNum { + warmupNum = supportResistancePeriod + } + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多1个多头订单 + EachMaxShort: 1, // 最多1个空头订单 + RunTimeFrames: []string{"1h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算移动平均线 + smaFast := ta.SMA(e.Close, fastPeriod) + smaSlow := ta.SMA(e.Close, slowPeriod) + + // 计算支撑阻力位(过去10根K线的最高价和最低价) + recentLow := ta.Lowest(e.Low, supportResistancePeriod) + recentHigh := ta.Highest(e.High, supportResistancePeriod) + + // 计算确认条件所需的指标 + highestSmaSlow := ta.Highest(smaSlow, confirmPeriod) + lowestSmaSlow := ta.Lowest(smaSlow, confirmPeriod) + lowestLow3 := ta.Lowest(e.Low, confirmPeriod) + highestHigh3 := ta.Highest(e.High, confirmPeriod) + + // 获取当前值 + closePrice := e.Close.Get(0) + smaSlowVal := smaSlow.Get(0) + recentLowVal := recentLow.Get(0) + recentHighVal := recentHigh.Get(0) + highestSmaSlowVal := highestSmaSlow.Get(0) + lowestSmaSlowVal := lowestSmaSlow.Get(0) + lowestLow3Val := lowestLow3.Get(0) + highestHigh3Val := highestHigh3.Get(0) + + // 生成交易信号 - 只计算一次交叉信号 + crossSignal := smaFast.Cross(smaSlow) + bullishCross := crossSignal == 1 // 10周期SMA上穿20周期SMA + bearishCross := crossSignal == -1 // 10周期SMA下穿20周期SMA + breakoutUp := closePrice > highestSmaSlowVal // 收盘价突破过去3根K线的20周期SMA最高点 + breakoutDown := closePrice < lowestSmaSlowVal // 收盘价跌破过去3根K线的20周期SMA最低点 + retestUp := lowestLow3Val > smaSlowVal // 过去3根K线的最低价保持在20周期SMA之上 + retestDown := highestHigh3Val < smaSlowVal // 过去3根K线的最高价保持在20周期SMA之下 + + // 三重确认条件 + buyCondition := bullishCross && breakoutUp && retestUp + sellCondition := bearishCross && breakoutDown && retestDown + + // 计算止损止盈 + longSL := recentLowVal // 买入止损设为过去10根K线的最低价 + shortSL := recentHighVal // 卖出止损设为过去10根K线的最高价 + + riskSizeLong := closePrice - longSL // 买入交易的风险大小 + riskSizeShort := shortSL - closePrice // 卖出交易的风险大小 + + longTP := closePrice + (riskSizeLong * riskRewardRatio) // 买入交易的止盈价格 + shortTP := closePrice - (riskSizeShort * riskRewardRatio) // 卖出交易的止盈价格 + + // 执行交易 + if buyCondition && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + OrderType: core.OrderTypeMarket, + StopLoss: longSL, + TakeProfit: longTP, + }) + } + + if sellCondition && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + OrderType: core.OrderTypeMarket, + StopLoss: shortSL, + TakeProfit: shortTP, + }) + } + }, + } +} diff --git a/llmjob/1662.go b/llmjob/1662.go new file mode 100644 index 0000000..2d1ba4a --- /dev/null +++ b/llmjob/1662.go @@ -0,0 +1,197 @@ +// 修复后的完整策略代码 +// 请勿修改package包名和任意函数名 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +因止盈止损触发价未检查是否合理的bug导致虚假净值曲线 +*/ + +// SuperTrendData 存储每个品种的Supertrend相关数据 +type SuperTrendData struct { + trend int // 当前趋势方向,1表示上升趋势,-1表示下降趋势 + longTrailStop float64 // 多头仓位的移动止损价格 + shortTrailStop float64 // 空头仓位的移动止损价格 + up float64 // 上轨值 + dn float64 // 下轨值 +} + +func J1662(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + atrPeriod := pol.DefInt("atrPeriod", 8, core.PNorm(5, 30)) + atrMultiplier := pol.Def("atrMultiplier", 2.5, core.PNorm(1.5, 5.0)) + + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + return &strat.TradeStrat{ + WarmupNum: baseAtrLen, // 预热K线数量,确保有足够数据计算指标 + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的Supertrend数据 + s.More = &SuperTrendData{ + trend: 1, // 初始趋势设为上升 + longTrailStop: math.NaN(), + shortTrailStop: math.NaN(), + up: math.NaN(), + dn: math.NaN(), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + data, _ := s.More.(*SuperTrendData) + + // 计算数据源 (hl2 = (high + low) / 2) + src := ta.HL2(e.High, e.Low) + + // 计算ATR值 + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + + // 计算Supertrend指标 + upVal := src.Get(0) - atr.Get(0)*atrMultiplier + dnVal := src.Get(0) + atr.Get(0)*atrMultiplier + + atr2 := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr2, lenAtr), baseAtrLen).Get(0) + + // 获取历史值 + up1 := data.up + if math.IsNaN(up1) { + up1 = upVal + } + dn1 := data.dn + if math.IsNaN(dn1) { + dn1 = dnVal + } + + // 更新上轨和下轨 + close1 := e.Close.Get(1) + if !math.IsNaN(close1) { + if close1 > up1 { + upVal = math.Max(upVal, up1) + } + if close1 < dn1 { + dnVal = math.Min(dnVal, dn1) + } + } + + // 保存当前上下轨值 + data.up = upVal + data.dn = dnVal + + // 判断趋势方向 + prevTrend := data.trend + closePrice := e.Close.Get(0) + if !math.IsNaN(closePrice) && !math.IsNaN(dn1) && !math.IsNaN(up1) && !math.IsNaN(atrBase) { + if data.trend == -1 && closePrice > dn1 { + data.trend = 1 // 趋势由下降转为上升 + } else if data.trend == 1 && closePrice < up1 { + data.trend = -1 // 趋势由上升转为下降 + } + } + + // 生成交易信号 + buySignal := data.trend == 1 && prevTrend == -1 + sellSignal := data.trend == -1 && prevTrend == 1 + + // 执行交易策略 + if buySignal { + // 开仓多头,使用下轨作为触发价格 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Stop: dnVal, + Log: true, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "XShort", + Dirt: core.OdDirtShort, + Log: true, + }) + } + + if sellSignal { + // 开仓空头,使用上轨作为触发价格 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + Stop: upVal, + Log: true, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "XLong", + Dirt: core.OdDirtLong, + Log: true, + }) + } + + pipValue := atrBase * 0 + trailingValue := atrBase * 0 + + // 移动止损逻辑 + if s.OrderNum > 0 { + // 多头移动止损 + if len(s.LongOrders) > 0 { + // 获取持仓均价 + avgPrice := s.PositionAvgPrice(core.OdDirtLong, "") + + if avgPrice > 0 { + if math.IsNaN(data.longTrailStop) || closePrice > avgPrice+trailingValue { + data.longTrailStop = e.High.Get(0) - trailingValue + } + + // 计算初始止损 + longStopLevel := upVal - pipValue + // 使用更严格的止损(更接近当前价格的止损) + finalStop := math.Max(data.longTrailStop, longStopLevel) + if finalStop >= closePrice { + s.CloseOrders(&strat.ExitReq{ + Tag: "SLLong", + Dirt: core.OdDirtLong, + }) + } else { + s.SetAllStopLoss(core.OdDirtLong, &ormo.ExitTrigger{ + Price: finalStop, + Tag: "sl", + }) + } + } + } + + // 空头移动止损 + if len(s.ShortOrders) > 0 { + // 获取持仓均价 + avgPrice := s.PositionAvgPrice(core.OdDirtShort, "") + + if avgPrice > 0 { + if math.IsNaN(data.shortTrailStop) || closePrice < avgPrice-trailingValue { + data.shortTrailStop = e.Low.Get(0) + trailingValue + } + + // 计算初始止损 + shortStopLevel := dnVal + pipValue + // 使用更严格的止损(更接近当前价格的止损) + finalStop := math.Min(data.shortTrailStop, shortStopLevel) + if finalStop < closePrice { + s.CloseOrders(&strat.ExitReq{ + Tag: "SLShort", + Dirt: core.OdDirtShort, + }) + } else { + s.SetAllStopLoss(core.OdDirtShort, &ormo.ExitTrigger{ + Price: finalStop, + Tag: "sl", + }) + } + } + } + } + }, + } +} diff --git a/llmjob/1665.go b/llmjob/1665.go new file mode 100644 index 0000000..663d222 --- /dev/null +++ b/llmjob/1665.go @@ -0,0 +1,278 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// StrategyState 保存每个品种的交易状态 +type StrategyState struct { + isLong bool // 当前是否持有多头仓位 + isShort bool // 当前是否持有空头仓位 + partialTpTriggered bool // 第一止盈是否已触发 + stopPrice float64 // 止损价格 + firstTpPrice float64 // 第一止盈价格 + secondTpPrice float64 // 第二止盈价格 + firstTpQty float64 // 第一止盈数量 + positionSize float64 // 仓位大小 + entryPrice float64 // 入场价格 +} + +func J1665(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastLength := pol.DefInt("fastLength", 5, core.PNorm(3, 15)) + emaRate := pol.Def("emaRate", 3.0, core.PNorm(1.5, 6.0)) + rsiLength := pol.DefInt("rsiLength", 7, core.PNorm(3, 21)) + atrPeriod := pol.DefInt("atrPeriod", 10, core.PNorm(5, 20)) + stopLossRate := pol.Def("stopLossRate", 5.0, core.PNorm(2.0, 10.0)) + firstTakeProfitRate := pol.Def("firstTakeProfitRate", 8.0, core.PNorm(4.0, 16.0)) + secondTakeProfitRate := pol.Def("secondTakeProfitRate", 12.0, core.PNorm(6.0, 24.0)) + firstTakeProfitQtyRate := pol.Def("firstTakeProfitQtyRate", 50.0, core.PNorm(25.0, 75.0)) + leverageRate := pol.Def("leverageRate", 3.0, core.PNorm(1.0, 5.0)) + + // 计算慢速EMA长度 + slowLength := int(float64(fastLength) * emaRate) + + return &strat.TradeStrat{ + WarmupNum: slowLength + 26, // 确保有足够数据计算所有指标 + HedgeOff: true, // 单向持仓 + EachMaxLong: 1, // 最多一个多单 + EachMaxShort: 1, // 最多一个空单 + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + s.More = &StrategyState{ + isLong: false, + isShort: false, + partialTpTriggered: false, + stopPrice: math.NaN(), + firstTpPrice: math.NaN(), + secondTpPrice: math.NaN(), + firstTpQty: math.NaN(), + positionSize: math.NaN(), + entryPrice: math.NaN(), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*StrategyState) + + // 计算技术指标 + fastEMA := ta.EMA(e.Close, fastLength) + slowEMA := ta.EMA(e.Close, slowLength) + rsi := ta.RSI(e.Close, rsiLength) + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + macd, signal := ta.MACD(e.Close, 12, 26, 9) + + // 获取当前值 + closePrice := e.Close.Get(0) + prevClosePrice := e.Close.Get(1) + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + if math.IsNaN(closePrice) { + return + } + + // 检测MACD交叉 + macdCross := macd.Cross(signal) + macdGolden := macdCross == 1 // MACD金叉 + macdDeath := macdCross == -1 // MACD死叉 + + // 生成交易信号 + longCondition := fastEMA.Get(0) > slowEMA.Get(0) && macdGolden && rsi.Get(0) < 70 + shortCondition := fastEMA.Get(0) < slowEMA.Get(0) && macdDeath && rsi.Get(0) > 30 + + // 平仓信号 + exitLong := shortCondition || fastEMA.Get(0) < slowEMA.Get(0) + exitShort := longCondition || fastEMA.Get(0) > slowEMA.Get(0) + + // 计算仓位大小 + if !math.IsNaN(atr.Get(0)) && atr.Get(0) > 0 { + maxRiskPerTrade := 0.05 + // 修复错误1:使用动态账户权益而非固定值1000 + stakeAmount := s.Strat.GetStakeAmount(s) + basePosition := stakeAmount * maxRiskPerTrade + atrAmount := atr.Get(0) * closePrice + riskPosition := basePosition / atrAmount * leverageRate + maxPosition := stakeAmount * 0.4 / closePrice + state.positionSize = math.Min(riskPosition, maxPosition) + // 修复错误6:将firstTpQty的初始化移到仓位计算函数内部 + state.firstTpQty = state.positionSize * (firstTakeProfitQtyRate / 100.0) + } + + // 多头入场 + if longCondition && !state.isLong && !state.isShort && !math.IsNaN(state.positionSize) { + state.entryPrice = closePrice + state.stopPrice = closePrice * (1 - stopLossRate/100) + state.firstTpPrice = closePrice * (1 + firstTakeProfitRate/100) + state.secondTpPrice = closePrice * (1 + secondTakeProfitRate/100) + + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + Amount: state.positionSize, + StopLoss: state.stopPrice, + StopLossTag: "stop_loss", + }) + state.isLong = true + state.partialTpTriggered = false + } + + // 空头入场 + if shortCondition && !state.isShort && !state.isLong && !math.IsNaN(state.positionSize) { + state.entryPrice = closePrice + state.stopPrice = closePrice * (1 + stopLossRate/100) + state.firstTpPrice = closePrice * (1 - firstTakeProfitRate/100) + state.secondTpPrice = closePrice * (1 - secondTakeProfitRate/100) + + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + Amount: state.positionSize, + StopLoss: state.stopPrice, + StopLossTag: "stop_loss", + }) + state.isShort = true + state.partialTpTriggered = false + } + + // 修复错误2:在每个K线更新时重新计算止盈止损价格 + if state.isLong { + state.stopPrice = state.entryPrice * (1 - stopLossRate/100) + state.firstTpPrice = state.entryPrice * (1 + firstTakeProfitRate/100) + if !state.partialTpTriggered { + state.secondTpPrice = state.entryPrice * (1 + secondTakeProfitRate/100) + } + + // 修复错误3:在触发第一止盈时立即调整第二止盈价格 + if !state.partialTpTriggered && (!math.IsNaN(prevClosePrice) && prevClosePrice >= state.firstTpPrice || highPrice >= state.firstTpPrice) { + state.partialTpTriggered = true + state.secondTpPrice = highPrice * (1 + 0.04) // 基于当前最高价再上涨4% + } + } + + if state.isShort { + state.stopPrice = state.entryPrice * (1 + stopLossRate/100) + state.firstTpPrice = state.entryPrice * (1 - firstTakeProfitRate/100) + if !state.partialTpTriggered { + state.secondTpPrice = state.entryPrice * (1 - secondTakeProfitRate/100) + } + + // 修复错误3:在触发第一止盈时立即调整第二止盈价格 + if !state.partialTpTriggered && (!math.IsNaN(prevClosePrice) && prevClosePrice <= state.firstTpPrice || lowPrice <= state.firstTpPrice) { + state.partialTpTriggered = true + state.secondTpPrice = lowPrice * (1 - 0.04) // 基于当前最低价再下跌4% + } + } + + // 多头平仓条件 + if exitLong && state.isLong { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_long", + Dirt: core.OdDirtLong, + }) + state.isLong = false + state.partialTpTriggered = false + } + + // 空头平仓条件 + if exitShort && state.isShort { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_short", + Dirt: core.OdDirtShort, + }) + state.isShort = false + state.partialTpTriggered = false + } + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + state, _ := s.More.(*StrategyState) + prevClosePrice := s.Env.Close.Get(1) + highPrice := s.Env.High.Get(0) + lowPrice := s.Env.Low.Get(0) + + if od.Short { + // 空仓管理 + if state.isShort { + // 修复错误7:使用前一根K线的收盘价和当前K线的最高价/最低价检查止损 + if (!math.IsNaN(prevClosePrice) && prevClosePrice >= state.stopPrice) || highPrice >= state.stopPrice { + return &strat.ExitReq{ + Tag: "short_stop_loss", + OrderID: od.ID, + } + } + + // 修复错误7:使用前一根K线的收盘价和当前K线的最低价检查第一止盈 + if !state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice <= state.firstTpPrice) || lowPrice <= state.firstTpPrice) { + return &strat.ExitReq{ + Tag: "short_first_take_profit", + OrderID: od.ID, + Amount: state.firstTpQty, + } + } + + // 修复错误7:使用前一根K线的收盘价和当前K线的最低价检查第二止盈 + if state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice <= state.secondTpPrice) || lowPrice <= state.secondTpPrice) { + state.isShort = false + state.partialTpTriggered = false + return &strat.ExitReq{ + Tag: "short_second_take_profit", + OrderID: od.ID, + } + } + } + } else { + // 多仓管理 + if state.isLong { + // 修复错误7:使用前一根K线的收盘价和当前K线的最低价检查止损 + if (!math.IsNaN(prevClosePrice) && prevClosePrice <= state.stopPrice) || lowPrice <= state.stopPrice { + return &strat.ExitReq{ + Tag: "long_stop_loss", + OrderID: od.ID, + } + } + + // 修复错误7:使用前一根K线的收盘价和当前K线的最高价检查第一止盈 + if !state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice >= state.firstTpPrice) || highPrice >= state.firstTpPrice) { + return &strat.ExitReq{ + Tag: "long_first_take_profit", + OrderID: od.ID, + Amount: state.firstTpQty, + } + } + + // 修复错误7:使用前一根K线的收盘价和当前K线的最高价检查第二止盈 + if state.partialTpTriggered && ((!math.IsNaN(prevClosePrice) && prevClosePrice >= state.secondTpPrice) || highPrice >= state.secondTpPrice) { + state.isLong = false + state.partialTpTriggered = false + return &strat.ExitReq{ + Tag: "long_second_take_profit", + OrderID: od.ID, + } + } + } + } + + return nil + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + state, _ := s.More.(*StrategyState) + + // 订单状态变化时更新策略状态 + if chgType == strat.OdChgExitFill { + if od.Short { + // 空单完全平仓 + state.isShort = false + state.partialTpTriggered = false + } else { + // 多单完全平仓 + state.isLong = false + state.partialTpTriggered = false + } + } + }, + } +} diff --git a/llmjob/199.go b/llmjob/199.go new file mode 100644 index 0000000..f899bcf --- /dev/null +++ b/llmjob/199.go @@ -0,0 +1,116 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J199(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + emaShortPeriod := pol.DefInt("emaShortPeriod", 12, core.PNorm(5, 20)) + emaPeriodRatio := pol.Def("emaPeriodRatio", 2.5, core.PNorm(1.5, 4)) + rsiOverbought := pol.DefInt("rsiOverbought", 70, core.PNorm(60, 85)) + rsiOversold := pol.DefInt("rsiOversold", 30, core.PNorm(15, 40)) + + // 计算长期EMA周期 + emaLongPeriod := int(float64(emaShortPeriod) * emaPeriodRatio) + + // MACD参数 + macdFastLength := 12 + macdSlowLength := 26 + macdSignalSmoothing := 9 + + // RSI参数 + rsiLength := 14 + + // 成交量移动平均周期 + volumeMAPeriod := 20 + + // 计算预热数量,取所有指标中最大的周期 + warmupNum := emaLongPeriod + if macdSlowLength > warmupNum { + warmupNum = macdSlowLength + } + if rsiLength > warmupNum { + warmupNum = rsiLength + } + if volumeMAPeriod > warmupNum { + warmupNum = volumeMAPeriod + } + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"4h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + emaShort := ta.EMA(e.Close, emaShortPeriod) + emaLong := ta.EMA(e.Close, emaLongPeriod) + + macdLine, signalLine := ta.MACD(e.Close, macdFastLength, macdSlowLength, macdSignalSmoothing) + + rsi := ta.RSI(e.Close, rsiLength) + + volumeMA := ta.SMA(e.Volume, volumeMAPeriod) + + // 获取当前价格 + closePrice := e.Close.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) { + return + } + + // 生成交易信号 - 修复重复计算EMA交叉的问题 + emaCross := emaShort.Cross(emaLong) // 只计算一次EMA交叉 + emaCrossUp := emaCross == 1 // 短期EMA上穿长期EMA + emaCrossDown := emaCross == -1 // 短期EMA下穿长期EMA + macdBullish := macdLine.Get(0) > signalLine.Get(0) // MACD多头信号 + macdBearish := macdLine.Get(0) < signalLine.Get(0) // MACD空头信号 + rsiNotOverbought := rsi.Get(0) < float64(rsiOverbought) // RSI未超买 + rsiNotOversold := rsi.Get(0) > float64(rsiOversold) // RSI未超卖 + volumeHigh := e.Volume.Get(0) > volumeMA.Get(0) // 成交量高于平均 + + // 交易条件判断 + buyCondition := emaCrossUp && macdBullish && rsiNotOverbought && volumeHigh + sellCondition := emaCrossDown && macdBearish && rsiNotOversold && volumeHigh + + // 计算止损止盈价格 - 修复为使用固定比例,与原始策略一致 + //longStopLoss := closePrice * 0.95 + //longTakeProfit := closePrice * 1.05 + shortStopLoss := closePrice * 1.05 + shortTakeProfit := closePrice * 0.95 + + // 执行交易 + _ = buyCondition + //if buyCondition && len(s.LongOrders) == 0 { + // s.OpenOrder(&strat.EnterReq{ + // Tag: "Buy", + // OrderType: core.OrderTypeMarket, + // StopLoss: longStopLoss, + // TakeProfit: longTakeProfit, + // StopLossTag: "Stop Loss", + // TakeProfitTag: "Take Profit", + // }) + //} + + if sellCondition && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + OrderType: core.OrderTypeMarket, + StopLoss: shortStopLoss, + TakeProfit: shortTakeProfit, + StopLossTag: "Stop Loss", + TakeProfitTag: "Take Profit", + }) + } + }, + } +} diff --git a/llmjob/2251.go b/llmjob/2251.go new file mode 100644 index 0000000..01a7b74 --- /dev/null +++ b/llmjob/2251.go @@ -0,0 +1,68 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J2251(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastPeriod := pol.DefInt("fastPeriod", 21, core.PNorm(5, 50)) + slowPeriod := pol.DefInt("slowPeriod", 55, core.PNorm(30, 100)) + slMultiplier := pol.Def("slMultiplier", 1.5, core.PNorm(0.8, 3)) + tpMultiplier := pol.Def("tpMultiplier", 1, core.PNorm(0.5, 2.5)) + atrLength := pol.DefInt("atrLength", 14, core.PNorm(7, 30)) + + return &strat.TradeStrat{ + WarmupNum: slowPeriod, // 预热数量取较长的EMA周期 + HedgeOff: true, // 单向持仓模式,与PineScript一致 + RunTimeFrames: []string{"4h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + fastMA := ta.EMA(e.Close, fastPeriod) + slowMA := ta.EMA(e.Close, slowPeriod) + atr := ta.ATR(e.High, e.Low, e.Close, atrLength) + + // 获取最新值 + atrVal := atr.Get(0) + closePrice := e.Close.Get(0) + + // 检测EMA交叉信号 + //goLong := fastMA.Cross(slowMA) == 1 + goShort := fastMA.Cross(slowMA) == -1 + + // 金叉信号:做多 + //if goLong && s.OrderNum == 0 { + // // 计算止损和止盈价格 + // slPrice := closePrice - atrVal*slMultiplier + // tpPrice := closePrice + atrVal*tpMultiplier*2 + // s.OpenOrder(&strat.EnterReq{ + // Tag: "Long", + // Short: false, + // Limit: closePrice - atrVal*slMultiplier*0.7, + // StopBars: 7, + // StopLoss: slPrice, // 做多止损价格 + // TakeProfit: tpPrice, // 做多止盈价格 + // }) + //} + + // 死叉信号:做空 + if goShort && s.OrderNum == 0 { + // 计算止损和止盈价格 + slPrice := closePrice + atrVal*slMultiplier + tpPrice := closePrice - atrVal*tpMultiplier + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + StopLoss: slPrice, // 做空止损价格 + TakeProfit: tpPrice, // 做空止盈价格 + }) + } + }, + } +} diff --git a/llmjob/2391.go b/llmjob/2391.go new file mode 100644 index 0000000..83e4e56 --- /dev/null +++ b/llmjob/2391.go @@ -0,0 +1,174 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +// Sta2391 用于保存每个品种的策略状态 +type Sta2391 struct { + posReversal123 float64 // 123反转策略的信号值 + posmStochCross float64 // 随机指标交叉策略的信号值 +} + +func J2391(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 123反转策略参数 + length := pol.DefInt("length", 40, core.PNorm(5, 30)) + kSmoothing := pol.DefInt("kSmoothing", 1, core.PNorm(1, 5)) + dLength := pol.DefInt("dLength", 3, core.PNorm(1, 5)) + level := pol.Def("level", 30, core.PNorm(30, 70)) + + // 随机指标交叉策略参数 + lengthSC := pol.DefInt("lengthSC", 20, core.PNorm(3, 21)) + dLengthSC := pol.DefInt("dLengthSC", 3, core.PNorm(1, 5)) + oversold := pol.Def("oversold", 10, core.PNorm(10, 30)) + overbought := pol.Def("overbought", 90, core.PNorm(70, 90)) + reverse := pol.Def("reverse", 0, core.PUniform(0, 1)) // 0表示不反转,1表示反转 + + // 计算预热数量,取两个策略中需要的历史数据最大值 + warmupNum := length + if lengthSC > warmupNum { + warmupNum = lengthSC + } + if kSmoothing+dLength > warmupNum { + warmupNum = kSmoothing + dLength + } + if dLengthSC > warmupNum { + warmupNum = dLengthSC + } + // 额外需要2个历史K线判断价格反转 + warmupNum += 2 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + TimeFrames: "8h", + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + s.More = &Sta2391{ + posReversal123: 0, + posmStochCross: 0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*Sta2391) + + // 获取价格数据 + close0 := e.Close.Get(0) // 当前收盘价 + close1 := e.Close.Get(1) // 前一个收盘价 + close2 := e.Close.Get(2) // 前两个收盘价 + + // 1. 计算123反转策略信号 + // 计算随机指标 + kFast := ta.Stoch(e.High, e.Low, e.Close, length) + // 对K值进行平滑 + vFast := ta.SMA(kFast, kSmoothing) + // 计算D值 + vSlow := ta.SMA(vFast, dLength) + + // 获取当前值 + vFastVal := vFast.Get(0) + vSlowVal := vSlow.Get(0) + + // 判断123反转策略信号 + if close2 < close1 && close1 < close0 && vFastVal < vSlowVal && vFastVal > level { + // 买入信号:两天内收盘价从低点转为高点,且随机指标快速线在慢速线下方但高于阈值 + state.posReversal123 = 1 + } else if close2 > close1 && close1 > close0 && vFastVal > vSlowVal && vFastVal < level { + // 卖出信号:两天内收盘价从高点转为低点,且随机指标快速线在慢速线上方但低于阈值 + state.posReversal123 = -1 + } + // 否则保持之前的信号(不需要else,因为不改变state.posReversal123的值) + + // 2. 计算随机指标交叉策略信号 + // 计算随机指标 + kFastSC := ta.Stoch(e.High, e.Low, e.Close, lengthSC) + // 计算D值 + vSlowSC := ta.SMA(kFastSC, dLengthSC) + + // 获取当前值 + vFastSCVal := kFastSC.Get(0) + vSlowSCVal := vSlowSC.Get(0) + + // 判断随机指标交叉策略信号 + if vFastSCVal >= vSlowSCVal && vFastSCVal < oversold && vSlowSCVal < oversold { + // 买入信号:快速线在慢速线上方,且两者都处于超卖区域 + state.posmStochCross = 1 + } else if vFastSCVal < vSlowSCVal && vFastSCVal > overbought && vSlowSCVal > overbought { + // 卖出信号:快速线在慢速线下方,且两者都处于超买区域 + state.posmStochCross = -1 + } + // 否则保持之前的信号(不需要else,因为不改变state.posmStochCross的值) + + // 3. 组合信号 + var pos float64 + if state.posReversal123 == 1 && state.posmStochCross == 1 { + // 两个策略都给出买入信号 + pos = 1 + } else if state.posReversal123 == -1 && state.posmStochCross == -1 { + // 两个策略都给出卖出信号 + pos = -1 + } else { + // 信号不一致,无交易信号 + pos = 0 + } + + // 4. 处理信号反转 + var possig float64 + if reverse == 1 { // 明确检查reverse是否为1 + if pos == 1 { + possig = -1 // 反转买入信号为卖出信号 + } else if pos == -1 { + possig = 1 // 反转卖出信号为买入信号 + } else { + possig = 0 + } + } else { + possig = pos // 不反转信号 + } + + // 5. 执行交易 + if possig == 1 { + // 买入信号 + if len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + // 如果有做空持仓,平仓 + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "close_short", + Dirt: core.OdDirtShort, + }) + } + } else if possig == -1 { + // 卖出信号 + if len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + }) + } + // 如果有做多持仓,平仓 + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "close_long", + Dirt: core.OdDirtLong, + }) + } + } else if possig == 0 { + // 无信号,平仓所有持仓 + if len(s.LongOrders) > 0 || len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "close_all", + Dirt: core.OdDirtBoth, + }) + } + } + }, + } +} diff --git a/llmjob/2439.go b/llmjob/2439.go new file mode 100644 index 0000000..4ddfb10 --- /dev/null +++ b/llmjob/2439.go @@ -0,0 +1,144 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "math" + "time" +) + +type WeekendData struct { + FridayClose float64 + HasLong bool + TotCost float64 + TotAmt float64 +} + +func J2439(pol *config.RunPolicyConfig) *strat.TradeStrat { + entryThreshold := pol.Def("entryThreshold", 0.02, core.PNorm(0.01, 0.1)) + profitTakingPercentThreshold := pol.Def("profitTakingPercentThreshold", 0.06, core.PNorm(0.01, 0.08)) + + return &strat.TradeStrat{ + WarmupNum: 7, // 需要最多7天的数据来获取周五收盘价 + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"5m"}, + OnStartUp: func(s *strat.StratJob) { + s.More = &WeekendData{FridayClose: math.NaN()} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + data, _ := s.More.(*WeekendData) + currentTime := time.Unix(e.TimeStop/1000, 0) + weekday := currentTime.Weekday() + + // 计算距离周五的天数 + var daysSinceFriday int + switch weekday { + case time.Saturday: + daysSinceFriday = 1 + case time.Sunday: + daysSinceFriday = 0 + case time.Monday: + daysSinceFriday = 6 + case time.Tuesday: + daysSinceFriday = 5 + case time.Wednesday: + daysSinceFriday = 4 + case time.Thursday: + daysSinceFriday = 3 + case time.Friday: + daysSinceFriday = 2 + } + + // 获取周五收盘价 - 使用日线数据 + if e.Close.Len() > daysSinceFriday { + data.FridayClose = e.Close.Get(daysSinceFriday) + } + + // 如果没有有效的周五收盘价,不执行交易逻辑 + if math.IsNaN(data.FridayClose) { + return + } + + // 检查止盈条件 + if s.EnteredNum > 0 { + totalProfitRate := 0.0 + for _, od := range s.LongOrders { + if od.Status >= ormo.InOutStatusPartEnter { + totalProfitRate += od.ProfitRate + } + } + for _, od := range s.ShortOrders { + if od.Status >= ormo.InOutStatusPartEnter { + totalProfitRate += od.ProfitRate + } + } + + if totalProfitRate > profitTakingPercentThreshold { + s.CloseOrders(&strat.ExitReq{ + Tag: "profit_take", + Dirt: core.OdDirtBoth, + }) + return + } + } + + // 周末交易逻辑(周日和周一) + if weekday == time.Sunday || weekday == time.Monday { + currentPrice := e.Close.Get(0) + if math.IsNaN(currentPrice) { + return + } + + upperThreshold := data.FridayClose * (1 + entryThreshold) + lowerThreshold := data.FridayClose * (1 - entryThreshold) + + // 没有持仓时的入场逻辑 + if s.OrderNum == 0 { + if currentPrice > upperThreshold { + s.OpenOrder(&strat.EnterReq{ + Tag: "short_entry", + Short: true, + }) + } else if currentPrice < lowerThreshold { + s.OpenOrder(&strat.EnterReq{ + Tag: "long_entry", + }) + } + } else { + // 已有持仓时的加仓逻辑 + for _, od := range s.ShortOrders { + if od.Status >= ormo.InOutStatusPartEnter && currentPrice > od.InitPrice*(1+entryThreshold) { + s.OpenOrder(&strat.EnterReq{ + Tag: "add_short", + Short: true, + }) + break + } + } + + for _, od := range s.LongOrders { + if od.Status >= ormo.InOutStatusPartEnter { + s.OpenOrder(&strat.EnterReq{ + Tag: "add_long", + }) + break + } + } + } + } + + // 周二平仓逻辑 + if weekday == time.Tuesday { + if s.OrderNum > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "monday_close", + Dirt: core.OdDirtBoth, + }) + } + } + }, + } +} diff --git a/llmjob/2523.go b/llmjob/2523.go new file mode 100644 index 0000000..bffb5d7 --- /dev/null +++ b/llmjob/2523.go @@ -0,0 +1,103 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +以大止损实现的高胜率,订单太少 +尝试使用atrBase针对CostRate、TakeProfit、StopLoss进行自适应,效果都更差。 +*/ + +func J2523(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + maLen := pol.DefInt("maLen", 240, core.PNorm(50, 300)) + adxThres := pol.Def("adxThres", 20, core.PNorm(15, 35)) + slRate := pol.Def("slRate", 2.5, core.PNorm(0.8, 5.0)) + + // 计算预热K线数,确保所有指标都能正确计算 + warmupNum := maLen + 20 + + //rates := make([]float64, 0, 10) + return &strat.TradeStrat{ + WarmupNum: warmupNum, + RunTimeFrames: []string{"3d"}, + StakeRate: 20, + EachMaxLong: 1, + EachMaxShort: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + maBig := ta.SMA(e.Close, maLen) + rsi := ta.RSI(e.Close, 14) + // 使用DMI计算ADX,与原始策略保持一致 + adx := ta.ADX(e.High, e.Low, e.Close, 14) + atr := ta.ATR(e.High, e.Low, e.Close, 14) + + // 获取当前值和历史值,避免重复计算 + curClose := e.Close.Get(0) + curMA := maBig.Get(0) + curRSI := rsi.Get(0) + curADX := adx.Get(0) + curATR := atr.Get(0) + + // 检查数据有效性 + if math.IsNaN(curClose) || math.IsNaN(curMA) || math.IsNaN(curRSI) || + math.IsNaN(curADX) || math.IsNaN(curATR) { + return + } + + // 检查2根K线前是否发生交叉 + closeXBig := e.Close.Cross(maBig) + rsi2 := rsi.Get(2) + adxOk := adx.Get(2) > adxThres + buyConfirmation := closeXBig == 3 && (rsi2 > 40) && adxOk + sellConfirmation := closeXBig == -3 && (rsi2 < 60) && adxOk + + // 执行交易逻辑 + if buyConfirmation { + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + TakeProfitVal: curClose * 0.02, // 2%止盈 + StopLossVal: slRate * curATR, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "XShort", + Dirt: core.OdDirtShort, + }) + } + + if sellConfirmation { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + TakeProfitVal: curClose * 0.02, // 2%止盈 + StopLossVal: slRate * curATR, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "XLong", + Dirt: core.OdDirtLong, + }) + } + }, + //OnShutDown: func(s *strat.StratJob) { + // if len(rates) == 0 { + // return + // } + // sort.Float64s(rates) + // size := len(rates) / 10 + // for i := 0; i < len(rates); i += size { + // block := rates[i:min(len(rates), i+size)] + // avg := floats.Sum(block) / float64(len(block)) + // fmt.Printf("rate %v: %.3f\n", i, avg) + // } + // rates = nil + //}, + } +} diff --git a/llmjob/2625.go b/llmjob/2625.go new file mode 100644 index 0000000..0061c1e --- /dev/null +++ b/llmjob/2625.go @@ -0,0 +1,168 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// AtrRsiStrategy 存储每个品种的持仓期间最高价和最低价 +type AtrRsiStrategy struct { + IntraTradeHigh float64 + IntraTradeLow float64 +} + +func J2625(pol *config.RunPolicyConfig) *strat.TradeStrat { + atrLength := pol.DefInt("atr_length", 18, core.PNorm(10, 50)) + atrMaRate := pol.Def("atr_ma_rate", 1.8, core.PNorm(1.2, 3.0)) + rsiLength := pol.DefInt("rsi_length", 9, core.PNorm(7, 30)) + rsiEntry := pol.DefInt("rsi_entry", 7, core.PNorm(5, 20)) + atrMaNormMin := pol.Def("atr_ma_norm_min", 0.3, core.PNorm(0.1, 0.5)) + atrMaNormMax := pol.Def("atr_ma_norm_max", 0.7, core.PNorm(0.5, 1.0)) + trailingPercent := pol.Def("trailing_percent", 1.3, core.PNorm(0.5, 3.0)) + + // 计算ATR移动平均线周期 + atrMaLength := int(float64(atrLength) * atrMaRate) + + // 计算RSI买入和卖出阈值 + rsiBuy := 50 + float64(rsiEntry) + rsiSell := 50 - float64(rsiEntry) + + // 确定预热数量,取最大参数值 + warmupNum := atrMaLength + if rsiLength > warmupNum { + warmupNum = rsiLength + } + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"1h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的持仓期间最高价和最低价 + s.More = &AtrRsiStrategy{ + IntraTradeHigh: 0, + IntraTradeLow: 0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*AtrRsiStrategy) + + // 计算技术指标 + atrValue := ta.ATR(e.High, e.Low, e.Close, atrLength) + atrMa := ta.SMA(atrValue, atrMaLength) + rsiValue := ta.RSI(e.Close, rsiLength) + + // 获取当前价格 + closePrice := e.Close.Get(0) + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(highPrice) || math.IsNaN(lowPrice) || + math.IsNaN(atrValue.Get(0)) || math.IsNaN(atrMa.Get(0)) || math.IsNaN(rsiValue.Get(0)) { + return + } + + // 计算ATR移动平均线的标准化值 + atrMaNorm := atrMa.Get(0) / closePrice * 100 + + // 获取当前持仓状态 + longOrders := s.LongOrders + shortOrders := s.ShortOrders + hasLongPosition := len(longOrders) > 0 + hasShortPosition := len(shortOrders) > 0 + + // 无持仓状态 + if !hasLongPosition && !hasShortPosition { + // 重置持仓期间最高价和最低价 + m.IntraTradeHigh = highPrice + m.IntraTradeLow = lowPrice + + // 检查波动率是否在适合交易范围内 + if atrMaNorm >= atrMaNormMin && atrMaNorm <= atrMaNormMax { + // 检查当前波动率是否高于平均波动率 + if atrValue.Get(0) > atrMa.Get(0) { + // 检查RSI信号 + if rsiValue.Get(0) > rsiBuy { + // RSI超买,做多 + s.OpenOrder(&strat.EnterReq{ + Tag: "B1", + OrderType: core.OrderTypeLimit, + Limit: closePrice + 5, + }) + } else if rsiValue.Get(0) < rsiSell { + // RSI超卖,做空 + s.OpenOrder(&strat.EnterReq{ + Tag: "S1", + Short: true, + OrderType: core.OrderTypeLimit, + Limit: closePrice - 5, + }) + } + } + } + } else if hasLongPosition { + // 多头持仓状态 + // 更新持仓期间最高价 + if highPrice > m.IntraTradeHigh { + m.IntraTradeHigh = highPrice + } + m.IntraTradeLow = lowPrice + + // 计算多头追踪止损价格 + longTp := m.IntraTradeHigh * (1 - trailingPercent/100) + + // 获取入场价格 + enterPrice := longOrders[0].InitPrice + + // 设置止损和止盈 + for _, od := range longOrders { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: longTp, + Rate: 1.0, // 全部退出 + Tag: "trailing_stop", + }) + + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: enterPrice * 1.03, + Rate: 1.0, // 全部退出 + Tag: "take_profit", + }) + } + } else if hasShortPosition { + // 空头持仓状态 + m.IntraTradeHigh = highPrice + // 更新持仓期间最低价 + if lowPrice < m.IntraTradeLow { + m.IntraTradeLow = lowPrice + } + + // 计算空头追踪止损价格 + shortTp := m.IntraTradeLow * (1 + trailingPercent/100) + + // 获取入场价格 + enterPrice := shortOrders[0].InitPrice + + // 设置止损和止盈 + for _, od := range shortOrders { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: shortTp, + Rate: 1.0, // 全部退出 + Tag: "trailing_stop", + }) + + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: enterPrice * 0.94, + Rate: 1.0, // 全部退出 + Tag: "take_profit", + }) + } + } + }, + } +} diff --git a/llmjob/2775.go b/llmjob/2775.go new file mode 100644 index 0000000..a327dbb --- /dev/null +++ b/llmjob/2775.go @@ -0,0 +1,104 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type MacdRsiState struct { + above50 bool // 标记RSI是否上穿50 + below50 bool // 标记RSI是否下穿50 + longEntryTime int64 // 记录多头入场时间 + shortEntryTime int64 // 记录空头入场时间 +} + +func J2775(pol *config.RunPolicyConfig) *strat.TradeStrat { + fastPeriod := pol.DefInt("fastPeriod", 15, core.PNorm(5, 20)) + macdRate := pol.Def("macdRate", 2.5, core.PNorm(1.5, 4)) + rsiPeriod := pol.DefInt("rsiPeriod", 16, core.PNorm(7, 21)) + exitBars := pol.DefInt("exitBars", 5, core.PNorm(1, 10)) + signalSmoothing := pol.DefInt("signalSmoothing", 9, core.PNorm(5, 15)) + + // 计算MACD慢线长度 + slowPeriod := int(float64(fastPeriod) * macdRate) + + return &strat.TradeStrat{ + WarmupNum: slowPeriod + signalSmoothing + rsiPeriod, + HedgeOff: true, // 单向持仓 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + s.More = &MacdRsiState{ + above50: false, + below50: false, + longEntryTime: 0, + shortEntryTime: 0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*MacdRsiState) + + // 计算MACD指标 + macdLine, signalLine := ta.MACD(e.Close, fastPeriod, slowPeriod, signalSmoothing) + + // 计算RSI指标 + rsi := ta.RSI(e.Close, rsiPeriod) + + // 检测MACD金叉和死叉 + bullishCross := macdLine.Cross(signalLine) == 1 + bearishCross := macdLine.Cross(signalLine) == -1 + + // 更新RSI状态 + if rsi.Get(0) > 50 && rsi.Get(1) <= 50 { + state.above50 = true + } + if rsi.Get(0) < 50 && rsi.Get(1) >= 50 { + state.below50 = true + } + + // 执行交易信号 + if bullishCross && state.above50 && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + }) + state.longEntryTime = e.TimeStop + } + + if bearishCross && state.below50 && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + }) + state.shortEntryTime = e.TimeStop + } + + // 检查退出条件 + if len(s.LongOrders) > 0 && state.longEntryTime > 0 { + holdBars := e.BarCount(state.longEntryTime) + if holdBars >= float64(exitBars) { + s.CloseOrders(&strat.ExitReq{ + Tag: "Exit Long", + Dirt: core.OdDirtLong, + }) + state.longEntryTime = 0 + } + } + + if len(s.ShortOrders) > 0 && state.shortEntryTime > 0 { + holdBars := e.BarCount(state.shortEntryTime) + if holdBars >= float64(exitBars) { + s.CloseOrders(&strat.ExitReq{ + Tag: "Exit Short", + Dirt: core.OdDirtShort, + }) + state.shortEntryTime = 0 + } + } + }, + } +} diff --git a/llmjob/2874.go b/llmjob/2874.go new file mode 100644 index 0000000..e7d3ad2 --- /dev/null +++ b/llmjob/2874.go @@ -0,0 +1,94 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J2874(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + rsiPeriod := pol.DefInt("rsiPeriod", 70, core.PNorm(20, 100)) + longExitThreshold := pol.Def("longExitThreshold", 0.65, core.PNorm(0.55, 0.8)) + shortExitThreshold := pol.Def("shortExitThreshold", 0.35, core.PNorm(0.2, 0.45)) + allowLong := pol.Def("allowLong", 1.0, core.PUniform(0, 1)) > 0.5 + allowShort := pol.Def("allowShort", 1.0, core.PUniform(0, 1)) > 0.5 + + return &strat.TradeStrat{ + WarmupNum: rsiPeriod, // 预热期至少为RSI周期 + HedgeOff: true, // 单向持仓模式,与原PineScript策略一致 + RunTimeFrames: []string{"4h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算各种价格组合 + ohlc4 := ta.HLC3(e.High, e.Low, e.Close).Add(e.Open).Div(4) + hlc3 := ta.HLC3(e.High, e.Low, e.Close) + hl2 := ta.HL2(e.High, e.Low) + + // 计算各种RSI值并归一化(除以100) + rsiOhlc4 := ta.RSI(ohlc4, rsiPeriod).Div(100) + rsiClose := ta.RSI(e.Close, rsiPeriod).Div(100) + rsiOpen := ta.RSI(e.Open, rsiPeriod).Div(100) + rsiHigh := ta.RSI(e.High, rsiPeriod).Div(100) + rsiHlc3 := ta.RSI(hlc3, rsiPeriod).Div(100) + rsiHl2 := ta.RSI(hl2, rsiPeriod).Div(100) + + // 计算RSI均值 + rsiAvg := rsiOhlc4.Add(rsiClose).Add(rsiOpen).Add(rsiHigh).Add(rsiHlc3).Add(rsiHl2).Div(6) + rsiAvgVal := rsiAvg.Get(0) + + // 检查数据有效性 + if math.IsNaN(rsiAvgVal) { + return + } + + // 检测RSI均值与0.5的交叉 + entryThreshold := 0.5 + crossSignal := rsiAvg.Cross(entryThreshold) + + // 生成交易信号 + longSignal := crossSignal == 1 // RSI均值上穿0.5 + shortSignal := crossSignal == -1 // RSI均值下穿0.5 + + // 生成平仓信号 + longExit := rsiAvgVal >= longExitThreshold || shortSignal + shortExit := rsiAvgVal <= shortExitThreshold || longSignal + + // 执行交易逻辑 + if allowLong && longSignal && len(s.LongOrders) == 0 { + // 开多头仓位 + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + + if allowShort && shortSignal && len(s.ShortOrders) == 0 { + // 开空头仓位 + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + }) + } + + if allowLong && len(s.LongOrders) > 0 && longExit { + // 平多头仓位 + s.CloseOrders(&strat.ExitReq{ + Tag: "long_exit", + Dirt: core.OdDirtLong, + }) + } + + if allowShort && len(s.ShortOrders) > 0 && shortExit { + // 平空头仓位 + s.CloseOrders(&strat.ExitReq{ + Tag: "short_exit", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/2876.go b/llmjob/2876.go new file mode 100644 index 0000000..d602178 --- /dev/null +++ b/llmjob/2876.go @@ -0,0 +1,110 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J2876(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + frequency := pol.DefInt("frequency", 28, core.PNorm(5, 30)) + rsiFrequency := pol.DefInt("rsiFrequency", 220, core.PNorm(14, 100)) + buyZoneDistance := pol.Def("buyZoneDistance", 7, core.PNorm(2, 15)) + avgDownATRSum := pol.DefInt("avgDownATRSum", 3, core.PNorm(1, 10)) + useAbsoluteRSIBarrier := pol.Def("useAbsoluteRSIBarrier", 1, core.PUniform(0, 1)) // 1表示true,0表示false + barrierLevel := pol.Def("barrierLevel", 45, core.PNorm(30, 70)) + + // 计算预热数量,取RSI周期和均线周期中的较大值 + warmupNum := rsiFrequency + if frequency > rsiFrequency { + warmupNum = frequency + } + // 再加上ATR周期 + warmupNum += 20 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"15m"}, + EachMaxLong: 3, // 最多允许3个多头订单,对应原策略的pyramiding=3 + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + momentumRSI := ta.RSI(e.Close, rsiFrequency) + momentumRSI_slow := ta.SMA(momentumRSI, frequency) + atr := ta.ATR(e.High, e.Low, e.Close, 20) + + // 获取当前值 + rsiValue := momentumRSI.Get(0) + rsiSlowValue := momentumRSI_slow.Get(0) + closePrice := e.Close.Get(0) + + // 检查数据有效性 + if math.IsNaN(rsiValue) || math.IsNaN(rsiSlowValue) || math.IsNaN(closePrice) { + return + } + + // 计算买卖区域 + buyZoneLower := rsiSlowValue * (1 - buyZoneDistance/100) + + // 生成交易信号 + useBarrier := useAbsoluteRSIBarrier > 0.5 + momentumRSISoftClose := (rsiValue > rsiSlowValue) && (rsiValue > barrierLevel || !useBarrier) + isClose := momentumRSISoftClose + + // 计算风险管理条件 + riskCondition := true + if len(s.LongOrders) > 0 { + // 计算平均持仓价格 + totalCost := 0.0 + totalAmount := 0.0 + for _, od := range s.LongOrders { + if od.Status >= ormo.InOutStatusPartEnter { + totalCost += od.EnterCost() + totalAmount += od.HoldAmount() + } + } + if totalAmount > 0 { + avgPrice := totalCost / totalAmount + // 计算过去avgDownATRSum个周期的ATR值之和 + atrSum := 0.0 + for i := 0; i < avgDownATRSum; i++ { + atrVal := atr.Get(i) + if !math.IsNaN(atrVal) { + atrSum += atrVal + } + } + // 计算风险条件:平均持仓价格 - ATR求和值 * 开仓数量 > 当前价格 + riskCondition = avgPrice-atrSum*float64(len(s.LongOrders)) > closePrice + } + } + + // 买入信号:RSI低于买入区域下限且满足风险条件 + isBuy := rsiValue < buyZoneLower && (riskCondition || len(s.LongOrders) == 0) + + // 执行交易操作 + if isBuy && len(s.LongOrders) < 3 { // 限制最多3个多头订单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + Short: false, + OrderType: core.OrderTypeMarket, + }) + } + + // 平仓信号 + if isClose && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Close", + Dirt: core.OdDirtLong, + OrderType: core.OrderTypeLimit, + Limit: closePrice, + }) + } + }, + } +} diff --git a/llmjob/2984.go b/llmjob/2984.go new file mode 100644 index 0000000..aebcec5 --- /dev/null +++ b/llmjob/2984.go @@ -0,0 +1,269 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// ZeroLagEMA 计算零滞后指数移动平均线 +func ZeroLagEMA(source *ta.Series, length int) *ta.Series { + res := source.To("_zlema", length) + if res.Cached() { + return res + } + if source.Len() < length { + return res.Append(math.NaN()) + } + ema1 := ta.EMA(source, length) + ema2 := ta.EMA(ema1, length) + c1 := ema1.Get(0) - ema2.Get(0) + zlema := ema1.Get(0) + c1 + return res.Append(zlema) +} + +// ZlemaState 存储每个品种的策略状态 +type ZlemaState struct { + goLong bool + sectionLongs int + sectionShorts int + sectionLongConditions int + sectionShortConditions int + totalLongs float64 + totalShorts float64 + averageLongs float64 + averageShorts float64 + lastHigh float64 + lastLow float64 + inLong bool + inShort bool +} + +func J2984(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastPeriod := pol.DefInt("fastPeriod", 10, core.PNorm(3, 20)) + maRate := pol.Def("maRate", 2.4, core.PNorm(1.5, 5)) + takeProfitRate := pol.Def("takeProfitRate", 3.5, core.PNorm(1, 10)) + trailingStopRate := pol.Def("trailingStopRate", 3.5, core.PNorm(1, 8)) + trailingActivationRate := pol.Def("trailingActivationRate", 16, core.PNorm(5, 25)) + stopLossPercent := pol.Def("stopLossPercent", 7.5, core.PNorm(1, 15)) + + // 金字塔加仓参数 + pyramidingLess := pol.DefInt("pyramidingLess", 1, core.PNorm(0, 5)) + pyramidingEqual := pol.DefInt("pyramidingEqual", 0, core.PNorm(0, 5)) + pyramidingGreater := pol.DefInt("pyramidingGreater", 1000000, core.PNorm(100, 1000000)) + + // 交易方向控制参数 + longsOnly := pol.Def("longsOnly", 0, core.PUniform(0, 1)) > 0.5 + shortsOnly := pol.Def("shortsOnly", 0, core.PUniform(0, 1)) > 0.5 + flipOpens := pol.Def("flipOpens", 0, core.PUniform(0, 1)) > 0.5 + + // 计算慢线周期 + slowPeriod := int(float64(fastPeriod) * maRate) + + return &strat.TradeStrat{ + WarmupNum: slowPeriod * 2, // 确保有足够的数据计算指标 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多开1单多仓 + EachMaxShort: 1, // 最多开1单空仓 + DrawDownExit: false, // 不使用回撤止盈 + RunTimeFrames: []string{"8h"}, + StopLoss: 0.075, + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + s.More = &ZlemaState{ + goLong: true, + sectionLongs: 0, + sectionShorts: 0, + sectionLongConditions: 0, + sectionShortConditions: 0, + totalLongs: 0, + totalShorts: 0, + averageLongs: 0, + averageShorts: 0, + lastHigh: 0, + lastLow: 0, + inLong: false, + inShort: false, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*ZlemaState) + + // 计算Zero-Lag EMA + zlemaFast := ZeroLagEMA(e.Close, fastPeriod) + zlemaSlow := ZeroLagEMA(e.Close, slowPeriod) + + // 获取当前值 + fastVal := zlemaFast.Get(0) + slowVal := zlemaSlow.Get(0) + fastVal1 := zlemaFast.Get(1) + slowVal1 := zlemaSlow.Get(1) + closePrice := e.Close.Get(0) + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + + // 检查数据有效性 + if math.IsNaN(fastVal) || math.IsNaN(slowVal) || math.IsNaN(closePrice) { + return + } + + // 生成基础交易信号 + longSignal := fastVal > slowVal && fastVal1 <= slowVal1 // 快线上穿慢线 + shortSignal := fastVal < slowVal && fastVal1 >= slowVal1 // 快线下穿慢线 + + // 应用交易方向翻转功能 + if flipOpens { + longSignal, shortSignal = shortSignal, longSignal + } + + // 应用仅做多/仅做空选项 + if longsOnly { + shortSignal = false + } + if shortsOnly { + longSignal = false + } + + // 更新连续信号计数 + if longSignal { + state.sectionLongs++ + state.sectionShorts = 0 + } else if shortSignal { + state.sectionShorts++ + state.sectionLongs = 0 + } + + // 应用金字塔式加仓条件 + longCondition := longSignal && (state.sectionLongs <= pyramidingLess || state.sectionLongs >= pyramidingGreater || state.sectionLongs == pyramidingEqual) + shortCondition := shortSignal && (state.sectionShorts <= pyramidingLess || state.sectionShorts >= pyramidingGreater || state.sectionShorts == pyramidingEqual) + + // 更新开仓价格和数量 + if longCondition { + state.totalLongs += closePrice + state.sectionLongConditions++ + state.averageLongs = state.totalLongs / float64(state.sectionLongConditions) + state.inLong = true + state.inShort = false + } else if shortCondition { + state.totalShorts += closePrice + state.sectionShortConditions++ + state.averageShorts = state.totalShorts / float64(state.sectionShortConditions) + state.inShort = true + state.inLong = false + } + + // 更新最高价/最低价追踪 + if state.inLong { + if state.lastHigh == 0 || highPrice > state.lastHigh { + state.lastHigh = highPrice + } + } else if state.inShort { + if state.lastLow == 0 || lowPrice < state.lastLow { + state.lastLow = lowPrice + } + } + + // 执行交易 + if longCondition && len(s.LongOrders) == 0 { + // 设置止损止盈 - 基于平均开仓价计算 + stopLossVal := state.averageLongs * (1 - stopLossPercent/100) + takeProfitVal := state.averageLongs * (1 + takeProfitRate/100) + + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + Short: false, + OrderType: core.OrderTypeMarket, + StopLossVal: stopLossVal, + TakeProfitVal: takeProfitVal, + StopLossTag: "stop_loss", + TakeProfitTag: "take_profit", + }) + } else if shortCondition && len(s.ShortOrders) == 0 { + // 设置止损止盈 - 基于平均开仓价计算 + stopLossVal := state.averageShorts * (1 + stopLossPercent/100) + takeProfitVal := state.averageShorts * (1 - takeProfitRate/100) + + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + OrderType: core.OrderTypeMarket, + StopLossVal: stopLossVal, + TakeProfitVal: takeProfitVal, + StopLossTag: "stop_loss", + TakeProfitTag: "take_profit", + }) + } + + // 检查平仓条件 + if state.inLong && shortSignal { + s.CloseOrders(&strat.ExitReq{ + Tag: "trend_reverse", + Dirt: core.OdDirtLong, + }) + } else if state.inShort && longSignal { + s.CloseOrders(&strat.ExitReq{ + Tag: "trend_reverse", + Dirt: core.OdDirtShort, + }) + } + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + e := s.Env + state, _ := s.More.(*ZlemaState) + closePrice := e.Close.Get(0) + + if od.Short { + // 做空订单检查 + if state.inShort { + // 检查追踪止损条件 - 包含激活条件判断 + if state.lastLow > 0 && + state.lastLow <= state.averageShorts*(1-trailingActivationRate/100) && // 激活条件 + closePrice >= state.lastLow*(1+trailingStopRate/100) { // 追踪止损条件 + return &strat.ExitReq{Tag: "trailing_stop"} + } + } + } else { + // 做多订单检查 + if state.inLong { + // 检查追踪止损条件 - 包含激活条件判断 + if state.lastHigh > 0 && + state.lastHigh >= state.averageLongs*(1+trailingActivationRate/100) && // 激活条件 + closePrice <= state.lastHigh*(1-trailingStopRate/100) { // 追踪止损条件 + return &strat.ExitReq{Tag: "trailing_stop"} + } + } + } + + return nil + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + state, _ := s.More.(*ZlemaState) + + // 订单状态变化时更新状态 + if chgType == strat.OdChgExitFill { + if od.Short { + // 做空订单已平仓 + state.inShort = false + state.averageShorts = 0 + state.totalShorts = 0 + state.sectionShorts = 0 + state.sectionShortConditions = 0 + state.lastLow = 0 + } else { + // 做多订单已平仓 + state.inLong = false + state.averageLongs = 0 + state.totalLongs = 0 + state.sectionLongs = 0 + state.sectionLongConditions = 0 + state.lastHigh = 0 + } + } + }, + } +} diff --git a/llmjob/3087.go b/llmjob/3087.go new file mode 100644 index 0000000..cb99fda --- /dev/null +++ b/llmjob/3087.go @@ -0,0 +1,154 @@ +package llmjob + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "gonum.org/v1/gonum/floats" + "math" + "sort" +) + +/* +基于初始的3087进行了简化,只保留盈利较好的14-26 +*/ + +// DCAStrategyState 存储每个品种的策略状态 +type DCAStrategyState struct { + currentSO int // 当前安全订单计数器 + firstPrice float64 // 初始订单价格 + totalQtys float64 // 总交易数量 + totalCost float64 // 累计花费 + realTotQtys float64 // 总交易数量 + realTotCost float64 // 累计花费 +} + +// calculateSafetyOrderThreshold 计算安全订单触发价格阈值 +func calculateSafetyOrderThreshold(initPrice, priceDeviation, stepScale float64, currentSO int) float64 { + curSO := float64(currentSO) + if stepScale == 1.0 { + return initPrice - initPrice*priceDeviation*stepScale*curSO + } + rate := priceDeviation*math.Pow(stepScale, curSO) - priceDeviation + return initPrice - initPrice*(rate/(stepScale-1)) +} + +// DynamicDCA 动态DCA策略 +func J3087(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + priceDeviation := pol.Def("priceDeviation", 0.82, core.PNorm(0.5, 3.0)) + volumeScale := pol.Def("volumeScale", 1.16, core.PNorm(1.05, 1.5)) + stepScale := pol.Def("stepScale", 1.06, core.PNorm(1.05, 1.3)) + takeProfit := pol.Def("takeProfit", 1.15, core.PNorm(0.5, 2.5)) + baseOrder := pol.Def("baseOrder", 10.0, core.PNorm(5, 20)) + startLvl := pol.DefInt("startLvl", 14.0, core.PNorm(1, 20)) + endLvl := pol.DefInt("endLvl", 26.0, core.PNorm(3, 30)) + maxSafeOrders := pol.DefInt("maxSafeOrders", 30, core.PNorm(5, 50)) + + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + var rates []float64 + // 14/15/16/17/18/19/20/21/22/23/24/25/26 + return &strat.TradeStrat{ + WarmupNum: 1, + HedgeOff: true, // 单向持仓 + RunTimeFrames: []string{"15m"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + s.More = &DCAStrategyState{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*DCAStrategyState) + + // 计算OHLC4加权均价 + price := (e.High.Get(0) + e.Low.Get(0) + e.Close.Get(0) + e.Open.Get(0)) / 4 + closeP := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + openRate := 0.004 * closeP / atrBase + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + openRate = max(0.7, min(1.5, openRate)) + + // 计算止盈水平 + hasOrders := state.currentSO > 0 + var avgPrice, realAvgPrice float64 + if hasOrders && state.totalQtys > 0 { + avgPrice = state.totalCost / state.totalQtys + realAvgPrice = state.realTotCost / state.realTotQtys + } + takeProfitLevel := avgPrice * (1 + takeProfit/100) + + // 初始建仓逻辑 + if state.currentSO == 0 && price > 0 { + // 计算买入数量 + qty := baseOrder / price + // 首次买入表现不佳,禁止实际买入 + state.totalQtys += qty + state.totalCost += qty * closeP + state.firstPrice = price + state.currentSO = 1 + } + + // 计算安全订单触发价格阈值 + priceRate := priceDeviation / 100 + threshold := calculateSafetyOrderThreshold(state.firstPrice, priceRate, stepScale, state.currentSO) + + // 动态补仓逻辑 + if hasOrders && price <= threshold && state.currentSO <= maxSafeOrders { + // 计算补仓数量 + orderQty := baseOrder * openRate * math.Pow(volumeScale, float64(state.currentSO-1)) / price + + // endLvl用于实际下单。maxSafeOrders用于止盈价格 + if state.currentSO >= startLvl && state.currentSO <= endLvl { + s.OpenOrder(&strat.EnterReq{ + Tag: fmt.Sprintf("%v", state.currentSO), + Amount: orderQty, + Log: true, + }) + state.realTotQtys += orderQty + state.realTotCost += orderQty * closeP + } + + state.totalQtys += orderQty + state.totalCost += orderQty * closeP + state.currentSO += 1 + } + + // 止盈平仓逻辑 + if price >= takeProfitLevel && hasOrders { + s.CloseOrders(&strat.ExitReq{ + Tag: "TakeProfit", + }) + if realAvgPrice > 0 { + rates = append(rates, takeProfitLevel/realAvgPrice) + } + + // 重置状态变量 + state.currentSO = 0 + state.totalQtys = 0.0 + state.totalCost = 0 + state.realTotQtys = 0 + state.realTotCost = 0 + state.firstPrice = 0 + } + }, + OnShutDown: func(s *strat.StratJob) { + if len(rates) == 0 { + return + } + sort.Float64s(rates) + step := len(rates) / 7 + for i := 0; i < len(rates); i += step { + block := rates[i:min(len(rates), i+step)] + avg := floats.Sum(block) / float64(len(block)) + fmt.Printf("rate: %v: %v\n", i, avg) + } + rates = nil + }, + } +} diff --git a/llmjob/3121.go b/llmjob/3121.go new file mode 100644 index 0000000..77d4854 --- /dev/null +++ b/llmjob/3121.go @@ -0,0 +1,88 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J3121(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + bbLength := pol.DefInt("bbLength", 20, core.PNorm(10, 50)) + bbStdDev := pol.Def("bbStdDev", 1.8, core.PNorm(1.0, 3.0)) + cmoLength := pol.DefInt("cmoLength", 10, core.PNorm(5, 30)) + cmoOverbought := pol.Def("cmoOverbought", 45.0, core.PNorm(30.0, 70.0)) + cmoOversold := pol.Def("cmoOversold", -50.0, core.PNorm(-70.0, -30.0)) + + return &strat.TradeStrat{ + WarmupNum: bbLength, // 预热期设置为布林带周期长度 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + RunTimeFrames: []string{"5m"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算布林带 + bbUpper, bbBasis, bbLower := ta.BBANDS(e.Close, bbLength, bbStdDev, bbStdDev) + + // 计算钱德动量振荡器 + cmo := ta.CMO(e.Close, cmoLength) + + // 获取当前值 + closePrice := e.Close.Get(0) + cmoValue := cmo.Get(0) + bbUpperValue := bbUpper.Get(0) + bbLowerValue := bbLower.Get(0) + bbBasisValue := bbBasis.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(cmoValue) || + math.IsNaN(bbUpperValue) || math.IsNaN(bbLowerValue) || math.IsNaN(bbBasisValue) { + return + } + + // 做多条件:价格下穿布林带下轨且CMO低于超卖阈值 + longCondition := e.Close.Cross(bbLower) == -1 && cmoValue < cmoOversold + + // 做空条件:价格上穿布林带上轨且CMO高于超买阈值 + shortCondition := e.Close.Cross(bbUpper) == 1 && cmoValue > cmoOverbought + + // 平多条件:价格上穿布林带中轨或CMO高于超买阈值 + exitLong := e.Close.Cross(bbBasis) == 1 || cmoValue > cmoOverbought + + // 平空条件:价格下穿布林带中轨或CMO低于超卖阈值 + exitShort := e.Close.Cross(bbBasis) == -1 || cmoValue < cmoOversold + + // 执行交易操作 + if longCondition && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + }) + } + + if shortCondition && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + }) + } + + if exitLong && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_long", + Dirt: core.OdDirtLong, + }) + } + + if exitShort && len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_short", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/3127.go b/llmjob/3127.go new file mode 100644 index 0000000..9e295ce --- /dev/null +++ b/llmjob/3127.go @@ -0,0 +1,55 @@ +// 修复后的完整策略代码 +// 请勿修改package包名和任意函数名 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J3127(pol *config.RunPolicyConfig) *strat.TradeStrat { + buyThreshold := pol.Def("buyThreshold", 0.025, core.PNorm(0.005, 0.05)) + sellThreshold := pol.Def("sellThreshold", 0.025, core.PNorm(0.005, 0.05)) + lookbackPeriod := pol.DefInt("lookbackPeriod", 16, core.PNorm(5, 20)) + + return &strat.TradeStrat{ + WarmupNum: lookbackPeriod, + HedgeOff: true, + EachMaxLong: 1, + RunTimeFrames: []string{"15m"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + closePrice := e.Close.Get(0) + + if math.IsNaN(closePrice) { + return + } + + // 修复:使用收盘价计算历史极值 + highest := ta.Highest(e.Close, lookbackPeriod).Get(0) + lowest := ta.Lowest(e.Close, lookbackPeriod).Get(0) + + buyTrigger := highest * (1 - buyThreshold) + sellTrigger := lowest * (1 + sellThreshold) + + if closePrice < buyTrigger && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Achat", + Short: false, + OrderType: core.OrderTypeMarket, + }) + } + + if closePrice > sellTrigger && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Vente", + Dirt: core.OdDirtLong, + Force: true, + }) + } + }, + } +} diff --git a/llmjob/3137.go b/llmjob/3137.go new file mode 100644 index 0000000..0d4104c --- /dev/null +++ b/llmjob/3137.go @@ -0,0 +1,127 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" +) + +type DDLState struct { + lastbull float64 // 最后一个涨停价(连续上涨时的最高点) + lastbear float64 // 最后一个跌停价(连续下跌时的最低点) + lot float64 // 计算后的开仓数量 +} + +func J3137(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + signalshort := pol.Def("signalshort", 5.1, core.PNorm(1.0, 10.0)) + signallong := pol.Def("signallong", -5.1, core.PNorm(-10.0, -1.0)) + capital := pol.Def("capital", 145.0, core.PNorm(10.0, 200.0)) + needlong := pol.Def("needlong", 1.0, core.PNorm(0.0, 1.0)) + needshort := pol.Def("needshort", 1.0, core.PNorm(0.0, 1.0)) + + return &strat.TradeStrat{ + WarmupNum: 2, // 至少需要2个K线数据才能计算bull和bear + HedgeOff: false, // 允许双向持仓 + RunTimeFrames: []string{"15m"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的状态 + s.More = &DDLState{ + lastbull: math.NaN(), + lastbear: math.NaN(), + lot: 0.0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*DDLState) + + // 获取当前和前一个K线的价格 + closePrice := e.Close.Get(0) + prevClose := e.Close.Get(1) + openPrice := e.Open.Get(0) + + // 计算capital比例,避免重复计算 + capitalRate := capital / 100 + + // 如果数据不足,返回 + if math.IsNaN(closePrice) || math.IsNaN(prevClose) { + return + } + + // 计算价格波动方向 + bull := closePrice > prevClose + bear := closePrice < prevClose + + // 更新最后一个涨停价和跌停价 + if bull { + state.lastbull = closePrice + } + if bear { + state.lastbear = closePrice + } + + // 如果还没有初始化lastbull和lastbear,返回 + if math.IsNaN(state.lastbull) || math.IsNaN(state.lastbear) { + return + } + + // 计算价格变化比例 + ddl := ((closePrice / state.lastbull) - 1) * 100 + dds := ((closePrice / state.lastbear) - 1) * 100 + + // 生成交易信号 + up := ddl < signallong + dn := dds > signalshort + + // 计算开仓数量(如果没有持仓) + if s.OrderNum == 0 { + // 修复:使用策略方法获取权益计算仓位 + stakeAmount := s.Strat.GetStakeAmount(s) + state.lot = stakeAmount / closePrice * capitalRate + } + + // truetime条件,与原始策略保持一致 + truetime := true + + // 执行交易 + if up && needlong > 0.5 && truetime { + // 修复:使用计算好的lot值作为开仓数量 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Short: false, + Amount: state.lot, + }) + } + + if dn && needshort > 0.5 && truetime { + // 修复:使用计算好的lot值作为开仓数量 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + Amount: state.lot, + }) + } + + // 平仓条件:开多仓后价格上涨就平多仓,开空仓后价格下跌就平空仓 + if len(s.LongOrders) > 0 && closePrice > openPrice { + // 修复:使用CloseOrders进行平仓 + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseLong", + Dirt: core.OdDirtLong, + }) + } + + if len(s.ShortOrders) > 0 && closePrice < openPrice { + // 修复:使用CloseOrders进行平仓 + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseShort", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/32.go b/llmjob/32.go new file mode 100644 index 0000000..06d00f5 --- /dev/null +++ b/llmjob/32.go @@ -0,0 +1,64 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J32(pol *config.RunPolicyConfig) *strat.TradeStrat { + smaLength := pol.DefInt("smaLength", 30, core.PNorm(5, 30)) + macdFastLength := pol.DefInt("macdFastLength", 25, core.PNorm(5, 20)) + macdSlowLength := pol.DefInt("macdSlowLength", 38, core.PNorm(15, 40)) + macdSignalSmoothing := pol.DefInt("macdSignalSmoothing", 3, core.PNorm(3, 15)) + warmupNum := smaLength + if macdSlowLength > warmupNum { + warmupNum = macdSlowLength + } + if macdFastLength+macdSignalSmoothing > warmupNum { + warmupNum = macdFastLength + macdSignalSmoothing + } + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, + EachMaxLong: 1, + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + sma10 := ta.SMA(e.Close, smaLength) + macdLine, signalLine := ta.MACD(e.Close, macdFastLength, macdSlowLength, macdSignalSmoothing) + closePrice := e.Close.Get(0) + sma10Value := sma10.Get(0) + macdValue := macdLine.Get(0) + signalValue := signalLine.Get(0) + if math.IsNaN(closePrice) || math.IsNaN(sma10Value) || math.IsNaN(macdValue) || math.IsNaN(signalValue) { + return + } + priceCrossSMA := e.Close.Cross(sma10) + macdCrossSignal := macdLine.Cross(signalLine) + longCondition := priceCrossSMA == 1 && macdCrossSignal == 1 + shortCondition := priceCrossSMA == -1 && macdCrossSignal == -1 + if longCondition && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseShort", + Dirt: core.OdDirtShort, + }) + } + if shortCondition && len(s.LongOrders) > 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseLong", + Dirt: core.OdDirtLong, + }) + } + }, + } +} diff --git a/llmjob/3464.go b/llmjob/3464.go new file mode 100644 index 0000000..5115638 --- /dev/null +++ b/llmjob/3464.go @@ -0,0 +1,173 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +type TrendlineData struct { + upper float64 + lower float64 + slopePh float64 + slopePl float64 + upos int + dnos int + prevUpos int + prevDnos int +} + +func J3464(pol *config.RunPolicyConfig) *strat.TradeStrat { + length := pol.DefInt("length", 13, core.PNorm(5, 30)) + mult := pol.Def("mult", 1.05, core.PNorm(0.5, 2.0)) + calcMethod := pol.Def("calcMethod", 0, core.PUniform(0, 2)) // 0: Atr, 1: Stdev, 2: Linreg + _ = pol.Def("backpaint", 1.0, core.PUniform(0, 1)) == 1.0 // 使用下划线忽略未使用的变量 + + return &strat.TradeStrat{ + WarmupNum: length * 2, + HedgeOff: true, // 单向持仓 + RunTimeFrames: []string{"4h"}, + OnStartUp: func(s *strat.StratJob) { + s.More = &TrendlineData{ + upper: 0, + lower: 0, + slopePh: 0, + slopePl: 0, + upos: 0, + dnos: 0, + prevUpos: 0, + prevDnos: 0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + data, _ := s.More.(*TrendlineData) + + // 获取当前价格 + closePrice := e.Close.Get(0) + if math.IsNaN(closePrice) { + return + } + + // 检测支点 - 实现与PineScript的pivothigh和pivotlow相似的功能 + // 需要检查当前K线是否是过去length周期内的最高点,并且左右两侧都有length个K线 + ph := false + pl := false + + // 确保有足够的历史数据 + if e.Close.Len() > length*2 { + // 检查当前K线是否是支点高 + currentHigh := e.High.Get(length) + isPivotHigh := true + + for i := 0; i < length; i++ { + if e.High.Get(i) >= currentHigh || e.High.Get(length*2-i) >= currentHigh { + isPivotHigh = false + break + } + } + + // 检查当前K线是否是支点低 + currentLow := e.Low.Get(length) + isPivotLow := true + + for i := 0; i < length; i++ { + if e.Low.Get(i) <= currentLow || e.Low.Get(length*2-i) <= currentLow { + isPivotLow = false + break + } + } + + ph = isPivotHigh && !math.IsNaN(currentHigh) + pl = isPivotLow && !math.IsNaN(currentLow) + } + + // 计算斜率 + var slope float64 + method := int(calcMethod) + switch method { + case 0: // Atr + atr := ta.ATR(e.High, e.Low, e.Close, length) + slope = atr.Get(0) / float64(length) * mult + case 1: // Stdev + stdDev := ta.StdDev(e.Close, length) + slope = stdDev.Get(0) / float64(length) * mult + case 2: // Linreg + // 简化版的线性回归斜率计算 + n := float64(length) + sumX := n * (n - 1) / 2 + sumY := ta.SMA(e.Close, length).Get(0) * n + sumXY := 0.0 + sumX2 := 0.0 + + for i := 0; i < length; i++ { + x := float64(i) + y := e.Close.Get(i) + if !math.IsNaN(y) { + sumXY += x * y + sumX2 += x * x + } + } + + // 计算斜率 + denominator := n*sumX2 - sumX*sumX + if denominator != 0 { + slope = (n*sumXY - sumX*sumY) / denominator * mult + } else { + slope = 0 + } + } + + // 更新趋势线斜率和值 + if ph { + data.slopePh = slope + data.upper = e.High.Get(length) + } else { + data.upper = data.upper - data.slopePh + } + + if pl { + data.slopePl = slope + data.lower = e.Low.Get(length) + } else { + data.lower = data.lower + data.slopePl + } + + // 保存前一个状态 + data.prevUpos = data.upos + data.prevDnos = data.dnos + + // 检测突破状态 + if ph { + data.upos = 0 + } else if closePrice > data.upper-data.slopePh*float64(length) { + data.upos = 1 + } + + if pl { + data.dnos = 0 + } else if closePrice < data.lower+data.slopePl*float64(length) { + data.dnos = 1 + } + + // 检测突破并生成交易信号 + upBreakout := data.upos > data.prevUpos + dnBreakout := data.dnos > data.prevDnos + + if upBreakout && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Up Breakout", + }) + } + + if dnBreakout && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Down Breakout", + Short: true, + }) + } + }, + } +} diff --git a/llmjob/3614.go b/llmjob/3614.go new file mode 100644 index 0000000..ae2ac1d --- /dev/null +++ b/llmjob/3614.go @@ -0,0 +1,66 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J3614(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + length := pol.DefInt("length", 66, core.PNorm(20, 100)) + hmaRate := pol.Def("hmaRate", 8.72, core.PNorm(3, 15)) + channelFactor := pol.Def("channelFactor", 0.98, core.PNorm(0.95, 0.995)) + + // 计算HMA实际周期 + hmaPeriod := int(float64(length) * hmaRate) + + return &strat.TradeStrat{ + WarmupNum: hmaPeriod, // 预热数量取最大的指标周期 + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"1h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算线性回归线 + linreg := ta.LinReg(e.Close, length) + linregValue := linreg.Get(0) + + // 计算通道上限线 + band2 := linregValue * channelFactor + + // 计算Hull移动平均线 + hma := ta.HMA(e.Close, hmaPeriod) + hmaValue := hma.Get(0) + + // 获取当前收盘价 + closePrice := e.Close.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(linregValue) || math.IsNaN(band2) || math.IsNaN(hmaValue) { + return + } + + // 生成交易信号 + longCondition := closePrice < band2 && hmaValue < closePrice + shortCondition := closePrice > linregValue + + // 执行交易操作 + if longCondition && len(s.LongOrders) == 0 { + // 满足做多条件且没有多头持仓时开仓 + s.OpenOrder(&strat.EnterReq{ + Tag: "BUY", + }) + } else if shortCondition && len(s.LongOrders) > 0 { + // 满足平仓条件且持有多头仓位时平仓 + s.CloseOrders(&strat.ExitReq{ + Tag: "SELL", + Dirt: core.OdDirtLong, + }) + } + }, + } +} diff --git a/llmjob/3644.go b/llmjob/3644.go new file mode 100644 index 0000000..0507fa7 --- /dev/null +++ b/llmjob/3644.go @@ -0,0 +1,218 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// SupertrendState 存储每个品种的策略状态 +type SupertrendState struct { + trend int // 趋势状态,1表示上升趋势,-1表示下降趋势 + prevTrend int // 前一期的趋势状态 + up float64 // 上轨 + dn float64 // 下轨 + tp float64 // 止盈价格 +} + +func J3644(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + atrPeriod := pol.DefInt("atrPeriod", 5, core.PNorm(2, 20)) + atrMultiplier := pol.Def("atrMultiplier", 4.7, core.PNorm(2.0, 8.0)) + profitRatio := pol.Def("profitRatio", 0.382, core.PNorm(0.2, 1.0)) + useSmaATR := pol.DefInt("useSmaATR", 1, core.PUniform(0, 1)) // 1=true, 0=false + srcType := pol.DefInt("srcType", 0, core.PUniform(0, 7)) // 0=hlc3, 1=high, 2=low, 3=open, 4=hl2, 5=close, 6=hlcc4, 7=ohlc4 + + return &strat.TradeStrat{ + WarmupNum: atrPeriod * 3, // 预热期设置为ATR周期的3倍,确保有足够数据 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多开1个多单 + EachMaxShort: 1, // 最多开1个空单 + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + s.More = &SupertrendState{ + trend: 1, // 初始趋势状态为上升趋势 + prevTrend: 1, // 前一期趋势状态也为上升趋势 + tp: 0, // 初始止盈价格 + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*SupertrendState) + + // 获取数据源 + var src *ta.Series + switch srcType { + case 0: + src = ta.HLC3(e.High, e.Low, e.Close) + case 1: + src = e.High + case 2: + src = e.Low + case 3: + src = e.Open + case 4: + src = ta.HL2(e.High, e.Low) + case 5: + src = e.Close + case 6: + // HLCC4: (high + low + close + close) / 4 + src = e.High.Add(e.Low).Add(e.Close).Add(e.Close).Div(4) + case 7: + // OHLC4: (open + high + low + close) / 4 + src = e.Open.Add(e.High).Add(e.Low).Add(e.Close).Div(4) + default: + src = ta.HLC3(e.High, e.Low, e.Close) + } + + // 计算ATR + var atr *ta.Series + if useSmaATR == 1 { + // 使用标准ATR计算 + atr = ta.ATR(e.High, e.Low, e.Close, atrPeriod) + } else { + // 使用SMA(TR)计算ATR + trueRange := ta.TR(e.High, e.Low, e.Close) + atr = ta.SMA(trueRange, atrPeriod) + } + + // 计算超级趋势通道 + closePrice := e.Close.Get(0) + prevClose := e.Close.Get(1) + atrVal := atr.Get(0) + srcVal := src.Get(0) + + // 计算上下轨 + up := srcVal - (atrMultiplier * atrVal) + dn := srcVal + (atrMultiplier * atrVal) + + // 获取前一期上下轨 + prevUp := state.up + prevDn := state.dn + + // 如果是第一期或前一期值无效,使用当前值 + if math.IsNaN(prevUp) { + prevUp = up + } + if math.IsNaN(prevDn) { + prevDn = dn + } + + // 趋势跟踪逻辑 + if !math.IsNaN(prevClose) && prevClose > prevUp { + up = math.Max(up, prevUp) + } + if !math.IsNaN(prevClose) && prevClose < prevDn { + dn = math.Min(dn, prevDn) + } + + // 更新趋势状态 + state.prevTrend = state.trend + if state.trend == -1 && closePrice > prevDn { + state.trend = 1 + } else if state.trend == 1 && closePrice < prevUp { + state.trend = -1 + } + + // 保存当前上下轨值 + state.up = up + state.dn = dn + + // 生成交易信号 + buySignal := state.trend == 1 && state.prevTrend == -1 + sellSignal := state.trend == -1 && state.prevTrend == 1 + + // 获取当前订单 + longOrders := s.LongOrders + shortOrders := s.ShortOrders + + // 更新多头订单的止损 + if len(longOrders) > 0 { + for _, od := range longOrders { + if od.Status >= ormo.InOutStatusFullEnter { + // 保持之前的止盈价 + tp := state.tp + if math.IsNaN(tp) || tp <= 0 { + tp = od.InitPrice + (od.InitPrice-up)*profitRatio + } + // 更新止损价为上轨 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: up, + Tag: "super_trend_sl", + }) + // 更新止盈价 + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: tp, + Tag: "super_trend_tp", + }) + state.tp = tp + } + } + } + + // 更新空头订单的止损 + if len(shortOrders) > 0 { + for _, od := range shortOrders { + if od.Status >= ormo.InOutStatusFullEnter { + // 保持之前的止盈价 + tp := state.tp + if math.IsNaN(tp) || tp <= 0 { + tp = od.InitPrice - (dn-od.InitPrice)*profitRatio + } + // 更新止损价为下轨 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: dn, + Tag: "super_trend_sl", + }) + // 更新止盈价 + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: tp, + Tag: "super_trend_tp", + }) + state.tp = tp + } + } + } + + // 买入信号 + if buySignal && len(longOrders) == 0 { + // 计算止盈价 + tp := closePrice + (closePrice-up)*profitRatio + state.tp = tp + // 开多单,使用限价单 + s.OpenOrder(&strat.EnterReq{ + Tag: "super_trend_long", + Short: false, + OrderType: core.OrderTypeLimit, + Limit: tp, + StopLoss: up, + StopLossTag: "super_trend_sl", + TakeProfit: tp, + TakeProfitTag: "super_trend_tp", + }) + } + + // 卖出信号 + if sellSignal && len(shortOrders) == 0 { + // 计算止盈价 + tp := closePrice - (dn-closePrice)*profitRatio + state.tp = tp + // 开空单,使用限价单 + s.OpenOrder(&strat.EnterReq{ + Tag: "super_trend_short", + Short: true, + OrderType: core.OrderTypeLimit, + Limit: tp, + StopLoss: dn, + StopLossTag: "super_trend_sl", + TakeProfit: tp, + TakeProfitTag: "super_trend_tp", + }) + } + }, + } +} diff --git a/llmjob/3683.go b/llmjob/3683.go new file mode 100644 index 0000000..3adb36b --- /dev/null +++ b/llmjob/3683.go @@ -0,0 +1,86 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type MacdState struct { + lastCrossUp bool + lastCrossDown bool +} + +func J3683(pol *config.RunPolicyConfig) *strat.TradeStrat { + fastPeriod := pol.DefInt("fastPeriod", 16, core.PNorm(5, 20)) + emaRatio := pol.Def("emaRatio", 1.7, core.PNorm(1.5, 4)) + signalPeriod := pol.DefInt("signalPeriod", 8, core.PNorm(5, 15)) + profitRate := pol.Def("profitRate", 0.012, core.PNorm(0.005, 0.03)) + lossRate := pol.Def("lossRate", 0.009, core.PNorm(0.005, 0.03)) + + // 计算慢速EMA周期 + slowPeriod := int(float64(fastPeriod) * emaRatio) + + // 确保预热K线数量足够计算MACD指标 + warmupNum := fastPeriod + slowPeriod + signalPeriod + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的状态变量 + s.More = &MacdState{ + lastCrossUp: false, + lastCrossDown: false, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*MacdState) + + // 计算MACD指标 + macdLine, signalLine := ta.MACD(e.Close, fastPeriod, slowPeriod, signalPeriod) + + // 检测金叉和死叉 + crossUp := macdLine.Cross(signalLine) + crossDown := signalLine.Cross(macdLine) + + // 更新交叉状态 + if crossUp == 1 { // 金叉 + state.lastCrossUp = true + state.lastCrossDown = false + } else if crossDown == 1 { // 死叉 + state.lastCrossUp = false + state.lastCrossDown = true + } + + // 获取当前价格 + currentPrice := e.Close.Get(0) + + // 连续金叉且当前无多头订单,开多仓 + if state.lastCrossUp && crossUp == 1 && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "买入开多", + TakeProfitVal: currentPrice * (1 + profitRate), + StopLossVal: currentPrice * (1 - lossRate), + TakeProfitTag: "多单止盈", + StopLossTag: "多单止损", + }) + } + + // 连续死叉且当前无空头订单,开空仓 + if state.lastCrossDown && crossDown == 1 && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "卖出开空", + Short: true, + TakeProfitVal: currentPrice * (1 - profitRate), + StopLossVal: currentPrice * (1 + lossRate), + TakeProfitTag: "空单止盈", + StopLossTag: "空单止损", + }) + } + }, + } +} diff --git a/llmjob/3707.go b/llmjob/3707.go new file mode 100644 index 0000000..9eb36ce --- /dev/null +++ b/llmjob/3707.go @@ -0,0 +1,72 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J3707(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义策略参数 + rangeLength := pol.DefInt("rangeLength", 5, core.PNorm(3, 20)) + useHeikinAshi := pol.Def("useHeikinAshi", 0.0, core.PUniform(0, 1)) // 1.0表示true,0.0表示false + + return &strat.TradeStrat{ + WarmupNum: rangeLength + 1, // 确保有足够的历史数据计算指标 + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"3d"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 获取K线数据 + var open, close *ta.Series + if useHeikinAshi > 0.5 { + // 使用Heikin Ashi蜡烛图 + haOpen, _, _, haClose := ta.HeikinAshi(e) + open = haOpen + close = haClose + } else { + // 使用标准K线 + open = e.Open + close = e.Close + } + + // 计算成交量范围 + highest := ta.Highest(e.Volume, rangeLength) + _ = ta.Lowest(e.Volume.Mul(-1), rangeLength) // 最低负成交量 + + // 获取当前值和前一个值 + highestCurr := highest.Get(0) + highestPrev := highest.Get(1) + closeCurr := close.Get(0) + openCurr := open.Get(0) + + // 检查数据有效性 + if math.IsNaN(highestCurr) || math.IsNaN(highestPrev) || math.IsNaN(closeCurr) || math.IsNaN(openCurr) { + return + } + + // 生成交易信号 + volumeBreakout := highestCurr > highestPrev + isBullish := closeCurr > openCurr + isBearish := closeCurr < openCurr + + // 执行交易 + if volumeBreakout && isBullish { + // 成交量突破前高且为阳线,做多 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long Entry", + }) + } else if volumeBreakout && isBearish { + // 成交量突破前高且为阴线,做空 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short Entry", + Short: true, + }) + } + }, + } +} diff --git a/llmjob/3746.go b/llmjob/3746.go new file mode 100644 index 0000000..b2d41b2 --- /dev/null +++ b/llmjob/3746.go @@ -0,0 +1,163 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +type KijunData struct { + initialCapital float64 + netProfit float64 +} + +func J3746(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + ksPeriod := pol.DefInt("ksPeriod", 30, core.PNorm(9, 50)) + atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 30)) + atrMultiplier := pol.Def("atrMultiplier", 1.5, core.PNorm(0.8, 4)) + targetPoints := pol.Def("targetPoints", 100, core.PNorm(50, 200)) + riskPercent := pol.Def("riskPercent", 4, core.PNorm(1, 10)) + equityProtection := pol.Def("equityProtection", 30, core.PNorm(10, 50)) + + return &strat.TradeStrat{ + WarmupNum: ksPeriod, // 预热K线数量,确保基均线计算有足够数据 + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"3d"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的特定数据 + s.More = &KijunData{ + initialCapital: 50000.0, // 初始资金 + netProfit: 0.0, // 初始净利润 + } + }, + + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*KijunData) + + // 计算技术指标 + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + atrValue := atr.Get(0) + + // 计算基均线 (Kijun Sen) + highest := ta.Highest(e.High, ksPeriod) + lowest := ta.Lowest(e.Low, ksPeriod) + kijunSen := highest.Add(lowest).Div(2) + kijunSenValue := kijunSen.Get(0) + + // 检查数据有效性 + if math.IsNaN(atrValue) || math.IsNaN(kijunSenValue) { + return + } + + // 生成交易信号 + openPrice := e.Open.Get(0) + closePrice := e.Close.Get(0) + baseLong := openPrice < kijunSenValue && closePrice > kijunSenValue + baseShort := openPrice > kijunSenValue && closePrice < kijunSenValue + + // 计算止损距离 + stopLossVal := atrValue * atrMultiplier + + // 更新净利润 + // 计算总浮动盈亏 + totalFloating := 0.0 + for _, od := range s.LongOrders { + totalFloating += od.Profit + } + for _, od := range s.ShortOrders { + totalFloating += od.Profit + } + m.netProfit = totalFloating + + // 权益保护检查 + equityStopout := false + if s.OrderNum > 0 { + // 计算当前余额(初始资金 + 净利润) + totalBalance := m.initialCapital + m.netProfit + + // 检查是否触发权益保护 + if totalFloating < 0 && math.Abs(totalFloating/totalBalance) > equityProtection/100 { + equityStopout = true + } + } + + // 权益保护触发时平仓所有头寸 + if equityStopout { + s.CloseOrders(&strat.ExitReq{ + Tag: "equity_protection", + Dirt: core.OdDirtBoth, + Force: true, + }) + return + } + + // 检查是否有持仓 + hasLongPosition := len(s.LongOrders) > 0 + hasShortPosition := len(s.ShortOrders) > 0 + + // 计算头寸大小 + balance := m.initialCapital + m.netProfit + riskAmount := balance * (riskPercent / 100) // 风险金额 + size := riskAmount / stopLossVal // 合约数量 + if size < 0.001 { // 最小合约数量检查 + size = 0.001 + } + + // 多头交易 + if baseLong && !hasLongPosition && !hasShortPosition { + s.OpenOrder(&strat.EnterReq{ + Tag: "l_en", + Amount: size, + StopLossVal: stopLossVal, + TakeProfitVal: targetPoints, + StopLossTag: "stop_loss", + TakeProfitTag: "take_profit", + }) + } + + // 空头交易 + if baseShort && !hasShortPosition && !hasLongPosition { + s.OpenOrder(&strat.EnterReq{ + Tag: "s_en", + Short: true, + Amount: size, + StopLossVal: stopLossVal, + TakeProfitVal: targetPoints, + StopLossTag: "stop_loss", + TakeProfitTag: "take_profit", + }) + } + }, + + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + // 计算基均线 + highest := ta.Highest(s.Env.High, ksPeriod) + lowest := ta.Lowest(s.Env.Low, ksPeriod) + kijunSen := highest.Add(lowest).Div(2) + kijunSenValue := kijunSen.Get(0) + + closePrice := s.Env.Close.Get(0) + + // 多头平仓条件:收盘价低于基均线 + if !od.Short && closePrice < kijunSenValue { + return &strat.ExitReq{ + Tag: "long_exit", + } + } + + // 空头平仓条件:收盘价高于基均线 + if od.Short && closePrice > kijunSenValue { + return &strat.ExitReq{ + Tag: "short_exit", + } + } + + return nil + }, + } +} diff --git a/llmjob/3762.go b/llmjob/3762.go new file mode 100644 index 0000000..b9852c4 --- /dev/null +++ b/llmjob/3762.go @@ -0,0 +1,270 @@ +package llmjob + +import ( + "fmt" + "math" + "sort" + "sync" + + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "github.com/banbox/banexg/utils" + ta "github.com/banbox/banta" + "go.uber.org/zap" +) + +/* +超跌反弹策略。30个品种为宜;已增加暴跌抄底机制。 +开单越多,后续抄底价指数下移 +做空部分没有利润,不稳定,小亏,禁用 +*/ + +type State3762 struct { + NoLongUntil int64 + BWidth float64 + MidPrice float64 + SlRange float64 + volRate float64 +} + +func J3762(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义策略参数 + p := pol.DefInt("p", 20, core.PNorm(5, 50)) + d := pol.Def("d", 30.0, core.PNorm(5, 50)) + mult := pol.Def("mult", 1.5, core.PNorm(1.0, 3.0)) + slRate := pol.Def("slRate", 25, core.PNorm(0, 250)) //default: 25 + // 某品种入场时,下移其他品种的倍率(相对BWidth) + // 0.3开单太多亏损,0.5开单不足亏损 + downOtherRate := 0.33 + + dayMS := int64(utils.TFToSecs("1d") * 1000) + longFillNum := float64(0) + fillLock := sync.Mutex{} + var allJobs []*strat.StratJob + + // 基于成交量计算的区间倍率,成交量太大时,适当调低做多和止损价格 + var rangeVolRate = float64(1) + allowPairs := 1 // 允许挂单的订单数,默认为品种数,随开单减少,防止交易所故障导致下单过多 + openPairs := 0 // 实际有订单的品种数(挂单+持仓) + + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + + calcBuyPrice := func(m *State3762) float64 { + return m.MidPrice - m.BWidth*(1+math.Pow(longFillNum, 1.5)*downOtherRate)*rangeVolRate + } + return &strat.TradeStrat{ + WarmupNum: baseAtrLen * 2, // 预热K线数量,确保有足够数据计算指标 + RunTimeFrames: []string{"15m"}, + BatchInOut: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &State3762{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*State3762) + // 计算基础布林带指标 + sma := ta.SMA(e.Close, p) + ma7 := ta.SMA(e.Close, 7).Get(0) + ma30 := ta.SMA(e.Close, 30).Get(0) + multCur := mult + if ma7 < ma30 { + // 熊市下移下限 + multCur *= 1.35 + } + stddev := ta.StdDev(e.Close, p).Get(0) * multCur + upperBB := sma.Get(0) + stddev + lowerBB := sma.Get(0) - stddev + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + maVol := ta.EMA(e.Volume, lenAtr) + volBase := ta.Lowest(ta.Highest(maVol, lenAtr), baseAtrLen).Get(0) + m.volRate = maVol.Get(0) / volBase + volH3Rate := ta.Highest(maVol, 3).Get(0) / volBase + + // 计算百分比偏离轨道 + atrRt := d + if ma7 < ma30 { + // 熊市下移下限 + atrRt *= 1.35 + } + upex2 := sma.Get(0) + atrBase*atrRt + dnex2 := sma.Get(0) - atrBase*atrRt + + // 计算最终轨道 + upexPrice := max(upperBB, (upex2+upperBB)/2) + dnexPrice := min(lowerBB, (dnex2+lowerBB)/2) + m.BWidth = (upexPrice - dnexPrice) / 2 + m.MidPrice = (upexPrice + dnexPrice) / 2 + //lowPrice, highPrice := dnexPrice, upexPrice + lowPrice := calcBuyPrice(m) + + // 获取当前价格 + closePrice := e.Close.Get(0) + + // 平仓条件:阳线 + if len(s.LongOrders) > 0 && s.LongOrders[0].Status >= ormo.InOutStatusPartEnter { + doExit := closePrice > e.Open.Get(0) + if volH3Rate > 5 { + // 放巨量入场的订单,等待阳线后一个K线出场 + doExit = e.Close.Get(1) > e.Open.Get(1) + } + if doExit { + s.CloseOrders(&strat.ExitReq{ + Tag: "ExitLong", + Dirt: core.OdDirtLong, + Log: true, + }) + } + } + if math.IsNaN(lowPrice) || lowPrice <= 0 { + return + } + slRange := min(atrBase*slRate, closePrice*0.25) * min(3, rangeVolRate) + m.SlRange = slRange + + // 执行交易逻辑 + //if !s.IsWarmUp && e.TimeStop >= 1760130900000 { + // line := fmt.Sprintf("%s upp: %.6f, low: %.6f upex2: %.6f, dnex2: %.6f dnex: %.6f slRange: %.6f", + // e.Symbol, upperBB, lowerBB, upex2, dnex2, lowPrice, slRange) + // s.Strat.WriteOutput(line, true) + //} + if len(s.LongOrders) == 0 { + // 使用限价单在下轨价格做多,不依赖当前价格是否突破轨道 + if e.TimeStop >= m.NoLongUntil && openPairs < allowPairs { + fillLock.Lock() + openPairs += 1 + fillLock.Unlock() + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Limit: lowPrice, // 以下轨价格作为限价单 + StopLossVal: slRange, + Log: true, + }) + } + } else { + for _, od := range s.LongOrders { + if od.Status == ormo.InOutStatusInit { + err := od.SetEnterLimit(lowPrice) + if err != nil { + log.Error("SetEnterLimit fail", zap.Error(err)) + } else { + err = od.SetStopLoss(&ormo.ExitTrigger{ + Price: lowPrice - slRange, + }) + if err != nil { + log.Error("SetStopLoss fail", zap.Error(err)) + } + } + } else if od.Status == ormo.InOutStatusFullEnter { + barNum := e.BarCount(od.Enter.UpdateAt) + if barNum > 2 && od.ProfitRate < 0 { + // 持仓2个k线,出现亏损,平仓 + s.CloseOrders(&strat.ExitReq{ + Tag: "ExitBad", + Dirt: core.OdDirtLong, + Log: true, + }) + m.NoLongUntil = e.TimeStop + dayMS + } + } + } + } + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + fillLock.Lock() + longFillNum = 0 + openPairs = 0 + volRates := make([]float64, 0, len(jobs)) + for _, s := range jobs { + if len(s.LongOrders) > 0 { + openPairs += 1 + for _, od := range s.LongOrders { + if od.Status >= ormo.InOutStatusPartEnter { + longFillNum += 1 + } + } + } + m, _ := s.More.(*State3762) + volRates = append(volRates, m.volRate) + } + fillLock.Unlock() + allJobs = jobs + sort.Float64s(volRates) + rangeVolRate = max(1, math.Pow(volRates[len(volRates)/2]/2, 0.3)) + allowPairs = int(math.Round(math.Pow(0.8, longFillNum) * float64(len(allJobs)))) + //jobs[0].Strat.WriteOutput(fmt.Sprintf("%.3f", rangeVolRate), true) + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + if chgType == core.OdChgExitFill { + if od.Enter.Amount > 0 { + m, _ := s.More.(*State3762) + m.NoLongUntil = od.ExitAt + dayMS + fillLock.Lock() + longFillNum -= 1 + openPairs -= 1 + fillLock.Unlock() + } + } else if chgType == core.OdChgEnterFill { + fillLock.Lock() + longFillNum += 1 + openPairs = 0 // 这里应当重新计算,避免并发和回调导致错误 + for _, job := range allJobs { + if len(job.LongOrders) > 0 { + openPairs += 1 + } + } + fillLock.Unlock() + // 降低允许开仓的品种数,避免交易所故障导致下单太多 + allowPairs = int(math.Round(math.Pow(0.85, longFillNum) * float64(len(allJobs)))) + needClose := openPairs - allowPairs // 需要关闭的订单数 + if needClose > 0 { + // 排序确保多次回测一致 + sort.Slice(allJobs, func(i, j int) bool { + return allJobs[i].Symbol.Symbol < allJobs[j].Symbol.Symbol + }) + } + odMgr := biz.GetOdMgr(s.Account) + // 移动其他品种的入场价格,避免同时开仓太多 + for _, job := range allJobs { + if job == s || len(job.LongOrders) == 0 { + continue + } + m, _ := job.More.(*State3762) + lowPrice := calcBuyPrice(m) + for _, odr := range job.LongOrders { + if odr.Status <= ormo.InOutStatusPartEnter { + if lowPrice <= 0 || needClose > 0 { + needClose -= 1 + job.CloseOrders(&strat.ExitReq{ + Tag: "cancel", + OrderID: odr.ID, + Force: true, + }) + odMgr.ProcessOrders(job) + } else { + err := odr.SetEnterLimit(lowPrice) + if err == nil { + odr.EnterTag = fmt.Sprintf("long%d", int(longFillNum)) + odr.SetStopLoss(&ormo.ExitTrigger{ + Price: max(lowPrice-m.SlRange, lowPrice*0.1), + }) + } + } + } else { + odr.SetStopLoss(&ormo.ExitTrigger{ + Price: max(lowPrice-m.SlRange, odr.Enter.Price*0.1), + }) + } + } + } + } + }, + } +} diff --git a/llmjob/3782.go b/llmjob/3782.go new file mode 100644 index 0000000..70b0f3b --- /dev/null +++ b/llmjob/3782.go @@ -0,0 +1,310 @@ +package llmjob + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "sort" + "sync" +) + +/* +需要多品种综合市场信息,品种数不可少于30 +此策略有综合市场信息并限制开单数量,回测和实盘开单预期不一致,但开单数量基本符合 +openMaxs中的前两个减少会降低收益 + +待改进: +zone1信号在22年有几次略大回撤 +*/ + +// ZoneTradeState 用于存储每个交易区间的状态信息 +type ZoneTradeState struct { + LastTimes [3]int64 // 3个买入信号的最后交易时间 + LastSellTime int64 // 最后卖出时间 + atrBase float64 + score float64 + upFlag float64 + volRate float64 +} + +func J3782(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + maPeriod := pol.DefInt("maPeriod", 100, core.PNorm(20, 200)) + baseRetra := pol.Def("baseRetra", 0.08, core.PNorm(0.05, 0.2)) + retraRate := pol.Def("retraRate", 1.8, core.PNorm(1.5, 2.5)) + profitRate := pol.Def("profitRate", 0.03, core.PNorm(0.015, 0.06)) + tradeDelay := pol.Def("tradeDelay", 40, core.PNorm(10, 100)) + slRate := pol.Def("slRate", 63, core.PNorm(5, 35)) + openMaxs := [3]int{2, 8, 13} // 同一个bar最大同时允许开首单的数量 + holdMaxs := [3]int{25, 35, 50} // 最大同时持仓数 + buyWeis := [3]float64{1, 1.5, 3} // 不同信号开仓倍率 + maxJobTrades := [3]int{1, 3, 4} // 单个品种最大开仓数量 + + // 计算各回撤比例 + retracement2 := baseRetra + retracement3 := baseRetra * retraRate + retracement4 := baseRetra * retraRate * 1.33 + + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + rates := make([]float64, 0, 10) + + allOpens := 0 + openNums := [3]int{0, 0, 0} // 同一个bar开单数量 + holdNums := [3]int{0, 0, 0} // 总计持仓数量 + openUnits := 0.0 + var pairLock sync.Mutex + addThres, firstRate := 1.0, 1.0 + avgAtrBase := 2.5 + maxAllowUnit := 3.0 // 这里默认为品种数 + rangeVolRate := 1.0 // 全局成交量倍率 + + checkMaxPair := func(level int, openNum int) (int, float64) { + if openNum >= maxJobTrades[level] { + return -1, 0 + } + costRate := buyWeis[level] + pairLock.Lock() + if openUnits < maxAllowUnit && openNums[level] < openMaxs[level] && holdNums[level] < holdMaxs[level] { + openNums[level] += 1 + holdNums[level] += 1 + allOpens += 1 + openUnits += costRate + } else { + level = -1 + } + pairLock.Unlock() + return level, costRate + } + return &strat.TradeStrat{ + WarmupNum: maPeriod, + BatchInfo: true, + BatchInOut: true, + RunTimeFrames: []string{"15m"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化状态信息 + s.More = &ZoneTradeState{ + LastTimes: [3]int64{}, + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", baseAtrLen * 2}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*ZoneTradeState) + + // 计算MA100 + ma100 := ta.SMA(e.Close, maPeriod) + ma100Val := ma100.Get(0) + + // 计算交易价格水平 + curRate := max(addThres, 1+state.score/10) + itemRate := max(0.7, min(math.Sqrt(state.atrBase/avgAtrBase), 1.4)) + buyPrice2 := ma100Val * (1 - retracement2*curRate*itemRate*firstRate) + buyPrice3 := ma100Val * (1 - retracement3*curRate*itemRate) + buyPrice4 := ma100Val * (1 - retracement4*curRate*itemRate) + sellPrice := ma100Val * (1 + profitRate) + gapRange := (buyPrice2 - buyPrice4) / 2 + + maVol := ta.EMA(e.Volume, lenAtr) + volBase := ta.Lowest(ta.Highest(maVol, lenAtr), baseAtrLen).Get(0) + state.volRate = maVol.Get(0) / volBase + + // 获取当前价格 + lowPrice := e.Low.Get(0) + highPrice := e.High.Get(0) + + // 检查交易间隔条件 + currentBarTime := e.TimeStop + + lastEnt := max(state.LastTimes[0], state.LastTimes[1], state.LastTimes[2]) + canTrade := e.BarCount(lastEnt) >= 10 + canTrade2 := canTrade && e.BarCount(state.LastTimes[0]) >= tradeDelay + canTrade3 := canTrade && e.BarCount(state.LastTimes[1]) >= tradeDelay + canTrade4 := canTrade && e.BarCount(state.LastTimes[2]) >= tradeDelay + canSell := e.BarCount(state.LastSellTime) >= tradeDelay + + // 获取当前总开仓数量,与原始策略的strategy.opentrades保持一致 + openTrades := len(s.LongOrders) + len(s.ShortOrders) + + buyIdx, costRate := -1, 0.0 + if lowPrice <= buyPrice4 && canTrade4 { + buyIdx, costRate = checkMaxPair(2, openTrades) + } else if lowPrice <= buyPrice3 && canTrade3 { + buyIdx, costRate = checkMaxPair(1, openTrades) + } else if lowPrice <= buyPrice2 && canTrade2 { + buyIdx, costRate = checkMaxPair(0, openTrades) + } + if buyIdx >= 0 && costRate > 0 { + callNum := allOpens + //if buyIdx == 0 { + // // 对信号1,累计开单较多时,下移入场价 + // pairLock.Lock() + // callNum = max(callNum, int(math.Round(float64(holdNums[0])*0.5))-3) + // pairLock.Unlock() + //} + // 同一个bar开单多次,从第三次开始,使用ln(num)下移入场价 + currRate := 1 + max(0, math.Log10(float64(callNum-2))) + // 用倒抛物线映射成交量倍率,作为入场价距离调整系数 + volRate := 1 + max(0, 0.4-math.Pow(1.4*(rangeVolRate-1.5), 2)) + rangeRate := math.Pow(volRate*currRate, 0.3) - 1 + limitPrice := float64(0) + if (rangeVolRate > 1.2 || callNum > 2) && rangeRate > 0.01 { + limitPrice = e.Close.Get(0) - rangeRate*gapRange + } + tag := fmt.Sprintf("zone%d", buyIdx+1) + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + CostRate: costRate, + Limit: limitPrice, + Log: true, + StopBars: 3, + }) + state.LastTimes[buyIdx] = currentBarTime + } + + // 检查卖出条件 + if s.OrderNum > 0 { + curPrice := e.Close.Get(0) + slRange := slRate * itemRate * curPrice / 100 + avgPrice := s.PositionAvgPrice(core.OdDirtLong, "") + if curPrice+slRange <= avgPrice { + s.CloseOrders(&strat.ExitReq{ + Tag: "StopLoss", + Log: true, + }) + state.LastSellTime = currentBarTime + } else { + exitPrice := avgPrice*(1+profitRate)*0.4 + sellPrice*0.6 + if openTrades > 0 && highPrice >= exitPrice && canSell { + s.CloseOrders(&strat.ExitReq{ + Tag: "SellAll", + Log: true, + }) + state.LastSellTime = currentBarTime + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // 多指标综合计算大周期行情下跌分数 + score := float64(0) + if ta.RSI(e.Close, 14).Get(0) < 30 { + score += 1 + } + roc := ta.ROC(e.Close, 20).Get(0) + if roc < -20 { + score += 2 + } else if roc < -10 { + score += 1 + } + plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + if minusDi.Get(0) > plusDi.Get(0) { + if adx > 40 { + score += 2 + } else if adx > 25 { + score += 1 + } + } + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + rates = append(rates, atrBase/(e.Close.Get(0)*0.01)) + //openRate := 0.006 * closePrice / atrBase + //if openRate < 1 { + // openRate = math.Sqrt(openRate) + //} + //openRate = max(0.7, min(1.5, openRate)) + ma7 := ta.SMA(e.Close, 7).Get(0) + ma20 := ta.SMA(e.Close, 20) + ma20V := ma20.Get(0) + ma99 := ta.SMA(e.Close, 99).Get(0) + if ma99 > ma20V { + score += 1 + } + state, _ := s.More.(*ZoneTradeState) + state.score = score + state.upFlag = 0 + state.atrBase = atrBase / (e.Close.Get(0) * 0.01) + if ma20V > ma99 && ma20V > ma20.Get(2) { + state.upFlag = 1 + } else if ma7 < min(ma20V, ma99) { + state.upFlag = -1 + } + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + pairLock.Lock() + openUnits = 0 + allOpens = 0 + openNums = [3]int{0, 0, 0} + holdNums = [3]int{0, 0, 0} + volRates := make([]float64, 0, len(jobs)) + for _, j := range jobs { + for _, od := range j.LongOrders { + if od.EnterTag == "zone1" { + holdNums[0] += 1 + } else if od.EnterTag == "zone2" { + holdNums[1] += 1 + } else if od.EnterTag == "zone3" { + holdNums[2] += 1 + } + } + m, _ := j.More.(*ZoneTradeState) + volRates = append(volRates, m.volRate) + } + pairLock.Unlock() + sort.Float64s(volRates) + rangeVolRate = max(1, math.Pow(volRates[len(volRates)/2]/2, 0.3)) + }, + OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) { + if len(jobs) < 3 { + return + } + // 多品种综合计算整体市场下行激烈程度 + downNum, downHeavyNum := float64(0), float64(0) + atrRates := make([]float64, 0, len(jobs)) + for _, j := range jobs { + state, _ := j.Job.More.(*ZoneTradeState) + if state.score >= 5 { + downHeavyNum += 1 + } + if state.score >= 3 { + downNum += 1 + } + atrRates = append(atrRates, state.atrBase) + } + jobNum := float64(len(jobs)) + downRate := downNum / jobNum + downHeavyRate := downHeavyNum / jobNum + sort.Float64s(atrRates) + avgAtrBase = atrRates[len(jobs)/2] + if downHeavyRate >= 0.8 { + // 极端下跌 + addThres, firstRate = 1.6, 1.2 + maxAllowUnit = jobNum * 0.5 + } else if downRate >= 0.85 && downHeavyRate >= 0.6 { + // 严重下跌 + addThres, firstRate = 1.45, 1.15 + maxAllowUnit = jobNum * 0.55 + } else if downRate >= 0.7 && downRate <= 0.85 { + // 市场中度下跌 + addThres, firstRate = 1.3, 1.1 + maxAllowUnit = jobNum * 0.6 + } else if downRate >= 0.5 && downRate <= 0.7 { + // 轻度下跌 + addThres, firstRate = 1.15, 1.05 + maxAllowUnit = jobNum * 0.7 + } else { + // 轻度或未下跌 + maxAllowUnit = jobNum + addThres, firstRate = 1, 1 + } + }, + } +} diff --git a/llmjob/3827.go b/llmjob/3827.go new file mode 100644 index 0000000..3b02517 --- /dev/null +++ b/llmjob/3827.go @@ -0,0 +1,144 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +// FibRSIGoldenCross 用于存储每个品种的斐波那契回撤水平 +type FibRSIGoldenCross struct { + FibHigh float64 + FibLow float64 + Fib38 float64 + Fib50 float64 + Fib61 float64 +} + +func J3827(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + short_ma_length := pol.DefInt("short_ma_length", 40, core.PNorm(20, 100)) + ma_ratio := pol.Def("ma_ratio", 3.8, core.PNorm(2, 6)) + rsi_length := pol.DefInt("rsi_length", 14, core.PNorm(7, 21)) + rsi_overbought := pol.Def("rsi_overbought", 70, core.PNorm(60, 80)) + rsi_oversold := pol.Def("rsi_oversold", 30, core.PNorm(20, 40)) + fib_period := pol.DefInt("fib_period", 50, core.PNorm(20, 100)) + + // 计算长期均线周期 + long_ma_length := int(float64(short_ma_length) * ma_ratio) + + // 计算预热数量,取所有指标中最大的周期 + warmup_num := int(math.Max(float64(long_ma_length), float64(fib_period))) + + return &strat.TradeStrat{ + WarmupNum: warmup_num, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + TimeFrames: "8h", + OnStartUp: func(s *strat.StratJob) { + // 初始化斐波那契回撤水平 + s.More = &FibRSIGoldenCross{ + FibHigh: math.NaN(), + FibLow: math.NaN(), + Fib38: math.NaN(), + Fib50: math.NaN(), + Fib61: math.NaN(), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + fib, _ := s.More.(*FibRSIGoldenCross) + + // 计算技术指标 + rsi := ta.RSI(e.Close, rsi_length) + short_ma := ta.SMA(e.Close, short_ma_length) + long_ma := ta.SMA(e.Close, long_ma_length) + + // 获取当前价格 + close_price := e.Close.Get(0) + if math.IsNaN(close_price) { + return + } + + // 更新斐波那契回撤水平 + highest := ta.Highest(e.Close, fib_period).Get(0) + lowest := ta.Lowest(e.Close, fib_period).Get(0) + + // 如果最高价或最低价发生变化,更新斐波那契回撤水平 + if !math.IsNaN(highest) && (math.IsNaN(fib.FibHigh) || highest != fib.FibHigh) { + fib.FibHigh = highest + } + if !math.IsNaN(lowest) && (math.IsNaN(fib.FibLow) || lowest != fib.FibLow) { + fib.FibLow = lowest + } + + // 计算斐波那契回撤水平 + if !math.IsNaN(fib.FibHigh) && !math.IsNaN(fib.FibLow) { + fib_range := fib.FibHigh - fib.FibLow + fib.Fib38 = fib.FibHigh - fib_range*0.382 + fib.Fib50 = fib.FibHigh - fib_range*0.5 + fib.Fib61 = fib.FibHigh - fib_range*0.618 + } + + // 获取当前指标值 + rsi_value := rsi.Get(0) + short_ma_value := short_ma.Get(0) + long_ma_value := long_ma.Get(0) + + // 检查指标是否有效 + if math.IsNaN(rsi_value) || math.IsNaN(short_ma_value) || math.IsNaN(long_ma_value) || + math.IsNaN(fib.Fib50) { + return + } + + // 检测交叉信号 - 只计算一次交叉信号 + cross_signal := short_ma.Cross(long_ma) + golden_cross := cross_signal == 1 // 1表示上穿 + death_cross := cross_signal == -1 // -1表示下穿 + + // 检查当前持仓 + long_position := s.Position(1, "") > 0 // 1表示多头 + short_position := s.Position(-1, "") > 0 // -1表示空头 + + // 多头入场条件:黄金交叉且价格在50%斐波那契水平之上且RSI未超买 + long_condition := golden_cross && close_price > fib.Fib50 && rsi_value < rsi_overbought + + // 空头入场条件:死亡交叉且价格在50%斐波那契水平之下且RSI未超卖 + short_condition := death_cross && close_price < fib.Fib50 && rsi_value > rsi_oversold + + // 执行交易 + if long_condition && !long_position { + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + }) + } + + if short_condition && !short_position { + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + }) + } + + // 多头平仓条件:RSI超买 + if long_position && rsi_value > rsi_overbought { + s.CloseOrders(&strat.ExitReq{ + Tag: "Buy", + Dirt: core.OdDirtLong, + }) + } + + // 空头平仓条件:RSI超卖 + if short_position && rsi_value < rsi_oversold { + s.CloseOrders(&strat.ExitReq{ + Tag: "Sell", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/3939.go b/llmjob/3939.go new file mode 100644 index 0000000..0e67596 --- /dev/null +++ b/llmjob/3939.go @@ -0,0 +1,160 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J3939(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + lwstPeriod := pol.DefInt("lwst_period", 10, core.PNorm(5, 25)) + lwstMultiplier := pol.Def("lwst_multiplier", 3.0, core.PNorm(1.5, 5.5)) + lwstLength := pol.DefInt("lwst_length", 20, core.PNorm(10, 40)) + trendLength := pol.DefInt("trend_length", 14, core.PNorm(7, 25)) + trendThreshold := pol.Def("trend_threshold", 0.5, core.PNorm(0.1, 2.0)) + wtChannelLength := pol.DefInt("wt_channel_length", 9, core.PNorm(5, 20)) + wtAverageLength := pol.DefInt("wt_average_length", 12, core.PNorm(6, 24)) + wtMaLength := pol.DefInt("wt_ma_length", 3, core.PNorm(2, 8)) + wtOverbought := pol.Def("wt_overbought", 53, core.PNorm(40, 70)) + wtOversold := pol.Def("wt_oversold", -53, core.PNorm(-70, -40)) // 修复:独立的超卖参数 + slPercent := pol.Def("sl_percent", 2.0, core.PNorm(0.5, 5.0)) + tpPercent := pol.Def("tp_percent", 4.0, core.PNorm(1.0, 10.0)) + + warmupNum := max(lwstPeriod*2, trendLength*2, wtChannelLength+wtAverageLength) + + return &strat.TradeStrat{ + Name: "Multi-Indicator-Consensus", + WarmupNum: warmupNum + 50, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, + EachMaxShort: 1, + + OnBar: func(s *strat.StratJob) { + e := s.Env + currentPrice := e.Close.Get(0) + + // =================== Indicator 1: Liquidity Weighted Supertrend =================== + // Volume-weighted component for dynamic sensitivity + volSma := ta.SMA(e.Volume, lwstLength) + volWeight := e.Volume.Div(volSma) + + // ATR-based bands with volume weighting + atr := ta.ATR(e.High, e.Low, e.Close, lwstPeriod) + hl2 := ta.HL2(e.High, e.Low) + _ = hl2.Add(atr.Mul(lwstMultiplier).Mul(volWeight)) // upperBand + _ = hl2.Sub(atr.Mul(lwstMultiplier).Mul(volWeight)) // lowerBand + + // Trend determination based on price action and previous trend + // 修复:使用Series保存趋势状态 + lwstTrendSeries := e.Close.To("_lwst_trend", 0) + var lwstTrend float64 + if e.Close.Len() > 1 { + prevTrend := lwstTrendSeries.Get(1) + if currentPrice > prevTrend { + lwstTrend = 1 + } else if currentPrice < prevTrend { + lwstTrend = -1 + } else { + lwstTrend = prevTrend + } + } else { + lwstTrend = 1.0 + } + lwstTrendSeries.Set(lwstTrend) + + // =================== Indicator 2: Trend Signals =================== + // Dual EMA system for trend detection + fastEma := ta.EMA(e.Close, trendLength) + slowEma := ta.EMA(e.Close, trendLength*2) + trendDiff := fastEma.Sub(slowEma).Div(slowEma).Mul(100) + trendDiffVal := trendDiff.Get(0) + + var trendSignal float64 + if trendDiffVal > trendThreshold { + trendSignal = 1 + } else if trendDiffVal < -trendThreshold { + trendSignal = -1 + } else { + trendSignal = 0 + } + + // =================== Indicator 3: Enhanced Wavetrend =================== + // Calculate Wavetrend components + ap := ta.HLC3(e.High, e.Low, e.Close) + esa := ta.EMA(ap, wtChannelLength) + d := ta.EMA(ap.Sub(esa).Abs(), wtChannelLength) + ci := ap.Sub(esa).Div(d.Mul(0.015)) + tci := ta.EMA(ci, wtAverageLength) + + // Generate main and signal lines + wt1 := tci + wt2 := ta.SMA(wt1, wtMaLength) + + // Generate Wavetrend Signal based on overbought/oversold conditions + wt1Val := wt1.Get(0) + wt2Val := wt2.Get(0) + + // 修复:使用Series保存wt_signal状态 + wtSignalSeries := e.Close.To("_wt_signal", 0) + var wtSignal float64 + if wt1Val > wtOverbought && wt2Val > wtOverbought { + wtSignal = -1 // 超买信号 + } else if wt1Val < wtOversold && wt2Val < wtOversold { // 修复:使用独立的wtOversold参数 + wtSignal = 1 // 超卖信号 + } else { + wtSignal = wtSignalSeries.Get(1) + } + wtSignalSeries.Set(wtSignal) + + // =================== Consensus Signal Generation =================== + // 修复:正确计算净信号数(范围-3到3) + consensusCount := 0 + if lwstTrend == 1 { + consensusCount++ + } else if lwstTrend == -1 { + consensusCount-- + } + if trendSignal == 1 { + consensusCount++ + } else if trendSignal == -1 { + consensusCount-- + } + if wtSignal == 1 { + consensusCount++ + } else if wtSignal == -1 { + consensusCount-- + } + + // Generate trading signals when majority (2+ indicators) agree + buySignal := consensusCount >= 2 + sellSignal := consensusCount <= -2 + + // =================== Trade Execution =================== + if buySignal && len(s.LongOrders) == 0 && len(s.ShortOrders) == 0 { + // 计算止损止盈价格 + slAmount := currentPrice * slPercent / 100 + tpAmount := currentPrice * tpPercent / 100 + + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + StopLossVal: slAmount, + TakeProfitVal: tpAmount, + }) + } else if sellSignal && len(s.ShortOrders) == 0 && len(s.LongOrders) == 0 { + // 计算做空止损止盈价格 + slAmount := currentPrice * slPercent / 100 + tpAmount := currentPrice * tpPercent / 100 + + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Short: false, + StopLossVal: slAmount, + TakeProfitVal: tpAmount, + }) + } + }, + } +} diff --git a/llmjob/3955.go b/llmjob/3955.go new file mode 100644 index 0000000..62cb825 --- /dev/null +++ b/llmjob/3955.go @@ -0,0 +1,109 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J3955(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + macdFastPeriod := pol.DefInt("macdFastPeriod", 14, core.PNorm(5, 20)) + macdSlowRate := pol.Def("macdSlowRate", 2, core.PNorm(1.5, 4)) + macdSignalPeriod := pol.DefInt("macdSignalPeriod", 9, core.PNorm(5, 15)) + vwmaFastPeriod := pol.DefInt("vwmaFastPeriod", 20, core.PNorm(10, 40)) + leverage := pol.DefInt("leverage", 1, core.PNorm(1, 20)) + + // 计算MACD慢线周期 + macdSlowPeriod := int(float64(macdFastPeriod) * macdSlowRate) + + return &strat.TradeStrat{ + WarmupNum: macdSlowPeriod + macdSignalPeriod, // 确保有足够的数据计算指标 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算MACD指标 + macdLine, signalLine := ta.MACD(e.Close, macdFastPeriod, macdSlowPeriod, macdSignalPeriod) + histogram := macdLine.Sub(signalLine) // 计算MACD柱状图 + + // 计算VWMA指标 + vwma20 := ta.VWMA(e.Close, e.Volume, vwmaFastPeriod) // 使用VWMA替代WMA,考虑成交量因素 + vwma50 := ta.VWMA(e.Close, e.Volume, 50) // 使用VWMA替代WMA,考虑成交量因素 + + // 获取当前指标值 + macdLineVal := macdLine.Get(0) + signalLineVal := signalLine.Get(0) + histogramVal := histogram.Get(0) + vwma20Val := vwma20.Get(0) + vwma50Val := vwma50.Get(0) + + // 检查数据有效性 + if math.IsNaN(macdLineVal) || math.IsNaN(signalLineVal) || math.IsNaN(histogramVal) || + math.IsNaN(vwma20Val) || math.IsNaN(vwma50Val) { + return + } + + // 生成MACD信号 + macdLongEntrySignal := histogramVal > 0 + macdShortEntrySignal := histogramVal < 0 + + // 生成VWMA信号 + vwmaLongEntrySignal := vwma20Val > vwma50Val + vwmaShortEntrySignal := vwma20Val < vwma50Val + + // 生成组合交易信号 + longEntry := macdLongEntrySignal && vwmaLongEntrySignal + shortEntry := macdShortEntrySignal && vwmaShortEntrySignal + + // 检测MACD交叉信号作为出场条件(优化为单次调用) + cross := macdLine.Cross(signalLine) + longExit := cross == -1 // MACD线下穿信号线 + shortExit := cross == 1 // MACD线上穿信号线 + + // 执行交易逻辑 + if longEntry && len(s.LongOrders) == 0 { + // 计算杠杆化后的合约数量 + costRate := float64(leverage) + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Short: false, + CostRate: costRate, + Leverage: float64(leverage), + OrderType: core.OrderTypeMarket, + }) + } + + if longExit && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "LongExit", + Dirt: core.OdDirtLong, + }) + } + + if shortEntry && len(s.ShortOrders) == 0 { + // 计算杠杆化后的合约数量 + costRate := float64(leverage) + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + CostRate: costRate, + Leverage: float64(leverage), + OrderType: core.OrderTypeMarket, + }) + } + + if shortExit && len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "ShortExit", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/3956.go b/llmjob/3956.go new file mode 100644 index 0000000..334f48e --- /dev/null +++ b/llmjob/3956.go @@ -0,0 +1,94 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J3956(pol *config.RunPolicyConfig) *strat.TradeStrat { + fastLength := pol.DefInt("fastLength", 9, core.PNorm(5, 20)) + slowMaRate := pol.Def("slowMaRate", 2.17, core.PNorm(1.5, 4)) + signalLength := pol.DefInt("signalLength", 20, core.PNorm(9, 30)) + entryLevel := pol.Def("entryLevel", 0, core.PNorm(-5, 5)) + longExitLevel := pol.Def("longExitLevel", 11, core.PNorm(5, 20)) + shortExitLevel := pol.Def("shortExitLevel", -9, core.PNorm(-20, -5)) + + // 计算慢速EMA周期 + slowLength := int(float64(fastLength) * slowMaRate) + + // 确定预热数量,取所有参数中的最大值 + warmupNum := fastLength + if slowLength > warmupNum { + warmupNum = slowLength + } + if signalLength > warmupNum { + warmupNum = signalLength + } + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算MACD指标 + fastEma := ta.EMA(e.Close, fastLength) + slowEma := ta.EMA(e.Close, slowLength) + macd := fastEma.Sub(slowEma) + signal := ta.EMA(macd, signalLength) + hist := macd.Sub(signal) + + // 获取当前值 + histVal := hist.Get(0) + if math.IsNaN(histVal) { + return + } + + // 检查交叉信号 + entryCross := hist.Cross(entryLevel) + longEntry := entryCross == 1 // 上穿入场水平线 + shortEntry := entryCross == -1 // 下穿入场水平线 + longExit := hist.Cross(longExitLevel) == 1 // 上穿多头出场水平线 + shortExit := hist.Cross(shortExitLevel) == -1 // 下穿空头出场水平线 + + // 获取当前持仓状态 + longPos := s.Position(core.OdDirtLong, "") + shortPos := s.Position(core.OdDirtShort, "") + + // 多头入场 + if longEntry && longPos == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + }) + } + + // 空头入场 + if shortEntry && shortPos == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + }) + } + + // 多头出场 + if longPos > 0 && longExit { + s.CloseOrders(&strat.ExitReq{ + Tag: "Long Exit", + Dirt: core.OdDirtLong, + }) + } + + // 空头出场 + if shortPos < 0 && shortExit { + s.CloseOrders(&strat.ExitReq{ + Tag: "Short Exit", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/3992.go b/llmjob/3992.go new file mode 100644 index 0000000..196fab1 --- /dev/null +++ b/llmjob/3992.go @@ -0,0 +1,113 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J3992(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastLength := pol.DefInt("fastLength", 10, core.PUniform(5, 20)) + macdSlowRate := pol.Def("macdSlowRate", 2.17, core.PNorm(1.5, 4)) + rsiThreshold := pol.DefInt("rsiThreshold", 65, core.PUniform(60, 80)) + bbMult := pol.Def("bbMult", 2.0, core.PNorm(1.5, 3.0)) + rsiLength := pol.DefInt("rsiLength", 14, core.PUniform(7, 21)) + + // 计算其他参数 + MACDLength := 9 // 固定值 + slowLength := int(float64(fastLength) * macdSlowRate) + rsiOverbought := float64(rsiThreshold) + rsiOversold := 100 - rsiOverbought + stopLossPercent := 0.02 // 2% stop loss + takeProfitPercent := 0.05 // 5% take profit + + // 计算预热数量,取最大值 + warmupNum := slowLength + if MACDLength > warmupNum { + warmupNum = MACDLength + } + if rsiLength > warmupNum { + warmupNum = rsiLength + } + if bbLength := 20; bbLength > warmupNum { + warmupNum = bbLength + } + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 检查数据长度是否足够 + if e.Close.Len() < warmupNum { + return + } + + // MACD calculations + fastMA := ta.EMA(e.Close, fastLength) + slowMA := ta.EMA(e.Close, slowLength) + macdLine := fastMA.Sub(slowMA) + signalLine := ta.EMA(macdLine, MACDLength) + + // RSI calculation + rsi := ta.RSI(e.Close, rsiLength) + + // Bollinger Bands calculation + bbLength := 20 + basis := ta.SMA(e.Close, bbLength) + stdDev := ta.StdDev(e.Close, bbLength) + dev := stdDev.Mul(bbMult) + upper := basis.Add(dev) + lower := basis.Sub(dev) + + // 获取当前值 + closePrice := e.Close.Get(0) + macdVal := macdLine.Get(0) + signalVal := signalLine.Get(0) + rsiVal := rsi.Get(0) + upperVal := upper.Get(0) + lowerVal := lower.Get(0) + + // 检查NaN值 + if math.IsNaN(closePrice) || math.IsNaN(macdVal) || math.IsNaN(signalVal) || + math.IsNaN(rsiVal) || math.IsNaN(upperVal) || math.IsNaN(lowerVal) { + return + } + + // Entry conditions + macdCrossOver := macdLine.Cross(signalLine) == 1 + rsiCrossUnder := rsi.Cross(rsiOversold) == -1 + longCondition := (macdCrossOver || rsiCrossUnder) && closePrice > lowerVal + + macdCrossUnder := macdLine.Cross(signalLine) == -1 + rsiCrossOver := rsi.Cross(rsiOverbought) == 1 + shortCondition := (macdCrossUnder || rsiCrossOver) && closePrice < upperVal + + // Long position logic + if longCondition && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + OrderType: core.OrderTypeMarket, + StopLossVal: closePrice * (1 - stopLossPercent), + TakeProfitVal: closePrice * (1 + takeProfitPercent), + }) + } + + // Short position logic + if shortCondition && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + OrderType: core.OrderTypeMarket, + StopLossVal: closePrice * (1 + stopLossPercent), + TakeProfitVal: closePrice * (1 - takeProfitPercent), + }) + } + }, + } +} diff --git a/llmjob/4009.go b/llmjob/4009.go new file mode 100644 index 0000000..136ea78 --- /dev/null +++ b/llmjob/4009.go @@ -0,0 +1,177 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +// Sta4009 存储每个品种的策略状态 +type Sta4009 struct { + atrTrailingStop float64 // ATR跟踪止损值 + position int // 当前持仓方向(1=多头,-1=空头,0=无持仓) + entryPrice float64 // 入场价格 + buyTarget float64 // 多头目标价格 + sellTarget float64 // 空头目标价格 +} + +func J4009(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + keyValue := pol.Def("keyValue", 1, core.PNorm(0.1, 5)) + atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(5, 30)) + useHeikinAshi := pol.DefInt("useHeikinAshi", 0, core.PNorm(0, 1)) // 0=false, 1=true + exitPercentage := pol.Def("exitPercentage", 0.002, core.PNorm(0.001, 0.01)) + rsiPeriod := pol.DefInt("rsiPeriod", 14, core.PNorm(7, 30)) + donchianLength := pol.DefInt("donchianLength", 20, core.PNorm(10, 50)) + + return &strat.TradeStrat{ + WarmupNum: donchianLength + 20, // 确保有足够数据计算所有指标 + HedgeOff: true, // 单向持仓模式 + TimeFrames: "1d", + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + s.More = &Sta4009{ + atrTrailingStop: math.NaN(), + position: 0, + entryPrice: math.NaN(), + buyTarget: math.NaN(), + sellTarget: math.NaN(), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*Sta4009) + + // 计算基础指标 + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + lossDistance := keyValue * atr.Get(0) + rsiValue := ta.RSI(e.Close, rsiPeriod).Get(0) + + // 计算价格源(普通收盘价或惠金-阿希收盘价) + var priceSource *ta.Series + if useHeikinAshi > 0 { + haOpen, haHigh, haLow, haClose := ta.HeikinAshi(e) + haCloseSeries := haOpen.Add(haHigh).Add(haLow).Add(haClose).Div(4) + priceSource = haCloseSeries + } else { + priceSource = e.Close + } + currentPrice := priceSource.Get(0) + prevPrice := priceSource.Get(1) + + // 计算ATR跟踪止损 + if !math.IsNaN(lossDistance) && !math.IsNaN(currentPrice) && !math.IsNaN(prevPrice) { + prevStop := state.atrTrailingStop + if math.IsNaN(prevStop) { + prevStop = 0 + } + + if currentPrice > prevStop && prevPrice > prevStop { + // 价格在止损线上方且前一价格也在止损线上方,止损线上移 + state.atrTrailingStop = math.Max(prevStop, currentPrice-lossDistance) + } else if currentPrice < prevStop && prevPrice < prevStop { + // 价格在止损线下方且前一价格也在止损线下方,止损线下移 + state.atrTrailingStop = math.Min(prevStop, currentPrice+lossDistance) + } else if currentPrice > prevStop { + // 价格上穿止损线,设置新的止损线 + state.atrTrailingStop = currentPrice - lossDistance + } else { + // 价格下穿止损线,设置新的止损线 + state.atrTrailingStop = currentPrice + lossDistance + } + } + + // 计算持仓方向 + if !math.IsNaN(prevPrice) && !math.IsNaN(state.atrTrailingStop) { + if prevPrice < state.atrTrailingStop && currentPrice > state.atrTrailingStop { + state.position = 1 // 多头信号 + } else if prevPrice > state.atrTrailingStop && currentPrice < state.atrTrailingStop { + state.position = -1 // 空头信号 + } + // 否则保持原有方向 + } + + // 计算唐奇安通道 + donchianUpper := ta.Highest(e.High, donchianLength) + donchianLower := ta.Lowest(e.Low, donchianLength) + donchianBasis := donchianUpper.Add(donchianLower).Div(2).Get(0) + + // 计算交叉信号 + above := currentPrice > state.atrTrailingStop && prevPrice <= state.atrTrailingStop + below := currentPrice < state.atrTrailingStop && prevPrice >= state.atrTrailingStop + + // 生成交易信号 + buySignal := currentPrice > state.atrTrailingStop && above && rsiValue > 50 && currentPrice > donchianBasis + sellSignal := currentPrice < state.atrTrailingStop && below && rsiValue < 50 && currentPrice < donchianBasis + + // 执行交易 + if buySignal && s.OrderNum == 0 { + state.entryPrice = currentPrice + state.buyTarget = state.entryPrice * (1 + exitPercentage) + state.sellTarget = state.entryPrice * (1 - exitPercentage) + + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + OrderType: core.OrderTypeMarket, + }) + } else if sellSignal && s.OrderNum == 0 { + state.entryPrice = currentPrice + state.buyTarget = state.entryPrice * (1 + exitPercentage) + state.sellTarget = state.entryPrice * (1 - exitPercentage) + + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + OrderType: core.OrderTypeMarket, + }) + } + + // 退出条件检查 + if len(s.LongOrders) > 0 { + od := s.LongOrders[0] + if od.Status >= ormo.InOutStatusFullEnter { + if currentPrice >= state.buyTarget { + // 达到止盈目标 + s.CloseOrders(&strat.ExitReq{ + Tag: "Take Profit", + Dirt: core.OdDirtLong, + Limit: state.buyTarget, + }) + } else if currentPrice <= state.sellTarget { + // 触发止损 + s.CloseOrders(&strat.ExitReq{ + Tag: "Stop Loss", + Dirt: core.OdDirtLong, + Limit: state.sellTarget, + }) + } + } + } + + if len(s.ShortOrders) > 0 { + od := s.ShortOrders[0] + if od.Status >= ormo.InOutStatusFullEnter { + if currentPrice <= state.sellTarget { + // 达到止盈目标 + s.CloseOrders(&strat.ExitReq{ + Tag: "Take Profit", + Dirt: core.OdDirtShort, + Limit: state.sellTarget, + }) + } else if currentPrice >= state.buyTarget { + // 触发止损 + s.CloseOrders(&strat.ExitReq{ + Tag: "Stop Loss", + Dirt: core.OdDirtShort, + Limit: state.buyTarget, + }) + } + } + } + }, + } +} diff --git a/llmjob/4034.go b/llmjob/4034.go new file mode 100644 index 0000000..5575931 --- /dev/null +++ b/llmjob/4034.go @@ -0,0 +1,128 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// 定义一个结构体来存储策略需要的变量 +type StrategyData struct { + atrValue float64 +} + +func J4034(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + fastLength := pol.DefInt("fastLength", 12, core.PNorm(5, 20)) + emaRate := pol.Def("emaRate", 2.33, core.PNorm(1.5, 5.0)) + rsiLength := pol.DefInt("rsiLength", 14, core.PNorm(7, 30)) + rsiOverbought := pol.DefInt("rsiOverbought", 70, core.PNorm(60, 85)) + rsiOversold := pol.DefInt("rsiOversold", 30, core.PNorm(15, 40)) + volLength := pol.DefInt("volLength", 20, core.PNorm(10, 40)) + atrLength := pol.DefInt("atrLength", 14, core.PNorm(7, 30)) + atrMultiplierSL := pol.Def("atrMultiplierSL", 1.8, core.PNorm(1.0, 3.0)) + atrMultiplierTP := pol.Def("atrMultiplierTP", 2.5, core.PNorm(1.5, 5.0)) + + // 计算慢速EMA周期 + slowLength := int(float64(fastLength) * emaRate) + + // 确定预热数量,取所有指标中最大的周期 + warmupNum := fastLength + if slowLength > warmupNum { + warmupNum = slowLength + } + if rsiLength > warmupNum { + warmupNum = rsiLength + } + if volLength > warmupNum { + warmupNum = volLength + } + if atrLength > warmupNum { + warmupNum = atrLength + } + // 额外增加1个K线用于突破确认 + warmupNum += 1 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + EachMaxLong: 1, // 最多同时持有一个多头订单 + EachMaxShort: 1, // 最多同时持有一个空头订单 + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化策略数据 + s.More = &StrategyData{atrValue: 0} + }, + + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + fastEMA := ta.EMA(e.Close, fastLength) + slowEMA := ta.EMA(e.Close, slowLength) + rsiValue := ta.RSI(e.Close, rsiLength) + volMA := ta.SMA(e.Volume, volLength) + atr := ta.ATR(e.High, e.Low, e.Close, atrLength) + + // 获取当前值 + closePrice := e.Close.Get(0) + highPrev := e.High.Get(1) + lowPrev := e.Low.Get(1) + volume := e.Volume.Get(0) + volMAValue := volMA.Get(0) + rsi := rsiValue.Get(0) + atrValue := atr.Get(0) + maCross := fastEMA.Cross(slowEMA) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(highPrev) || math.IsNaN(lowPrev) || + math.IsNaN(volume) || math.IsNaN(volMAValue) || math.IsNaN(rsi) || math.IsNaN(atrValue) { + return + } + + // 存储ATR值以便在跟踪止损中使用 + data, _ := s.More.(*StrategyData) + data.atrValue = atrValue + + // K线突破条件 + longCandleBreak := closePrice > highPrev + shortCandleBreak := closePrice < lowPrev + + // 入场条件 + longCondition := maCross == 1 && rsi < float64(rsiOverbought) && volume > volMAValue && longCandleBreak + shortCondition := maCross == -1 && rsi > float64(rsiOversold) && volume > volMAValue && shortCandleBreak + + // 执行交易 + if longCondition && len(s.LongOrders) == 0 { + // 计算具体止损止盈价格 + longStop := closePrice - atrMultiplierSL*atrValue + longTP := closePrice + atrMultiplierTP*atrValue + + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Short: false, + OrderType: core.OrderTypeMarket, + StopLoss: longStop, + TakeProfit: longTP, + }) + } + + if shortCondition && len(s.ShortOrders) == 0 { + // 计算具体止损止盈价格 + shortStop := closePrice + atrMultiplierSL*atrValue + shortTP := closePrice - atrMultiplierTP*atrValue + + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + OrderType: core.OrderTypeMarket, + StopLoss: shortStop, + TakeProfit: shortTP, + }) + } + }, + } +} diff --git a/llmjob/4067.go b/llmjob/4067.go new file mode 100644 index 0000000..22ec291 --- /dev/null +++ b/llmjob/4067.go @@ -0,0 +1,477 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "time" +) + +// State4067 存储每个品种的策略状态 +type State4067 struct { + step int + inTrade bool + tradeDirection int // 1=多头, -1=空头, 0=无 + entryPrice float64 + tp1Hit bool + buytp1Hit bool + buySLPrice float64 + buyTP1Price float64 + buyTP2Price float64 + selltp1Hit bool + sellSLPrice float64 + sellTP1Price float64 + sellTP2Price float64 + higherEMA float64 // 更高时间周期EMA值 +} + +// MTFCross 多时间周期EMA交叉策略 +func J4067(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + fastPeriod := pol.DefInt("fastPeriod", 9, core.PNorm(5, 20)) + slowRate := pol.Def("slowRate", 5.56, core.PNorm(2, 10)) + higherEMARate := pol.Def("higherEMARate", 2, core.PNorm(1.5, 5)) + slRate := pol.Def("slRate", 1.6, core.PNorm(0.5, 5)) + tp1Rate := pol.Def("tp1Rate", 1.4, core.PNorm(0.5, 3)) + beoffRate := pol.Def("beoffRate", 0.3, core.PNorm(0.1, 1)) + atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 30)) + + // 交易时间控制参数 + startHour := pol.DefInt("startHour", 1, core.PNorm(0, 23)) + endHour := pol.DefInt("endHour", 25, core.PNorm(0, 47)) + tradeOnMonday := pol.DefInt("tradeOnMonday", 1, core.PNorm(0, 1)) != 0 + tradeOnTuesday := pol.DefInt("tradeOnTuesday", 1, core.PNorm(0, 1)) != 0 + tradeOnWednesday := pol.DefInt("tradeOnWednesday", 1, core.PNorm(0, 1)) != 0 + tradeOnThursday := pol.DefInt("tradeOnThursday", 1, core.PNorm(0, 1)) != 0 + tradeOnFriday := pol.DefInt("tradeOnFriday", 1, core.PNorm(0, 1)) != 0 + + // 计算衍生参数 + slowPeriod := int(float64(fastPeriod) * slowRate) + higherPeriod := int(float64(slowPeriod) * higherEMARate) + + // 调整endHour为24小时格式 + adjustedEndHour := endHour % 24 + + return &strat.TradeStrat{ + WarmupNum: slowPeriod, // 预热K线数量 + HedgeOff: true, // 单向持仓 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + RunTimeFrames: []string{"4h"}, + // 初始化策略状态 + OnStartUp: func(s *strat.StratJob) { + s.More = &State4067{ + step: 0, + inTrade: false, + tradeDirection: 0, + entryPrice: math.NaN(), + tp1Hit: false, + buytp1Hit: false, + selltp1Hit: false, + buySLPrice: math.NaN(), + buyTP1Price: math.NaN(), + buyTP2Price: math.NaN(), + sellSLPrice: math.NaN(), + sellTP1Price: math.NaN(), + sellTP2Price: math.NaN(), + higherEMA: math.NaN(), + } + }, + + // 定义需要订阅的其他时间周期数据 + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {Pair: "_cur_", TimeFrame: "15m", WarmupNum: higherPeriod}, + } + }, + + // 处理更高时间周期数据 + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + state, _ := s.More.(*State4067) + // 计算更高时间周期的EMA + higherEMA := ta.EMA(e.Close, higherPeriod) + if higherEMA.Len() > 0 { + state.higherEMA = higherEMA.Get(0) + } + }, + + // 主策略逻辑 + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*State4067) + + // 计算技术指标 + fastEMA := ta.EMA(e.Close, fastPeriod) + slowEMA := ta.EMA(e.Close, slowPeriod) + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + + // 获取当前值 + closePrice := e.Close.Get(0) + fastEMAV := fastEMA.Get(0) + slowEMAV := slowEMA.Get(0) + atrV := atr.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(fastEMAV) || math.IsNaN(slowEMAV) || + math.IsNaN(atrV) || math.IsNaN(state.higherEMA) { + return + } + + // 计算止损止盈距离 + slDist := atrV * slRate + tp1Dist := atrV * tp1Rate + tp2Dist := atrV * tp1Rate * 2 // 第二止盈是第一止盈的2倍 + beoffDist := atrV * beoffRate + + // 检查交易时间 + t := time.UnixMilli(e.TimeStart) + isTradingTime := checkTradingTime(t, startHour, adjustedEndHour, + tradeOnMonday, tradeOnTuesday, tradeOnWednesday, tradeOnThursday, tradeOnFriday) + + // 更新信号状态步骤 + updateStep(state, fastEMA, slowEMA) + + // 生成交易信号(仅在交易时间内) + if isTradingTime { + if state.step == 1 && !state.inTrade && fastEMAV > slowEMAV && fastEMAV > state.higherEMA { + // 买入信号 + state.inTrade = true + state.tradeDirection = 1 + state.entryPrice = closePrice + state.buytp1Hit = false + state.tp1Hit = false + state.buySLPrice = state.entryPrice - slDist + state.buyTP1Price = state.entryPrice + tp1Dist + state.buyTP2Price = state.entryPrice + tp2Dist + state.step = 3 + + // 开多单 + //s.OpenOrder(&strat.EnterReq{ + // Tag: "Buy", + // Short: false, + // StopLoss: state.buySLPrice, + // TakeProfit: state.buyTP1Price, + // StopLossVal: slDist, + //}) + } else if state.step == 2 && !state.inTrade && fastEMAV < slowEMAV && fastEMAV < state.higherEMA { + // 卖出信号 + state.inTrade = true + state.tradeDirection = -1 + state.entryPrice = closePrice + state.selltp1Hit = false + state.tp1Hit = false + state.sellSLPrice = state.entryPrice + slDist + state.sellTP1Price = state.entryPrice - tp1Dist + state.sellTP2Price = state.entryPrice - tp2Dist + state.step = 4 + + // 开空单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + StopLoss: state.sellSLPrice, + TakeProfit: state.sellTP1Price, + StopLossVal: slDist, + }) + } + } + + // 管理多头仓位 + if state.inTrade && state.tradeDirection == 1 && len(s.LongOrders) > 0 { + od := s.LongOrders[0] + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + + // 检查是否达到第一止盈目标 + if highPrice >= state.buyTP1Price && !state.tp1Hit { + state.tp1Hit = true + state.buytp1Hit = true + // 移动止损到保本位置 + state.buySLPrice = state.entryPrice + beoffDist + // 平仓25% + s.CloseOrders(&strat.ExitReq{ + Tag: "TP1 Hit", + Dirt: core.OdDirtLong, + ExitRate: 0.25, + OrderType: core.OrderTypeMarket, + }) + // 更新剩余仓位的止损止盈 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: state.buySLPrice, + Rate: 1.0, + Tag: "Breakeven", + }) + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: state.buyTP2Price, + Rate: 1.0, + Tag: "TP2", + }) + } + + // 检查是否触发止损 + if lowPrice <= state.buySLPrice && !state.tp1Hit { + // 止损出场 + s.CloseOrders(&strat.ExitReq{ + Tag: "SL Hit", + Dirt: core.OdDirtLong, + ExitRate: 1.0, + OrderType: core.OrderTypeMarket, + }) + resetState(state) + } + + // 检查是否达到第二止盈目标 + if state.tp1Hit && highPrice >= state.buyTP2Price { + // 第二止盈出场 + s.CloseOrders(&strat.ExitReq{ + Tag: "TP2 Hit", + Dirt: core.OdDirtLong, + ExitRate: 1.0, + OrderType: core.OrderTypeMarket, + }) + resetState(state) + } + + // 检查保本止损是否触发 + if state.tp1Hit && lowPrice <= state.buySLPrice { + // 保本止损出场 + s.CloseOrders(&strat.ExitReq{ + Tag: "Breakeven Hit", + Dirt: core.OdDirtLong, + ExitRate: 1.0, + OrderType: core.OrderTypeMarket, + }) + resetState(state) + } + } + + // 管理空头仓位 + if state.inTrade && state.tradeDirection == -1 && len(s.ShortOrders) > 0 { + od := s.ShortOrders[0] + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + + // 检查是否达到第一止盈目标 + if lowPrice <= state.sellTP1Price && !state.tp1Hit { + state.tp1Hit = true + state.selltp1Hit = true + // 移动止损到保本位置 + state.sellSLPrice = state.entryPrice - beoffDist + // 平仓25% + s.CloseOrders(&strat.ExitReq{ + Tag: "TP1 Hit", + Dirt: core.OdDirtShort, + ExitRate: 0.25, + OrderType: core.OrderTypeMarket, + }) + // 更新剩余仓位的止损止盈 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: state.sellSLPrice, + Rate: 1.0, + Tag: "Breakeven", + }) + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: state.sellTP2Price, + Rate: 1.0, + Tag: "TP2", + }) + } + + // 检查是否触发止损 + if highPrice >= state.sellSLPrice && !state.tp1Hit { + // 止损出场 + s.CloseOrders(&strat.ExitReq{ + Tag: "SL Hit", + Dirt: core.OdDirtShort, + ExitRate: 1.0, + OrderType: core.OrderTypeMarket, + }) + resetState(state) + } + + // 检查是否达到第二止盈目标 + if state.tp1Hit && lowPrice <= state.sellTP2Price { + // 第二止盈出场 + s.CloseOrders(&strat.ExitReq{ + Tag: "TP2 Hit", + Dirt: core.OdDirtShort, + ExitRate: 1.0, + OrderType: core.OrderTypeMarket, + }) + resetState(state) + } + + // 检查保本止损是否触发 + if state.tp1Hit && highPrice >= state.sellSLPrice { + // 保本止损出场 + s.CloseOrders(&strat.ExitReq{ + Tag: "Breakeven Hit", + Dirt: core.OdDirtShort, + ExitRate: 1.0, + OrderType: core.OrderTypeMarket, + }) + resetState(state) + } + } + + // 更新交易状态 + if state.inTrade && state.tradeDirection == 1 && state.step == 2 { + // 多头收到卖出信号 + state.inTrade = false + state.tradeDirection = 0 + state.step = 2 + } + + if state.inTrade && state.tradeDirection == -1 && state.step == 1 { + // 空头收到买入信号 + state.inTrade = false + state.tradeDirection = 0 + state.step = 1 + } + }, + + // 自定义订单退出逻辑 + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + state, _ := s.More.(*State4067) + + // 多头订单检查 + if !od.Short && state.inTrade && state.tradeDirection == 1 { + lowPrice := s.Env.Low.Get(0) + + // 检查是否触发止损 + if lowPrice <= state.buySLPrice { + return &strat.ExitReq{ + Tag: "SL Hit", + Dirt: core.OdDirtLong, + ExitRate: 1.0, + } + } + + // 检查是否达到第二止盈目标 + if state.tp1Hit { + highPrice := s.Env.High.Get(0) + if highPrice >= state.buyTP2Price { + return &strat.ExitReq{ + Tag: "TP2 Hit", + Dirt: core.OdDirtLong, + ExitRate: 1.0, + } + } + } + } + + // 空头订单检查 + if od.Short && state.inTrade && state.tradeDirection == -1 { + highPrice := s.Env.High.Get(0) + + // 检查是否触发止损 + if highPrice >= state.sellSLPrice { + return &strat.ExitReq{ + Tag: "SL Hit", + Dirt: core.OdDirtShort, + ExitRate: 1.0, + } + } + + // 检查是否达到第二止盈目标 + if state.tp1Hit { + lowPrice := s.Env.Low.Get(0) + if lowPrice <= state.sellTP2Price { + return &strat.ExitReq{ + Tag: "TP2 Hit", + Dirt: core.OdDirtShort, + ExitRate: 1.0, + } + } + } + } + + return nil + }, + } +} + +// checkTradingTime 检查当前时间是否在交易时间内 +func checkTradingTime(t time.Time, startHour, adjustedEndHour int, + tradeOnMonday, tradeOnTuesday, tradeOnWednesday, tradeOnThursday, tradeOnFriday bool) bool { + // 获取当前小时和星期几 + currentHour := t.Hour() + currentDay := t.Weekday() + + // 检查是否在交易小时内 + isTradingHour := false + if startHour < adjustedEndHour { + isTradingHour = currentHour >= startHour && currentHour < adjustedEndHour + } else { + isTradingHour = currentHour >= startHour || currentHour < adjustedEndHour + } + + // 检查是否在交易日内 + isTradingDay := false + switch currentDay { + case time.Monday: + isTradingDay = tradeOnMonday + case time.Tuesday: + isTradingDay = tradeOnTuesday + case time.Wednesday: + isTradingDay = tradeOnWednesday + case time.Thursday: + isTradingDay = tradeOnThursday + case time.Friday: + isTradingDay = tradeOnFriday + } + + return isTradingHour && isTradingDay +} + +// updateStep 更新信号状态步骤 +func updateStep(state *State4067, fastEMA, slowEMA *ta.Series) { + // 检查EMA交叉 + crossUp := fastEMA.Cross(slowEMA) + crossDown := slowEMA.Cross(fastEMA) + + // 更新状态步骤 + if (state.step == 0 || state.step == 4) && crossUp == 1 && fastEMA.Get(0) > state.higherEMA { + state.step = 1 + } + + if state.step == 3 && crossUp == 1 && fastEMA.Get(0) > state.higherEMA { + state.step = 3 + } + + if state.step == 2 && crossUp == 1 && fastEMA.Get(0) > state.higherEMA { + state.step = 1 + } + + if (state.step == 0 || state.step == 3) && crossDown == 1 && fastEMA.Get(0) < state.higherEMA { + state.step = 2 + } + + if state.step == 4 && crossDown == 1 && fastEMA.Get(0) < state.higherEMA { + state.step = 4 + } + + if state.step == 1 && crossDown == 1 && fastEMA.Get(0) < state.higherEMA { + state.step = 2 + } +} + +// resetState 重置策略状态 +func resetState(state *State4067) { + state.inTrade = false + state.tradeDirection = 0 + state.entryPrice = math.NaN() + state.tp1Hit = false + state.buytp1Hit = false + state.selltp1Hit = false + state.buySLPrice = math.NaN() + state.buyTP1Price = math.NaN() + state.buyTP2Price = math.NaN() + state.sellSLPrice = math.NaN() + state.sellTP1Price = math.NaN() + state.sellTP2Price = math.NaN() + state.step = 0 +} diff --git a/llmjob/419.go b/llmjob/419.go new file mode 100644 index 0000000..ce6b89a --- /dev/null +++ b/llmjob/419.go @@ -0,0 +1,134 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type FLIData struct { + trendLine float64 + iTrend float64 +} + +func J419(pol *config.RunPolicyConfig) *strat.TradeStrat { + bbPeriod := pol.DefInt("bbPeriod", 38, core.PNorm(10, 50)) + bbDeviations := pol.Def("bbDeviations", 0.65, core.PNorm(0.5, 3.0)) + useATRFilter := pol.Def("useATRFilter", 0.0, core.PUniform(0, 1)) // 0=false, 1=true + atrPeriod := pol.DefInt("atrPeriod", 5, core.PNorm(3, 20)) + + return &strat.TradeStrat{ + WarmupNum: bbPeriod + 5, // 确保有足够数据计算指标 + HedgeOff: true, // 单向持仓模式 + TimeFrames: "4h", + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的数据 + s.More = &FLIData{ + trendLine: 0.0, + iTrend: 0.0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + data, _ := s.More.(*FLIData) + + // 计算布林带 + bbMiddle := ta.SMA(e.Close, bbPeriod) + stdDev := ta.StdDev(e.Close, bbPeriod) + bbUpper := bbMiddle.Add(stdDev.Mul(bbDeviations)) + bbLower := bbMiddle.Sub(stdDev.Mul(bbDeviations)) + + // 获取当前价格 + closePrice := e.Close.Get(0) + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(highPrice) || math.IsNaN(lowPrice) { + return + } + + // 确定布林带信号 + bbSignal := 0 + if closePrice > bbUpper.Get(0) { + bbSignal = 1 // 看涨信号 + } else if closePrice < bbLower.Get(0) { + bbSignal = -1 // 看跌信号 + } + + // 计算趋势线 + prevTrendLine := data.trendLine + var newTrendLine float64 + + if useATRFilter > 0.5 { // 启用ATR过滤器 + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + atrValue := atr.Get(0) + + switch bbSignal { + case 1: // 看涨信号 + newTrendLine = lowPrice - atrValue + if newTrendLine < prevTrendLine { + newTrendLine = prevTrendLine + } + case -1: // 看跌信号 + newTrendLine = highPrice + atrValue + if newTrendLine > prevTrendLine { + newTrendLine = prevTrendLine + } + default: // 无信号 + newTrendLine = prevTrendLine + } + } else { // 不启用ATR过滤器 + switch bbSignal { + case 1: // 看涨信号 + newTrendLine = lowPrice + if newTrendLine < prevTrendLine { + newTrendLine = prevTrendLine + } + case -1: // 看跌信号 + newTrendLine = highPrice + if newTrendLine > prevTrendLine { + newTrendLine = prevTrendLine + } + default: // 无信号 + newTrendLine = prevTrendLine + } + } + + // 更新趋势线 + data.trendLine = newTrendLine + + // 确定趋势方向 + prevITrend := data.iTrend + var newITrend float64 + + if newTrendLine > prevTrendLine { + newITrend = 1 // 上升趋势 + } else if newTrendLine < prevTrendLine { + newITrend = -1 // 下降趋势 + } else { + newITrend = prevITrend // 保持原趋势 + } + + // 生成交易信号 + if prevITrend == -1 && newITrend == 1 { + // 趋势从下跌转为上涨,产生买入信号 + s.OpenOrder(&strat.EnterReq{ + Tag: "buy", + }) + } else if prevITrend == 1 && newITrend == -1 { + // 趋势从上涨转为下跌,产生卖出信号 + s.OpenOrder(&strat.EnterReq{ + Tag: "sell", + Short: true, + }) + } + + // 更新趋势方向 + data.iTrend = newITrend + }, + } +} diff --git a/llmjob/4217.go b/llmjob/4217.go new file mode 100644 index 0000000..1aedb2e --- /dev/null +++ b/llmjob/4217.go @@ -0,0 +1,110 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J4217(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + fastLength := pol.DefInt("fastLength", 9, core.PNorm(5, 20)) + maRate := pol.Def("maRate", 2.33, core.PNorm(1.5, 5)) + rsiLength := pol.DefInt("rsiLength", 14, core.PNorm(7, 21)) + rsiThreshold := pol.DefInt("rsiThreshold", 70, core.PNorm(60, 80)) + atrStopLoss := pol.Def("atrStopLoss", 2.0, core.PNorm(1.0, 4.0)) + atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 21)) + // 添加MACD参数配置 + macdFast := pol.DefInt("macdFast", 12, core.PNorm(8, 20)) + macdSlow := pol.DefInt("macdSlow", 26, core.PNorm(20, 40)) + macdSignal := pol.DefInt("macdSignal", 9, core.PNorm(5, 15)) + + // 计算慢速EMA周期 + slowLength := int(float64(fastLength) * maRate) + + // 计算RSI超买超卖水平 + rsiOverboughtLevel := float64(rsiThreshold) + rsiOversoldLevel := 100 - float64(rsiThreshold) + + return &strat.TradeStrat{ + WarmupNum: slowLength + atrPeriod, // 确保有足够的数据计算指标 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多开1个多单 + EachMaxShort: 1, // 最多开1个空单 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + fastMA := ta.EMA(e.Close, fastLength) + slowMA := ta.EMA(e.Close, slowLength) + rsi := ta.RSI(e.Close, rsiLength) + // 使用配置的MACD参数 + macdLine, signalLine := ta.MACD(e.Close, macdFast, macdSlow, macdSignal) + atr := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + + // 获取最新值 + fastMAVal := fastMA.Get(0) + slowMAVal := slowMA.Get(0) + rsiVal := rsi.Get(0) + atrVal := atr.Get(0) + + // 检查数据有效性 + if math.IsNaN(fastMAVal) || math.IsNaN(slowMAVal) || math.IsNaN(rsiVal) || math.IsNaN(atrVal) { + return + } + + // 判断趋势 + bullishTrend := fastMAVal > slowMAVal + bearishTrend := fastMAVal < slowMAVal + + // 生成交易信号 + longCondition := bullishTrend && rsiVal < rsiOverboughtLevel && macdLine.Cross(signalLine) == 1 + shortCondition := bearishTrend && rsiVal > rsiOversoldLevel && macdLine.Cross(signalLine) == -1 + + // 计算止损距离 + stopLossDistance := atrVal * atrStopLoss + + // 执行交易操作 + if longCondition && len(s.LongOrders) == 0 { + // 开多单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Short: false, + StopLossVal: stopLossDistance, + StopLossTag: "ATR Stop Loss", + }) + } + + if shortCondition && len(s.ShortOrders) == 0 { + // 开空单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + StopLossVal: stopLossDistance, + StopLossTag: "ATR Stop Loss", + }) + } + + // 平仓条件 + if len(s.LongOrders) > 0 && (bearishTrend || rsiVal > rsiOverboughtLevel) { + // 平多单 + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Long", + Dirt: core.OdDirtLong, + }) + } + + if len(s.ShortOrders) > 0 && (bullishTrend || rsiVal < rsiOversoldLevel) { + // 平空单 + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Short", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/4225.go b/llmjob/4225.go new file mode 100644 index 0000000..8efcbcf --- /dev/null +++ b/llmjob/4225.go @@ -0,0 +1,113 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +// StrategyMore 存储每个品种的策略状态 +type StrategyMore struct { + LastBarNum float64 // 上次交易的K线编号 +} + +// MultiEMACross 多重均线交叉与时间间隔整合策略 +func J4225(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + emaBasePeriod := pol.DefInt("emaBasePeriod", 4, core.PNorm(3, 15)) + emaMidRatio := pol.Def("emaMidRatio", 2.1, core.PNorm(1.5, 4)) + emaSlowRatio := pol.Def("emaSlowRatio", 10, core.PNorm(5, 20)) + timeInterval := pol.DefInt("timeInterval", 35, core.PNorm(10, 100)) + atrMultiplier := pol.Def("atrMultiplier", 1.9, core.PNorm(0.5, 5)) + + // 计算EMA周期 + emaMidPeriod := int(float64(emaBasePeriod) * emaMidRatio) + emaSlowPeriod := int(float64(emaBasePeriod) * emaSlowRatio) + + // 确定预热K线数量,取最慢EMA周期的2倍 + warmupNum := emaSlowPeriod * 2 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的策略状态 + s.More = &StrategyMore{ + LastBarNum: -1, // 初始化为-1,表示尚未交易 + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*StrategyMore) + + // 计算EMA指标 + emaFast := ta.EMA(e.Close, emaBasePeriod) + emaMid := ta.EMA(e.Close, emaMidPeriod) + emaSlow := ta.EMA(e.Close, emaSlowPeriod) + + // 计算ATR用于动态止盈止损 + atr := ta.ATR(e.High, e.Low, e.Close, 14) + atrVal := atr.Get(0) + + // 检查EMA交叉条件 - 修复重复计算问题 + fastCrossSlow := emaFast.Cross(emaSlow) + midCrossSlow := emaMid.Cross(emaSlow) + fastCrossSlowUp := fastCrossSlow == 1 // 快线上穿慢线 + midCrossSlowUp := midCrossSlow == 1 // 中线上穿慢线 + fastCrossSlowDown := fastCrossSlow == -1 // 快线下穿慢线 + midCrossSlowDown := midCrossSlow == -1 // 中线下穿慢线 + + // 生成交易信号 + buyCondition := fastCrossSlowUp && midCrossSlowUp + sellCondition := fastCrossSlowDown && midCrossSlowDown + + // 检查时间间隔是否满足要求 + currentBarNum := e.BarCount(0) + barsSinceLastTrade := currentBarNum - m.LastBarNum + timeElapsedOk := m.LastBarNum == -1 || barsSinceLastTrade >= float64(timeInterval) + + // 如果有交易信号且时间间隔满足要求 + if (buyCondition || sellCondition) && timeElapsedOk { + // 先平掉所有现有仓位 + if len(s.LongOrders) > 0 || len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "close_all", + Dirt: core.OdDirtBoth, + Force: true, + }) + } + + // 计算止盈止损价格 + stopLossDist := atrVal * atrMultiplier + takeProfitDist := stopLossDist * 1.67 // 保持5:3的盈亏比 + + // 根据信号方向开仓 + if buyCondition { + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + Short: false, + OrderType: core.OrderTypeMarket, + StopLossVal: stopLossDist, + TakeProfitVal: takeProfitDist, + StopLossTag: "stop_loss", + TakeProfitTag: "take_profit", + }) + m.LastBarNum = currentBarNum + } else if sellCondition { + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + OrderType: core.OrderTypeMarket, + StopLossVal: stopLossDist, + TakeProfitVal: takeProfitDist, + StopLossTag: "stop_loss", + TakeProfitTag: "take_profit", + }) + m.LastBarNum = currentBarNum + } + } + }, + } +} diff --git a/llmjob/4267.go b/llmjob/4267.go new file mode 100644 index 0000000..b4f7d1f --- /dev/null +++ b/llmjob/4267.go @@ -0,0 +1,156 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J4267(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + shortMaLength := pol.DefInt("shortMaLength", 10, core.PNorm(5, 20)) + maRate := pol.Def("maRate", 1.8, core.PNorm(1.5, 4)) + rsiOverbought := pol.DefInt("rsiOverbought", 75, core.PNorm(60, 85)) + rsiOversold := pol.DefInt("rsiOversold", 25, core.PNorm(15, 40)) + bbMultiplier := pol.Def("bbMultiplier", 2.0, core.PNorm(1.5, 3.0)) + + // 计算长期均线周期 + longMaLength := int(float64(shortMaLength) * maRate) + + // 确定预热数量,取所有指标中最大的周期 + warmupNum := longMaLength + if warmupNum < 20 { + warmupNum = 20 + } + if warmupNum < 26 { + warmupNum = 26 + } + + return &strat.TradeStrat{ + Name: "Multi-Technical-Indicator-Synergistic-Trading-System", + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个做多订单 + EachMaxShort: 1, // 最多一个做空订单 + RunTimeFrames: []string{"4h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算移动平均线 + shortMa := ta.SMA(e.Close, shortMaLength) + longMa := ta.SMA(e.Close, longMaLength) + + // 计算RSI + rsi := ta.RSI(e.Close, 14) + + // 计算MACD + macdLine, signalLine := ta.MACD(e.Close, 12, 26, 9) + macdHist := macdLine.Sub(signalLine) + + // 计算布林带 + bbUpper, _, bbLower := ta.BBANDS(e.Close, 20, bbMultiplier, bbMultiplier) + + // 获取当前价格 + closePrice := e.Close.Get(0) + if math.IsNaN(closePrice) { + return + } + + // 检测主要交易信号:MA交叉 + maCross := shortMa.Cross(longMa) + buySignal := maCross == 1 // 短期均线上穿长期均线 + sellSignal := maCross == -1 // 短期均线下穿长期均线 + + // 检测辅助交易信号 + rsiVal := rsi.Get(0) + macdHistVal := macdHist.Get(0) + bbLowerVal := bbLower.Get(0) + bbUpperVal := bbUpper.Get(0) + + additionalBuySignal := !math.IsNaN(rsiVal) && !math.IsNaN(macdHistVal) && !math.IsNaN(bbLowerVal) && + rsiVal < float64(rsiOversold) && macdHistVal > 0 && closePrice < bbLowerVal + + additionalSellSignal := !math.IsNaN(rsiVal) && !math.IsNaN(macdHistVal) && !math.IsNaN(bbUpperVal) && + rsiVal > float64(rsiOverbought) && macdHistVal < 0 && closePrice > bbUpperVal + + // 执行交易策略 + if buySignal { + // 平掉所有做空订单 + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "MA_Sell_Close", + Dirt: core.OdDirtShort, + Force: true, + }) + } + + // 开做多订单 + if len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "MA_Buy", + }) + } + } else if sellSignal { + // 平掉所有做多订单 + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "MA_Buy_Close", + Dirt: core.OdDirtLong, + Force: true, + }) + } + + // 平掉所有做空订单 + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "MA_Sell_Close", + Dirt: core.OdDirtShort, + Force: true, + }) + } + + // 开做空订单 + if len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "MA_Sell", + Short: true, + }) + } + } + + // 辅助信号处理(仅在主要信号未触发时执行) + if additionalBuySignal && !buySignal && len(s.LongOrders) == 0 { + // 平掉所有做空订单 + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Aux_Sell_Close", + Dirt: core.OdDirtShort, + Force: true, + }) + } + + // 开做多订单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Aux_Buy", + }) + } else if additionalSellSignal && !sellSignal && len(s.ShortOrders) == 0 { + // 平掉所有做多订单 + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Aux_Buy_Close", + Dirt: core.OdDirtLong, + Force: true, + }) + } + + // 开做空订单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Aux_Sell", + Short: true, + }) + } + }, + } +} diff --git a/llmjob/4316.go b/llmjob/4316.go new file mode 100644 index 0000000..08bb262 --- /dev/null +++ b/llmjob/4316.go @@ -0,0 +1,115 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J4316(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + ema1Length := pol.DefInt("ema1_length", 19, core.PNorm(5, 50)) + ema2Length := pol.DefInt("ema2_length", 50, core.PNorm(20, 100)) + rsiOverbought := pol.DefInt("rsi_overbought", 70, core.PNorm(60, 80)) + rsiOversold := pol.DefInt("rsi_oversold", 30, core.PNorm(20, 40)) + macdFast := pol.DefInt("macd_fast", 12, core.PNorm(8, 16)) + macdSlow := pol.DefInt("macd_slow", 26, core.PNorm(20, 32)) + macdSignal := pol.DefInt("macd_signal", 9, core.PNorm(6, 12)) + atrLength := pol.DefInt("atr_length", 14, core.PNorm(7, 21)) + atrMult := pol.Def("atr_mult", 1.85, core.PNorm(1.5, 5)) + useVolume := pol.Def("use_volume", 1.0, core.PNorm(0.0, 1.0)) > 0.5 // 修复:使用Def代替DefBool + volumeMult := pol.Def("volume_mult", 2.0, core.PNorm(1.0, 4.0)) + capitalRisk := pol.Def("capital_risk", 2.0, core.PNorm(0.5, 5.0)) / 100 + + return &strat.TradeStrat{ + WarmupNum: ema2Length + 20, // 确保所有指标都有足够数据 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + DrawDownExit: true, // 启用回撤止盈 + RunTimeFrames: []string{"15m"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + ema1 := ta.EMA(e.Close, ema1Length) + ema2 := ta.EMA(e.Close, ema2Length) + rsi := ta.RSI(e.Close, 14) + + // 计算MACD + macdLine := ta.EMA(e.Close, macdFast).Sub(ta.EMA(e.Close, macdSlow)) + macdSignalLine := ta.EMA(macdLine, macdSignal) + macdHist := macdLine.Sub(macdSignalLine) + + // 计算ATR + atr := ta.ATR(e.High, e.Low, e.Close, atrLength) + + // 计算成交量过滤条件 + volumeMA := ta.SMA(e.Volume, 20) + volumeFilter := e.Volume.Get(0) > volumeMA.Get(0)*volumeMult + + // 检查数据有效性 + if math.IsNaN(ema1.Get(0)) || math.IsNaN(ema2.Get(0)) || + math.IsNaN(rsi.Get(0)) || math.IsNaN(macdHist.Get(0)) || + math.IsNaN(atr.Get(0)) { + return + } + + // 信号过滤 - 优化重复计算 + crossResult := ema1.Cross(ema2) + emaCrossUp := crossResult == 1 // 快速EMA上穿慢速EMA + emaCrossDown := crossResult == -1 // 快速EMA下穿慢速EMA + rsiFilterLong := rsi.Get(0) > float64(rsiOversold) // RSI高于超卖水平 + rsiFilterShort := rsi.Get(0) < float64(rsiOverbought) // RSI低于超买水平 + macdFilterLong := macdHist.Get(0) > 0 // MACD柱状图为正 + macdFilterShort := macdHist.Get(0) < 0 // MACD柱状图为负 + volumeOk := !useVolume || volumeFilter // 成交量条件满足 + + // 生成交易信号 + longEntry := emaCrossUp && rsiFilterLong && macdFilterLong && volumeOk + shortEntry := emaCrossDown && rsiFilterShort && macdFilterShort && volumeOk + + // 风险管理计算 + atrValue := atr.Get(0) // 修复:删除未使用的closePrice变量 + + // 计算交易规模 - 修复基于ATR的交易规模计算 + riskAmount := s.Strat.GetStakeAmount(s) * capitalRisk + tradeSize := riskAmount / math.Max(atrValue*atrMult, 1.0) + + // 计算追踪止损价格 + + // 执行交易 + if longEntry && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Amount: tradeSize, + // 修复:移除不存在的TrailOffset字段 + }) + } + + if shortEntry && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + Amount: tradeSize, + // 修复:移除不存在的TrailOffset字段 + }) + } + }, + + // 实现回撤止盈逻辑 + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + // 根据最大盈利回撤比例决定止盈 + if maxChg > 0.1 { // 盈利超过10% + return 0.5 // 回撤50%止盈 + } else if maxChg > 0.05 { // 盈利超过5% + return 0.7 // 回撤70%止盈 + } + return 0 // 不设置回撤止盈 + }, + } +} diff --git a/llmjob/4513.go b/llmjob/4513.go new file mode 100644 index 0000000..83ff1a2 --- /dev/null +++ b/llmjob/4513.go @@ -0,0 +1,268 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// State4513 存储每个品种的策略状态 +type State4513 struct { + dir int // 当前趋势方向,1=上升趋势,-1=下降趋势 + longStop float64 // 多头止损价格 + shortStop float64 // 空头止损价格 + longStopPrev float64 // 前一个多头止损价格 + shortStopPrev float64 // 前一个空头止损价格 + trailingStop float64 // 当前跟踪止损价格 +} + +// Demo 创建并返回策略实例 +func J4513(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + bbLength := pol.DefInt("BBLength", 100, core.PNorm(20, 200)) + stdDev := pol.Def("StdDev", 10, core.PNorm(1, 20)) + useMovingAverage := pol.Def("useMovingAverage", 1, core.PUniform(0, 1)) // 1=true, 0=false + maType := pol.DefInt("maType", 3, core.PUniform(0, 5)) // 0=ema, 1=sma, 2=hma, 3=rma, 4=vwma, 5=wma + lookbackPeriod := pol.DefInt("lookbackPeriod", 22, core.PNorm(5, 50)) + atrMaType := pol.DefInt("atrMaType", 2, core.PUniform(0, 5)) // 0=ema, 1=sma, 2=hma, 3=rma, 4=vwma, 5=wma + atrLength := pol.DefInt("atrLength", 10, core.PNorm(5, 30)) + atrMult := pol.Def("atrMult", 4.2, core.PNorm(1, 10)) + wicks := pol.Def("wicks", 0, core.PUniform(0, 1)) // 1=true, 0=false + considerYearlyHighLow := pol.Def("considerYearlyHighLow", 0, core.PUniform(0, 1)) // 1=true, 0=false + considerNewLongTermHighLows := pol.Def("considerNewLongTermHighLows", 0, core.PUniform(0, 1)) // 1=true, 0=false + tradeDirection := pol.DefInt("tradeDirection", 0, core.PUniform(0, 2)) // 0=all, 1=long, 2=short + + // 固定参数 + shortHighLowPeriod := 100 + longHighLowPeriod := 200 + + return &strat.TradeStrat{ + Name: "Bollinger Percentage Bands", + WarmupNum: int(math.Max(float64(bbLength), float64(longHighLowPeriod))) + 10, + HedgeOff: true, // 单向持仓 + EachMaxLong: 1, + EachMaxShort: 1, + RunTimeFrames: []string{"8h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化策略状态 + state := &State4513{ + dir: 1, + longStop: 0, + shortStop: 0, + } + s.More = state + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*State4513) + + // 1. 计算布林带指标 + // 修复错误1:布林带计算参数错误 + // 原始策略使用bb(close, BBLength, StdDev),其中StdDev是标准差倍数 + // 修改为使用正确的参数格式,提供上下轨的标准差倍数 + upperClose, _, lowerClose := ta.BBANDS(e.Close, bbLength, stdDev, stdDev) + upperHigh, _, lowerHigh := ta.BBANDS(e.High, bbLength, stdDev, stdDev) + upperLow, _, lowerLow := ta.BBANDS(e.Low, bbLength, stdDev, stdDev) + + // 2. 计算布林带百分比 + percentBClose := e.Close.Sub(lowerClose).Div(upperClose.Sub(lowerClose)).Mul(100) + percentBHigh := e.High.Sub(lowerHigh).Div(upperHigh.Sub(lowerHigh)).Mul(100) + percentBLow := e.Low.Sub(lowerLow).Div(upperLow.Sub(lowerLow)).Mul(100) + + // 3. 应用移动平均线平滑 + var newClose, newHigh, newLow *ta.Series + if useMovingAverage > 0.5 { + percentBMAClose := getMovingAverage(percentBClose, maType, lookbackPeriod) + percentBMAHigh := getMovingAverage(percentBHigh, maType, lookbackPeriod) + percentBMALow := getMovingAverage(percentBLow, maType, lookbackPeriod) + newClose = percentBMAClose + newHigh = percentBMAHigh + newLow = percentBMALow + } else { + newClose = percentBClose + newHigh = percentBHigh + newLow = percentBLow + } + + // 4. 计算ATR和止损 + closePrev := newClose.Back(1) + highPrev := newHigh.Back(1) + lowPrev := newLow.Back(1) + + trueRange := newHigh.Max(closePrev).Sub(newLow.Min(closePrev)) + averageTrueRange := getMovingAverage(trueRange, atrMaType, atrLength) + atr := averageTrueRange.Mul(atrMult) + + longStopVal := newClose.Sub(atr) + shortStopVal := newClose.Add(atr) + + // 修复错误2:止损更新逻辑不完整 + // 原始策略使用nz(longStop[1], longStop)处理前一个止损值不存在的情况 + // 修改为正确处理初始状态,并确保止损只能向有利方向移动 + if state.longStopPrev == 0 { + state.longStopPrev = longStopVal.Get(0) + } + + if state.shortStopPrev == 0 { + state.shortStopPrev = shortStopVal.Get(0) + } + + // 修复错误3:使用Series运算创建常量而不是ta.Const + if wicks > 0.5 { + if lowPrev.Get(0) > state.longStopPrev { + // 使用Max确保止损只能向有利方向移动 + longStopVal = longStopVal.Max(newClose.Sub(newClose).Add(state.longStopPrev)) + } + } else { + if closePrev.Get(0) > state.longStopPrev { + longStopVal = longStopVal.Max(newClose.Sub(newClose).Add(state.longStopPrev)) + } + } + + if wicks > 0.5 { + if highPrev.Get(0) < state.shortStopPrev { + shortStopVal = shortStopVal.Min(newClose.Sub(newClose).Add(state.shortStopPrev)) + } + } else { + if closePrev.Get(0) < state.shortStopPrev { + shortStopVal = shortStopVal.Min(newClose.Sub(newClose).Add(state.shortStopPrev)) + } + } + + // 5. 判断趋势方向 + // 修复错误4:趋势方向判断可能存在边界条件处理错误 + // 原始策略使用三元运算符,逻辑清晰且紧凑 + // 修改为更接近原始策略的实现 + if state.dir == -1 { + if wicks > 0.5 { + if newHigh.Get(0) > state.shortStopPrev { + state.dir = 1 + } + } else { + if newClose.Get(0) > state.shortStopPrev { + state.dir = 1 + } + } + } else if state.dir == 1 { + if wicks > 0.5 { + if newLow.Get(0) < state.longStopPrev { + state.dir = -1 + } + } else { + if newClose.Get(0) < state.longStopPrev { + state.dir = -1 + } + } + } + + if state.dir == 1 { + state.trailingStop = longStopVal.Get(0) + } else { + state.trailingStop = shortStopVal.Get(0) + } + + // 6. 判断辅助条件 + yearlyHighCondition, yearlyLowCondition := getYearlyHighLowCondition(e, considerYearlyHighLow > 0.5) + newHighS, newLowS := calculateNewHighLows(e, shortHighLowPeriod, longHighLowPeriod, considerNewLongTermHighLows > 0.5) + + // 7. 生成交易信号 + buyCondition := state.dir == 1 && yearlyHighCondition && newHighS + exitBuyCondition := state.dir == -1 + sellCondition := state.dir == -1 && yearlyLowCondition && newLowS + exitSellCondition := state.dir == 1 + + // 8. 执行交易 + if buyCondition && (tradeDirection == 0 || tradeDirection == 1) { + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + Short: false, + }) + } + + if exitBuyCondition && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "ExitBuy", + Dirt: core.OdDirtLong, + }) + } + + if sellCondition && (tradeDirection == 0 || tradeDirection == 2) { + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + }) + } + + if exitSellCondition && len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "ExitSell", + Dirt: core.OdDirtShort, + }) + } + + // 更新状态 + state.longStop = longStopVal.Get(0) + state.shortStop = shortStopVal.Get(0) + state.longStopPrev = state.longStop + state.shortStopPrev = state.shortStop + }, + } +} + +// getMovingAverage 根据类型获取移动平均线 +func getMovingAverage(source *ta.Series, maType int, length int) *ta.Series { + switch maType { + case 0: // ema + return ta.EMA(source, length) + case 1: // sma + return ta.SMA(source, length) + case 2: // hma + return ta.HMA(source, length) + case 3: // rma + return ta.RMA(source, length) + case 4: // vwma - 在Banbot中可能没有直接对应,使用SMA代替 + return ta.SMA(source, length) + case 5: // wma + return ta.WMA(source, length) + default: + return ta.SMA(source, length) + } +} + +// calculateNewHighLows 计算新的高低点条件 +func calculateNewHighLows(e *ta.BarEnv, shortPeriod, longPeriod int, consider bool) (bool, bool) { + shortHigh := ta.Highest(e.High, shortPeriod) + longHigh := ta.Highest(e.High, longPeriod) + shortLow := ta.Lowest(e.Low, shortPeriod) + longLow := ta.Lowest(e.Low, longPeriod) + + newHigh := true + newLow := true + + if consider { + newHigh = math.Abs(shortHigh.Get(0)-longHigh.Get(0)) < 1e-10 + newLow = math.Abs(shortLow.Get(0)-longLow.Get(0)) < 1e-10 + } + + return newHigh, newLow +} + +// getYearlyHighLowCondition 获取年度高低点条件 +func getYearlyHighLowCondition(e *ta.BarEnv, consider bool) (bool, bool) { + if !consider { + return true, true + } + + // 在Banbot中,我们无法直接获取年度数据,所以简化处理 + // 使用较长期的高低点来近似年度高低点 + yearlyHigh := ta.Highest(e.High, 365) + yearlyLow := ta.Lowest(e.Low, 365) + + yearlyHighCondition := e.Close.Get(0) > yearlyHigh.Get(0) + yearlyLowCondition := e.Close.Get(0) < yearlyLow.Get(0) + + return yearlyHighCondition, yearlyLowCondition +} diff --git a/llmjob/4583.go b/llmjob/4583.go new file mode 100644 index 0000000..6529339 --- /dev/null +++ b/llmjob/4583.go @@ -0,0 +1,96 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J4583(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义策略参数 + rsiLength := pol.DefInt("rsiLength", 5, core.PNorm(2, 14)) + maLength := pol.DefInt("maLength", 200, core.PNorm(50, 300)) + exitMaLength := pol.DefInt("exitMaLength", 5, core.PNorm(3, 20)) + overboughtThreshold := pol.Def("overboughtThreshold", 80, core.PNorm(70, 95)) + oversoldThreshold := pol.Def("oversoldThreshold", 20, core.PNorm(5, 30)) + stopLossPercentage := pol.Def("stopLossPercentage", 3, core.PNorm(1, 10)) + + return &strat.TradeStrat{ + WarmupNum: maLength, // 预热期设置为长期均线周期 + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"1h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算技术指标 + rsi2 := ta.RSI(e.Close, rsiLength) + ma200 := ta.SMA(e.Close, maLength) + ma5Exit := ta.SMA(e.Close, exitMaLength) + + // 获取当前指标值 + rsiValue := rsi2.Get(0) + closePrice := e.Close.Get(0) + ma200Value := ma200.Get(0) + ma5ExitValue := ma5Exit.Get(0) + + // 判断趋势方向 + positiveTrend := closePrice > ma200Value + negativeTrend := closePrice < ma200Value + + // 生成交易信号 + buyCondition := rsiValue < oversoldThreshold && positiveTrend + sellCondition := rsiValue > overboughtThreshold && negativeTrend + + // 生成出场信号 + exitLongCondition := closePrice > ma5ExitValue + exitShortCondition := closePrice < ma5ExitValue + + // 执行交易逻辑 + if buyCondition && len(s.LongOrders) == 0 { + // 开多单,设置止损百分比 + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + Short: false, + StopLossRate: stopLossPercentage / 100, + StopLossTag: "stop_loss", + }) + } else if sellCondition && len(s.ShortOrders) == 0 { + // 开空单,设置止损百分比 + s.OpenOrder(&strat.EnterReq{ + Tag: "Sell", + Short: true, + StopLossRate: stopLossPercentage / 100, + StopLossTag: "stop_loss", + }) + } + + // 计算止损水平 + var stopLossLevelLong, stopLossLevelShort float64 + if len(s.LongOrders) > 0 { + longEntryPrice := s.LongOrders[0].InitPrice + stopLossLevelLong = longEntryPrice * (1 - stopLossPercentage/100) + } + if len(s.ShortOrders) > 0 { + shortEntryPrice := s.ShortOrders[0].InitPrice + stopLossLevelShort = shortEntryPrice * (1 + stopLossPercentage/100) + } + + // 多头出场条件(包含止损触发) + if len(s.LongOrders) > 0 && (exitLongCondition || closePrice <= stopLossLevelLong) { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_long", + Dirt: core.OdDirtLong, + }) + } + + // 空头出场条件(包含止损触发) + if len(s.ShortOrders) > 0 && (exitShortCondition || closePrice >= stopLossLevelShort) { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_short", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/4670.go b/llmjob/4670.go new file mode 100644 index 0000000..94b1bb7 --- /dev/null +++ b/llmjob/4670.go @@ -0,0 +1,71 @@ +// 修复后的完整策略代码 +// 请勿修改package包名和任意函数名 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J4670(pol *config.RunPolicyConfig) *strat.TradeStrat { + fastEMAPeriod := pol.DefInt("fastEMAPeriod", 10, core.PNorm(3, 15)) + emaRate := pol.Def("emaRate", 2.5, core.PNorm(1.5, 5)) + stopLossPercent := pol.Def("stopLossPercent", 2, core.PNorm(0.5, 5)) + takeProfitPercent := pol.Def("takeProfitPercent", 4, core.PNorm(1, 10)) + + // 计算慢速EMA周期 + slowEMAPeriod := int(float64(fastEMAPeriod) * emaRate) + + return &strat.TradeStrat{ + WarmupNum: slowEMAPeriod * 2, // 预热数量设置为慢速EMA周期的2倍 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多同时持有一个多头仓位 + EachMaxShort: 1, // 最多同时持有一个空头仓位 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算EMA指标 + ema5 := ta.EMA(e.Close, fastEMAPeriod) + ema15 := ta.EMA(e.Close, slowEMAPeriod) + + // 获取当前价格 + currentPrice := e.Close.Get(0) + if math.IsNaN(currentPrice) { + return + } + + // 检测EMA交叉(只计算一次) + crossSignal := ema5.Cross(ema15) + longCondition := crossSignal == 1 // 5周期EMA上穿15周期EMA + shortCondition := crossSignal == -1 // 5周期EMA下穿15周期EMA + + // 执行交易逻辑 + if longCondition && len(s.LongOrders) == 0 { + // 开多单,使用入场价格计算止损止盈 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + StopLossVal: currentPrice * stopLossPercent / 100, + TakeProfitVal: currentPrice * takeProfitPercent / 100, + StopLossTag: "Stop Loss", + TakeProfitTag: "Take Profit", + }) + } + + if shortCondition && len(s.ShortOrders) == 0 { + // 开空单,使用入场价格计算止损止盈 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + StopLossVal: currentPrice * stopLossPercent / 100, + TakeProfitVal: currentPrice * takeProfitPercent / 100, + StopLossTag: "Stop Loss", + TakeProfitTag: "Take Profit", + }) + } + }, + } +} diff --git a/llmjob/4994.go b/llmjob/4994.go new file mode 100644 index 0000000..68fad6f --- /dev/null +++ b/llmjob/4994.go @@ -0,0 +1,90 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J4994(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + rsiLength := pol.DefInt("RSIlength", 14, core.PNorm(5, 50)) + rsiTop := pol.Def("RSItop", 70.0, core.PNorm(69, 90)) + rsiBottom := pol.Def("RSIbottom", 35.0, core.PNorm(20, 40)) + rvolLenRatio := pol.Def("rvolLenRatio", 1.0, core.PNorm(0.5, 3.0)) + rvolThreshold := pol.Def("RVOLthreshold", 1.5, core.PNorm(1.2, 3.0)) + + // 计算实际成交量周期 + rvolLen := int(float64(rsiLength) * rvolLenRatio) + + return &strat.TradeStrat{ + Name: "RelativeVolumeStrategy", + WarmupNum: rsiLength + rvolLen + 1, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多1个多头仓位 + EachMaxShort: 1, // 最多1个空头仓位 + RunTimeFrames: []string{"4h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + volume := e.Volume.Get(0) + + // 计算RSI指标 + rsiVal := ta.RSI(e.Close, rsiLength) + currentRSI := rsiVal.Get(0) + + // 计算相对成交量 + avgVolume := ta.SMA(e.Volume, rvolLen) + rvolVal := volume / avgVolume.Get(0) + + // 检测RSI交叉信号 + rsiCrossTop := rsiVal.Cross(rsiTop) + rsiCrossBottom := rsiVal.Cross(rsiBottom) + + // 多头条件:RSI上穿阈值且成交量放大 + longCondition := rsiCrossTop == 1 && rvolVal > rvolThreshold + // 空头条件:RSI下穿阈值且成交量放大 + shortCondition := rsiCrossBottom == -1 && rvolVal > rvolThreshold + // 平多条件:RSI回落 + closeLong := currentRSI < 69 + // 平空条件:RSI反弹 + closeShort := currentRSI > 35 + + // 执行交易逻辑 + hasLong := len(s.LongOrders) > 0 + hasShort := len(s.ShortOrders) > 0 + + // 多头开仓 + if longCondition && !hasLong { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Short: false, + }) + } + + // 多头平仓 + if closeLong && hasLong { + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseLong", + Dirt: core.OdDirtLong, + }) + } + + // 空头开仓 + if shortCondition && !hasShort { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + }) + } + + // 空头平仓 + if closeShort && hasShort { + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseShort", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/5115.go b/llmjob/5115.go new file mode 100644 index 0000000..b2238ea --- /dev/null +++ b/llmjob/5115.go @@ -0,0 +1,88 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J5115(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastPeriod := pol.DefInt("fastPeriod", 11, core.PNorm(3, 15)) + maRate := pol.Def("maRate", 4.4, core.PNorm(2.0, 8.0)) + atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 30)) + slMultiplier := pol.Def("slMultiplier", 1.3, core.PNorm(1.0, 5.0)) + + // 计算长期均线周期 + longPeriod := int(float64(fastPeriod) * maRate) + + // 确保预热足够长以覆盖所有指标 + warmup := longPeriod + atrPeriod + 50 + + return &strat.TradeStrat{ + Name: "Moving-Average-Convergence-Momentum-Cloud-Strategy", + WarmupNum: warmup, + HedgeOff: true, // 单向持仓 + DrawDownExit: false, + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + close := e.Close + + // 计算SMA + maShort := ta.SMA(close, fastPeriod) + maLong := ta.SMA(close, longPeriod) + cross := maShort.Cross(maLong) + + // 计算ATR用于止损 + atr := ta.ATR(e.High, e.Low, close, atrPeriod) + atrVal := atr.Get(0) + + // RSI仅用于展示,不参与信号过滤 + _ = ta.RSI(close, 14) // 保留调用但不用于条件判断 + + currentPrice := close.Get(0) + if math.IsNaN(currentPrice) || math.IsNaN(atrVal) { + return + } + + // 买入信号:SMA上穿(无RSI过滤) + if cross == 1 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Buy", + StopLossVal: atrVal * slMultiplier, + StopLossTag: "atr_sl", + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "ExitShort", + Dirt: core.OdDirtShort, + }) + } + + // 卖出信号:SMA下穿 + if cross == -1 { + s.OpenOrder(&strat.EnterReq{ + Tag: "sell", + Short: true, + StopLossVal: atrVal * slMultiplier, + StopLossTag: "atr_sl", + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "exitLong", + Dirt: core.OdDirtLong, + }) + } + + // 可选:计算并保留其他指标用于分析或未来扩展 + // bbU, bbM, bbL := ta.BBANDS(close, 20, 2, 2) + // tenkan := ta.SMA(ta.HL2(e.High, e.Low), 9) + // kijun := ta.SMA(ta.HL2(e.High, e.Low), 26) + // senkouA := ta.SMA(ta.Add(tenkan, kijun).Div(2), 26) + // senkouB := ta.SMA(ta.HL2(e.High, e.Low), 52) + // momentum := close.Sub(close.Back(1)) + // roc := close.Sub(close.Back(12)).Div(close.Back(12)).Mul(100) + }, + } +} diff --git a/llmjob/5129.go b/llmjob/5129.go new file mode 100644 index 0000000..5c06dc7 --- /dev/null +++ b/llmjob/5129.go @@ -0,0 +1,249 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "time" +) + +type MfiStrategyState struct { + DayStart int64 + DayLow float64 + DayHigh float64 + EntryTime int64 + LongSlPrice float64 + ShortSlPrice float64 + LongTpPrice float64 + ShortTpPrice float64 + TrailStop float64 + TrailStopShort float64 + EntryHigh float64 + EntryLow float64 +} + +func J5129(pol *config.RunPolicyConfig) *strat.TradeStrat { + mfiPeriod := pol.DefInt("mfiPeriod", 3, core.PNorm(2, 10)) + barBodySize := pol.Def("barBodySize", 0.65, core.PNorm(0.3, 0.8)) + maPeriod := pol.DefInt("maPeriod", 120, core.PNorm(20, 200)) + useMaFilter := pol.Def("useMaFilter", 1.0, nil) + useShorts := pol.Def("useShorts", 1.0, nil) + slType := pol.Def("slType", 0, nil) // 0:Strategy Stop, 1:Swing, 2:ATR + swingLookback := pol.DefInt("swingLookback", 10, core.PNorm(5, 30)) + swingPercInc := pol.Def("swingPercInc", 0.03, core.PNorm(0.01, 0.1)) + atrLength := pol.DefInt("atrLength", 14, core.PNorm(7, 30)) + atrMultiple := pol.Def("atrMultiple", 6.5, core.PNorm(2.0, 10.0)) + rewardRatio := pol.Def("rewardRatio", 3.2, core.PNorm(1.5, 4.0)) + useTpSl := pol.Def("useTpSl", 1.0, nil) + useTimedExit := pol.Def("useTimedExit", 0.0, nil) + useTrailingStop := pol.Def("useTrailingStop", 0.0, nil) + allowDirectPositionReverse := pol.Def("allowDirectPositionReverse", 1.0, nil) + + return &strat.TradeStrat{ + Name: "Money Flow Index 5 min Strategy", + Version: 1, + WarmupNum: 100, + HedgeOff: false, + EachMaxLong: 1, + EachMaxShort: 1, + RunTimeFrames: []string{"15m"}, + OnStartUp: func(s *strat.StratJob) { + s.More = &MfiStrategyState{ + DayStart: -1, + DayLow: math.Inf(1), + DayHigh: math.Inf(-1), + EntryTime: 0, + LongSlPrice: math.NaN(), + ShortSlPrice: math.NaN(), + LongTpPrice: math.NaN(), + ShortTpPrice: math.NaN(), + TrailStop: math.NaN(), + TrailStopShort: math.NaN(), + EntryHigh: math.NaN(), + EntryLow: math.NaN(), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + now := e.TimeStop + c, h, l, o := e.Close.Get(0), e.High.Get(0), e.Low.Get(0), e.Open.Get(0) + if math.IsNaN(c) || math.IsNaN(h) || math.IsNaN(l) { + return + } + + // 更新交易日状态 + t := time.UnixMilli(now) + dayStartMs := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).UnixMilli() + m := s.More.(*MfiStrategyState) + if m.DayStart != dayStartMs { + m.DayStart = dayStartMs + m.DayLow = l + m.DayHigh = h + } else { + if l < m.DayLow { + m.DayLow = l + } + if h > m.DayHigh { + m.DayHigh = h + } + } + + // 计算指标 + mfi := ta.MFI(e, mfiPeriod) + smaVal := ta.SMA(e.Close, maPeriod) + maFilter := c > smaVal.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, atrLength).Get(0) + + // K线实体判断 - 修复错误3:与原始策略保持一致 + barSize := h - l + var barBodySizeVal float64 + if c > o { + barBodySizeVal = (o - c) * -1 + } else { + barBodySizeVal = (o - c) + } + shortWicks := barBodySizeVal > barSize*barBodySize + + // 交易信号 + prevMfi := mfi.Get(1) + buySignal := !math.IsNaN(prevMfi) && prevMfi == 100 && c > o && shortWicks && (useMaFilter == 0.0 || maFilter) + sellSignal := false + if useShorts == 1.0 { + sellSignal = !math.IsNaN(prevMfi) && prevMfi == 0 && c < o && shortWicks && (useMaFilter == 0.0 || !maFilter) + } + + // 检查60分钟退出规则 - 修复错误2:添加60分钟退出规则 + if useTimedExit == 1.0 && m.EntryTime > 0 { + timeSinceEntry := (now - m.EntryTime) / 60000 // 转换为分钟 + if timeSinceEntry >= 60 { + // 60分钟后平仓 + if len(s.LongOrders) > 0 || len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Dirt: core.OdDirtBoth}) + m.EntryTime = 0 + } + } + } + + // 计算止损止盈 - 修复错误1和4:记录开仓时的止损价格 + var longSl, shortSl, longTp, shortTp float64 + if useTpSl == 1.0 { + switch int(slType) { + case 0: // Strategy Stop + longSl = m.DayLow * (1 - swingPercInc) + shortSl = m.DayHigh * (1 + swingPercInc) + case 1: // Swing Lo/Hi + ll := ta.Lowest(e.Low, swingLookback).Get(0) * (1 - swingPercInc) + hh := ta.Highest(e.High, swingLookback).Get(0) * (1 + swingPercInc) + longSl = ll + shortSl = hh + case 2: // ATR Stop + ohlc4 := (o + h + l + c) / 4 + longSl = ohlc4 - atr*atrMultiple + shortSl = ohlc4 + atr*atrMultiple + } + longTp = c + (c-longSl)*rewardRatio + shortTp = c - (shortSl-c)*rewardRatio + } + + // 更新追踪止损 - 修复错误5:添加追踪止损功能 + if useTrailingStop == 1.0 { + if len(s.LongOrders) > 0 { + for _, pos := range s.LongOrders { + if pos.Status == ormo.InOutStatusFullEnter { // 多头仓位 + if math.IsNaN(m.EntryHigh) || h > m.EntryHigh { + m.EntryHigh = h + } + trailOffset := pos.InitPrice - m.LongSlPrice + newTrailStop := m.EntryHigh - trailOffset + if math.IsNaN(m.TrailStop) || newTrailStop > m.TrailStop { + m.TrailStop = newTrailStop + } + } + } + } + if len(s.ShortOrders) > 0 { + for _, pos := range s.ShortOrders { + if pos.Status == ormo.InOutStatusFullEnter { // 空头仓位 + if math.IsNaN(m.EntryLow) || l < m.EntryLow { + m.EntryLow = l + } + trailOffset := m.ShortSlPrice - pos.InitPrice + newTrailStop := m.EntryLow + trailOffset + if math.IsNaN(m.TrailStopShort) || newTrailStop < m.TrailStopShort { + m.TrailStopShort = newTrailStop + } + } + } + } + } + + // 开仓 - 修复错误6:检查持仓数量和挂单数量 + canOpenLong := len(s.LongOrders) == 0 && (len(s.ShortOrders) == 0 || (allowDirectPositionReverse == 1.0 && len(s.ShortOrders) > 0)) + canOpenShort := len(s.ShortOrders) == 0 && (len(s.LongOrders) == 0 || (allowDirectPositionReverse == 1.0 && len(s.LongOrders) > 0)) + + if buySignal && canOpenLong { + // 记录开仓时的止损价格 + m.LongSlPrice = longSl + m.LongTpPrice = longTp + m.EntryHigh = h + m.EntryTime = now + + req := &strat.EnterReq{ + Tag: "long", + StopLoss: longSl, + TakeProfit: longTp, + StopLossTag: "sl", + TakeProfitTag: "tp", + } + s.OpenOrder(req) + } + + if sellSignal && canOpenShort { + // 记录开仓时的止损价格 + m.ShortSlPrice = shortSl + m.ShortTpPrice = shortTp + m.EntryLow = l + m.EntryTime = now + + req := &strat.EnterReq{ + Tag: "short", + Short: true, + StopLoss: shortSl, + TakeProfit: shortTp, + StopLossTag: "sl", + TakeProfitTag: "tp", + } + s.OpenOrder(req) + } + + // 更新现有订单的止损止盈 + if len(s.LongOrders) > 0 && !math.IsNaN(m.LongSlPrice) { + for _, order := range s.LongOrders { + if order.Status == ormo.InOutStatusInit { + order.SetStopLoss(&ormo.ExitTrigger{Price: m.LongSlPrice, Tag: "sl"}) + order.SetTakeProfit(&ormo.ExitTrigger{Price: m.LongTpPrice, Tag: "tp"}) + if useTrailingStop == 1.0 && !math.IsNaN(m.TrailStop) { + order.SetStopLoss(&ormo.ExitTrigger{Price: m.TrailStop, Tag: "sl"}) + } + } + } + } + + if len(s.ShortOrders) > 0 && !math.IsNaN(m.ShortSlPrice) { + for _, order := range s.ShortOrders { + if order.Status == ormo.InOutStatusInit { + order.SetStopLoss(&ormo.ExitTrigger{Price: m.ShortSlPrice, Tag: "sl"}) + order.SetTakeProfit(&ormo.ExitTrigger{Price: m.ShortTpPrice, Tag: "tp"}) + if useTrailingStop == 1.0 && !math.IsNaN(m.TrailStopShort) { + order.SetStopLoss(&ormo.ExitTrigger{Price: m.TrailStopShort, Tag: "sl"}) + } + } + } + } + }, + } +} diff --git a/llmjob/559.go b/llmjob/559.go new file mode 100644 index 0000000..df25ce7 --- /dev/null +++ b/llmjob/559.go @@ -0,0 +1,64 @@ +// 修复后的banbot完整策略代码 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J559(pol *config.RunPolicyConfig) *strat.TradeStrat { + fastPeriod := pol.DefInt("fastPeriod", 14, core.PNorm(5, 20)) + macdRate := pol.Def("macdRate", 2.35, core.PNorm(1.5, 4)) + signalPeriod := 9 // 固定值,不在可调参数中 + + // 计算慢线周期 + slowPeriod := int(float64(fastPeriod) * macdRate) + + // 预热数量应该大于慢线周期 + warmupNum := slowPeriod + 10 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算MACD指标 + fastLine, slowLine := ta.MACD(e.Close, fastPeriod, slowPeriod, signalPeriod) + + // 获取当前和前一个周期的值 + fastCurr := fastLine.Get(0) + fastPrev := fastLine.Get(1) + slowCurr := slowLine.Get(0) + slowPrev := slowLine.Get(1) + + // 检查数据有效性 + if math.IsNaN(fastCurr) || math.IsNaN(fastPrev) || math.IsNaN(slowCurr) || math.IsNaN(slowPrev) { + return + } + + // 检测MACD快线上穿慢线(金叉) + if fastCurr > slowCurr && fastPrev < slowPrev && len(s.LongOrders) == 0 { + // 执行买入操作 + s.OpenOrder(&strat.EnterReq{ + Tag: "Enter Long", + }) + } + + // 检测MACD快线下穿慢线(死叉) + if fastCurr < slowCurr && fastPrev > slowPrev && len(s.ShortOrders) == 0 { + // 执行卖出操作 + s.OpenOrder(&strat.EnterReq{ + Tag: "Enter Short", + Short: true, + }) + } + }, + } +} diff --git a/llmjob/5609.go b/llmjob/5609.go new file mode 100644 index 0000000..351beee --- /dev/null +++ b/llmjob/5609.go @@ -0,0 +1,215 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// State5609 存储每个品种的策略状态 +type State5609 struct { + longEntryPrice float64 + shortEntryPrice float64 + longEntryBar int64 + shortEntryBar int64 +} + +func J5609(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + atrPeriod := pol.DefInt("atrPeriod", 14, core.PNorm(7, 30)) + bbLength := pol.DefInt("bbLength", 18, core.PNorm(10, 50)) + bbStdDev := pol.Def("bbStdDev", 1.9, core.PNorm(1, 3)) + rsiPeriod := pol.DefInt("rsiPeriod", 12, core.PNorm(7, 21)) + atrMultiplier := pol.Def("atrMultiplier", 1.6, core.PNorm(0.5, 3)) + vwapLength := pol.DefInt("vwapLength", 20, core.PNorm(10, 40)) + targetMultiplier := pol.Def("targetMultiplier", 2.4, core.PNorm(1, 4)) + maxHoldingBars := pol.DefInt("maxHoldingBars", 3, core.PNorm(2, 10)) + + // 计算预热期,确保所有指标都有足够的数据计算 + warmupNum := bbLength + if atrPeriod > warmupNum { + warmupNum = atrPeriod + } + if rsiPeriod > warmupNum { + warmupNum = rsiPeriod + } + if vwapLength > warmupNum { + warmupNum = vwapLength + } + // 额外增加20个K线确保稳定性 + warmupNum += 20 + + return &strat.TradeStrat{ + Name: "OptionsStrategy", + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个多头订单 + EachMaxShort: 1, // 最多一个空头订单 + RunTimeFrames: []string{"4h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的策略状态 + s.More = &State5609{ + longEntryPrice: 0, + shortEntryPrice: 0, + longEntryBar: 0, + shortEntryBar: 0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state := s.More.(*State5609) + + // 计算技术指标 + atrValue := ta.ATR(e.High, e.Low, e.Close, atrPeriod) + smaValue := ta.SMA(e.Close, bbLength) + stdDev := ta.StdDev(e.Close, bbLength) + upperBB := smaValue.Add(stdDev.Mul(bbStdDev)) + lowerBB := smaValue.Sub(stdDev.Mul(bbStdDev)) + rsiValue := ta.RSI(e.Close, rsiPeriod) + // 修复错误1:使用正确的VWAP计算方法 + vwap := ta.Sum(e.Close.Mul(e.Volume), vwapLength).Div(ta.Sum(e.Volume, vwapLength)) + volSMA := ta.SMA(e.Volume, 10) + atrSMA := ta.SMA(atrValue, 20) + + // 获取当前值 + closePrice := e.Close.Get(0) + atrVal := atrValue.Get(0) + upperBBVal := upperBB.Get(0) + lowerBBVal := lowerBB.Get(0) + rsiVal := rsiValue.Get(0) + vwapVal := vwap.Get(0) + volumeVal := e.Volume.Get(0) + volSMAVal := volSMA.Get(0) + atrSMAVal := atrSMA.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(atrVal) || math.IsNaN(upperBBVal) || + math.IsNaN(lowerBBVal) || math.IsNaN(rsiVal) || math.IsNaN(vwapVal) || + math.IsNaN(volumeVal) || math.IsNaN(volSMAVal) || math.IsNaN(atrSMAVal) { + return + } + + // 计算条件 + volSpike := volumeVal > volSMAVal*1.5 + atrFilter := atrVal > atrSMAVal*0.5 + + // 生成交易信号 + longCE := e.Close.Cross(upperBB) == 1 && rsiVal > 60 && volSpike && closePrice > vwapVal && atrFilter + longPE := e.Close.Cross(lowerBB) == -1 && rsiVal < 40 && volSpike && closePrice < vwapVal && atrFilter + + // 计算止损止盈 + longStopLoss := 0.0 + longTarget := 0.0 + shortStopLoss := 0.0 + shortTarget := 0.0 + + // 修复错误5:使用实际成交的平均价格作为止损止盈的计算基准 + if len(s.LongOrders) > 0 { + // 获取多头持仓的平均价格 + longAvgPrice := state.longEntryPrice + if longAvgPrice > 0 { + longStopLoss = longAvgPrice - atrMultiplier*atrVal + longTarget = longAvgPrice + targetMultiplier*atrVal + } + } + + if len(s.ShortOrders) > 0 { + // 获取空头持仓的平均价格 + shortAvgPrice := state.shortEntryPrice + if shortAvgPrice > 0 { + shortStopLoss = shortAvgPrice + atrMultiplier*atrVal + shortTarget = shortAvgPrice - targetMultiplier*atrVal + } + } + + // 执行交易 + if longCE && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "CE Entry", + OrderType: core.OrderTypeMarket, + StopLossVal: atrMultiplier * atrVal, + TakeProfitVal: targetMultiplier * atrVal, + }) + // 记录入场价格和K线时间 + state.longEntryPrice = closePrice + state.longEntryBar = e.TimeStop + } + + if longPE && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "PE Entry", + Short: true, + OrderType: core.OrderTypeMarket, + StopLossVal: atrMultiplier * atrVal, + TakeProfitVal: targetMultiplier * atrVal, + }) + // 记录入场价格和K线时间 + state.shortEntryPrice = closePrice + state.shortEntryBar = e.TimeStop + } + + // 出场管理 + if len(s.LongOrders) > 0 { + // 计算持仓时间(K线数) + holdBars := e.BarCount(state.longEntryBar) + + // 止盈出场 + if closePrice >= longTarget { + s.CloseOrders(&strat.ExitReq{ + Tag: "CE Target Hit", + Dirt: core.OdDirtLong, + Force: true, + }) + } + // 止损出场 + if closePrice <= longStopLoss { + s.CloseOrders(&strat.ExitReq{ + Tag: "CE Stop Loss Hit", + Dirt: core.OdDirtLong, + Force: true, + }) + } + // 时间出场 + if int64(holdBars) >= int64(maxHoldingBars) { + s.CloseOrders(&strat.ExitReq{ + Tag: "CE Timed Exit", + Dirt: core.OdDirtLong, + Force: true, + }) + } + } + + if len(s.ShortOrders) > 0 { + // 计算持仓时间(K线数) + holdBars := e.BarCount(state.shortEntryBar) + + // 止盈出场 + if closePrice <= shortTarget { + s.CloseOrders(&strat.ExitReq{ + Tag: "PE Target Hit", + Dirt: core.OdDirtShort, + Force: true, + }) + } + // 止损出场 + if closePrice >= shortStopLoss { + s.CloseOrders(&strat.ExitReq{ + Tag: "PE Stop Loss Hit", + Dirt: core.OdDirtShort, + Force: true, + }) + } + // 时间出场 + if int64(holdBars) >= int64(maxHoldingBars) { + s.CloseOrders(&strat.ExitReq{ + Tag: "PE Timed Exit", + Dirt: core.OdDirtShort, + Force: true, + }) + } + } + }, + } +} diff --git a/llmjob/5689.go b/llmjob/5689.go new file mode 100644 index 0000000..844fef5 --- /dev/null +++ b/llmjob/5689.go @@ -0,0 +1,204 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +因止损未检查bug导致表现看起来很好,不可用 +*/ + +type State5689 struct { + entryPrice float64 + hlv3 int // 保存三重均线状态 +} + +func J5689(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + tpPercent := pol.Def("tpPercent", 0.33, core.PNorm(0.15, 0.5)) // 止盈百分比 + length := pol.DefInt("length", 18, core.PNorm(10, 50)) // 通道计算周期 + lengthChop := pol.DefInt("lengthChop", 14, core.PNorm(5, 30)) // CHOP指标周期 + emaPeriod := pol.DefInt("emaPeriod", 10, core.PNorm(5, 30)) // EMA平滑周期 + maPeriod := pol.DefInt("maPeriod", 50, core.PNorm(30, 100)) // 主均线周期 + channelRate := pol.Def("channelRate", 0.15, core.PNorm(0.1, 0.5)) // 通道倍率 + exitPeriod := pol.DefInt("exitPeriod", 12, core.PNorm(5, 30)) // 退出均线周期 + + return &strat.TradeStrat{ + WarmupNum: maPeriod, // 预热K线数量,至少需要主均线周期 + RunTimeFrames: []string{"1h"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的状态变量 + s.More = &State5689{ + entryPrice: math.NaN(), + hlv3: 0, // 初始状态为0 + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*State5689) + + // 计算技术指标 + upper := ta.Highest(e.High, length) + lower := ta.Lowest(e.Low, length) + + // 计算output值:价格在通道中的百分比位置 + output := e.Close.Sub(upper).Div(upper.Sub(lower)).Mul(100) + emaOutput := ta.EMA(output, emaPeriod) + + // 自定义MA函数:双重WMA平滑 + maFunc := func(src *ta.Series, len int) *ta.Series { + wma1 := ta.WMA(src, len/2) + wma2 := ta.WMA(src, len) + return ta.WMA(wma1.Mul(2).Sub(wma2), int(math.Round(math.Sqrt(float64(len))))) + } + + // 计算主均线和通道 + BBMC := maFunc(e.Close, maPeriod) + rangema := ta.EMA(ta.TR(e.High, e.Low, e.Close), maPeriod) + upperk := BBMC.Add(rangema.Mul(channelRate)) + lowerk := BBMC.Sub(rangema.Mul(channelRate)) + + // 计算退出均线 + exitHigh := ta.WMA(e.High, exitPeriod) + exitLow := ta.WMA(e.Low, exitPeriod) + + // 计算RSI指标(原始代码中计算但未使用) + _ = ta.RSI(e.Close, 14) + + // 计算CHOP指标(虽然不影响交易逻辑,但为了完整性实现) + atr1 := ta.ATR(e.High, e.Low, e.Close, 1) + sumAtr := ta.Sum(atr1, lengthChop) + highestChop := ta.Highest(e.High, lengthChop) + lowestChop := ta.Lowest(e.Low, lengthChop) + chopRange := highestChop.Sub(lowestChop) + // 避免除以零 + chopRangeValid := chopRange.Get(0) > 0 + if chopRangeValid { + _ = sumAtr.Div(chopRange).Mul(100) + } else { + _ = ta.EMA(e.Close, 1).Mul(0) // 创建全0序列 + } + + // 获取当前值 + closePrice := e.Close.Get(0) + upperkVal := upperk.Get(0) + lowerkVal := lowerk.Get(0) + BBMCVal := BBMC.Get(0) + outputVal := output.Get(0) + emaOutputVal := emaOutput.Get(0) + exitHighVal := exitHigh.Get(0) + exitLowVal := exitLow.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) || math.IsNaN(BBMCVal) || math.IsNaN(outputVal) || math.IsNaN(emaOutputVal) { + return + } + + // 判断价格位置和颜色 + colorBlue := closePrice > upperkVal + colorBarFuchsia := closePrice < lowerkVal + + // 更新三重均线状态 + if closePrice > exitHighVal { + state.hlv3 = 1 + } else if closePrice < exitLowVal { + state.hlv3 = -1 + } + // 否则保持上一个状态 + + // 计算退出线 + _ = exitHigh + if state.hlv3 < 0 { + _ = exitLow + } + + // 生成交易信号 + closeXBBMC := e.Close.Cross(BBMC) + longCondition := closeXBBMC == 1 && outputVal > emaOutputVal + shortCondition := closeXBBMC == -1 && outputVal < emaOutputVal + + // 执行交易逻辑 + if longCondition && len(s.LongOrders) == 0 { + // 记录入场价格 + state.entryPrice = closePrice + // 开多单 + s.OpenOrder(&strat.EnterReq{ + Tag: "ENTER-LONG", + OrderType: core.OrderTypeMarket, + TakeProfit: closePrice * (1 + tpPercent), + }) + } + + // 为已有订单设置动态止损 + if len(s.LongOrders) > 0 && !math.IsNaN(state.entryPrice) { + od := s.LongOrders[0] + if od.Status == ormo.InOutStatusFullEnter { + // 只有当color_bar为FUCHSIA时才设置止损 + if colorBarFuchsia && !math.IsNaN(BBMCVal) { + // 设置BBMC作为止损价格 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: BBMCVal, + Rate: 1.0, // 全部退出 + Tag: "dynamic_stop_loss", + }) + } else { + // 不设置止损 + od.SetStopLoss(nil) + } + + // 更新止盈 + takeProfitPrice := state.entryPrice * (1 + tpPercent) + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: takeProfitPrice, + Rate: 1.0, // 全部退出 + Tag: "take_profit", + }) + } + } + + // 做空部分(原始代码中被注释,这里也注释掉) + + if shortCondition && len(s.ShortOrders) == 0 { + state.entryPrice = closePrice + s.OpenOrder(&strat.EnterReq{ + Tag: "ENTER-SHORT", + OrderType: core.OrderTypeMarket, + Short: true, + TakeProfit: closePrice * (1 - tpPercent), + }) + } + + if len(s.ShortOrders) > 0 && !math.IsNaN(state.entryPrice) { + od := s.ShortOrders[0] + if od.Status == ormo.InOutStatusFullEnter { + // 只有当color_bar为BLUE时才设置止损 + if colorBlue && !math.IsNaN(BBMCVal) { + // 设置BBMC作为止损价格 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: BBMCVal, + Rate: 1.0, // 全部退出 + Tag: "dynamic_stop_loss_short", + }) + } else { + // 不设置止损 + od.SetStopLoss(nil) + } + + // 更新止盈 + takeProfitPrice := state.entryPrice * (1 - tpPercent) + od.SetTakeProfit(&ormo.ExitTrigger{ + Price: takeProfitPrice, + Rate: 1.0, // 全部退出 + Tag: "take_profit_short", + }) + } + } + + }, + } +} diff --git a/llmjob/570.go b/llmjob/570.go new file mode 100644 index 0000000..1844446 --- /dev/null +++ b/llmjob/570.go @@ -0,0 +1,104 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J570(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + fastLength := pol.DefInt("fastLength", 20, core.PNorm(5, 20)) + macdRate := pol.Def("macdRate", 3, core.PNorm(1.5, 3.5)) + MACDLength := pol.DefInt("MACDLength", 12, core.PNorm(5, 15)) + macdThreshold := pol.Def("macdThreshold", -0.25, core.PNorm(-0.8, -0.1)) + takeProfitValue := pol.Def("takeProfitValue", 12, core.PNorm(2, 15)) + + // 计算慢速EMA周期 + slowLength := int(float64(fastLength) * macdRate) + + // 确定预热数量,需要足够的K线来计算MACD和检测谷底 + warmupNum := slowLength + MACDLength + 3 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算MACD指标 + fastEMA := ta.EMA(e.Close, fastLength) + slowEMA := ta.EMA(e.Close, slowLength) + macd := fastEMA.Sub(slowEMA) + aMACD := ta.EMA(macd, MACDLength) + delta := macd.Sub(aMACD) + + // 获取当前、前一个和前两个时间点的delta值 + delta0 := delta.Get(0) + delta1 := delta.Get(1) + delta2 := delta.Get(2) + + // 获取当前MACD值和收盘价 + macdValue := macd.Get(0) + closePrice := e.Close.Get(0) + + // 检查数据有效性 + if math.IsNaN(delta0) || math.IsNaN(delta1) || math.IsNaN(delta2) || + math.IsNaN(macdValue) || math.IsNaN(closePrice) { + return + } + + // 检测MACD谷底 + isValley := delta0 > delta1 && delta1 < delta2 + + // 谷底买入信号 + if isValley && macdValue <= macdThreshold && delta0 < 0 { + // 设置止盈价格 + + // 生成买入订单 + s.OpenOrder(&strat.EnterReq{ + Tag: "valley_buy", + TakeProfitVal: takeProfitValue, + }) + } + + // 检测delta与0的交叉 + crossResult := delta.Cross(0) + + // delta上穿0,买入信号 + if crossResult == 1 { + // 获取当前最高价和最低价 + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + + if !math.IsNaN(highPrice) && !math.IsNaN(lowPrice) { + // 生成买入订单,使用固定止盈值0.3 + s.OpenOrder(&strat.EnterReq{ + Tag: "MacdLE", + TakeProfitVal: 0.3, + }) + } + } + + // delta下穿0,卖出信号 + if crossResult == -1 { + // 获取当前最高价和最低价 + highPrice := e.High.Get(0) + lowPrice := e.Low.Get(0) + + if !math.IsNaN(highPrice) && !math.IsNaN(lowPrice) { + // 生成卖出订单,使用固定止盈值0.3 + s.OpenOrder(&strat.EnterReq{ + Tag: "MacdSE", + Short: true, + TakeProfitVal: 0.3, + }) + } + } + }, + } +} diff --git a/llmjob/5743.go b/llmjob/5743.go new file mode 100644 index 0000000..369a0f7 --- /dev/null +++ b/llmjob/5743.go @@ -0,0 +1,100 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J5743(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastPeriod := pol.DefInt("fastPeriod", 12, core.PNorm(5, 20)) + macdRate := pol.Def("macdRate", 2.17, core.PNorm(1.5, 4)) + signalPeriod := pol.DefInt("signalPeriod", 9, core.PNorm(5, 15)) + stopLossBars := pol.DefInt("stopLossBars", 4, core.PNorm(1, 10)) + riskRewardRatio := pol.Def("riskRewardRatio", 2, core.PNorm(1, 3)) + direction := pol.DefInt("direction", 2, core.PUniform(0, 2)) // 0: long, 1: short, 2: both + + // 计算慢速周期 + slowPeriod := int(float64(fastPeriod) * macdRate) + + // 确定预热数量,取所有参数中的最大值 + warmupNum := fastPeriod + if slowPeriod > warmupNum { + warmupNum = slowPeriod + } + if signalPeriod > warmupNum { + warmupNum = signalPeriod + } + if stopLossBars > warmupNum { + warmupNum = stopLossBars + } + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + EachMaxLong: 1, // 最多一个做多订单 + EachMaxShort: 1, // 最多一个做空订单 + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算MACD指标 + macdLine, signalLine := ta.MACD(e.Close, fastPeriod, slowPeriod, signalPeriod) + + // 获取当前值 + macdVal := macdLine.Get(0) + signalVal := signalLine.Get(0) + closePrice := e.Close.Get(0) + + // 计算直方图值 + histVal := macdVal - signalVal + + // 检测交叉信号 + macdCross := macdLine.Cross(signalLine) + longSignal := macdCross == 1 && histVal > 0 // MACD金叉且直方图为正 + shortSignal := macdCross == -1 && histVal < 0 // MACD死叉且直方图为负 + + // 计算止损价格 + longStopLoss := ta.Lowest(e.Low, stopLossBars).Get(0) + shortStopLoss := ta.Highest(e.High, stopLossBars).Get(0) + + // 检查止损价格有效性 + if math.IsNaN(longStopLoss) || math.IsNaN(shortStopLoss) { + return + } + + // 执行做多交易 + if direction != 1 && longSignal && len(s.LongOrders) == 0 { + // 计算止盈价格 + distance := closePrice - longStopLoss + takeProfit := closePrice + distance*riskRewardRatio + + s.OpenOrder(&strat.EnterReq{ + Tag: "Larga", + Short: false, + OrderType: core.OrderTypeMarket, + StopLoss: longStopLoss, + TakeProfit: takeProfit, + }) + } + + // 执行做空交易 + if direction != 0 && shortSignal && len(s.ShortOrders) == 0 { + // 计算止盈价格 + distance := shortStopLoss - closePrice + takeProfit := closePrice - distance*riskRewardRatio + + s.OpenOrder(&strat.EnterReq{ + Tag: "Corta", + Short: true, + OrderType: core.OrderTypeMarket, + StopLoss: shortStopLoss, + TakeProfit: takeProfit, + }) + } + }, + } +} diff --git a/llmjob/5763.go b/llmjob/5763.go new file mode 100644 index 0000000..501e671 --- /dev/null +++ b/llmjob/5763.go @@ -0,0 +1,131 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J5763(pol *config.RunPolicyConfig) *strat.TradeStrat { + keh := pol.DefInt("keh", 33, core.PNorm(5, 30)) + tenkanPeriod := pol.DefInt("tenkanPeriod", 23, core.PNorm(5, 20)) + kijunTenkanRatio := pol.Def("kijunTenkanRatio", 3.3, core.PNorm(2.0, 4.0)) + senkouBPeriod := pol.DefInt("senkouBPeriod", 75, core.PNorm(30, 100)) + displacement := pol.DefInt("displacement", 28, core.PNorm(15, 40)) + + // 计算基准线周期 + kijunPeriod := int(float64(tenkanPeriod) * kijunTenkanRatio) + + // 计算预热期数量,考虑最长的周期和位移 + warmupNum := senkouBPeriod + displacement + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算Hull移动平均线 + // 当前周期HMA计算 + halfKeh := int(math.Round(float64(keh) / 2)) + n2ma := ta.WMA(e.Close, halfKeh).Mul(2) + nma := ta.WMA(e.Close, keh) + diff := n2ma.Sub(nma) + sqn := int(math.Round(math.Sqrt(float64(keh)))) + n1 := ta.WMA(diff, sqn) + + // 前一周期HMA计算 + closeBack1 := e.Close.Back(1) + n2ma1 := ta.WMA(closeBack1, halfKeh).Mul(2) + nma1 := ta.WMA(closeBack1, keh) + diff1 := n2ma1.Sub(nma1) + sqn1 := int(math.Round(math.Sqrt(float64(keh)))) + n2 := ta.WMA(diff1, sqn1) + + // 计算一目均衡图组件 + // 转换线(Tenkan-sen) + tenkanSenHigh := ta.Highest(e.High, tenkanPeriod) + tenkanSenLow := ta.Lowest(e.Low, tenkanPeriod) + tenkanSen := tenkanSenHigh.Add(tenkanSenLow).Div(2) + + // 基准线(Kijun-sen) + kijunSenHigh := ta.Highest(e.High, kijunPeriod) + kijunSenLow := ta.Lowest(e.Low, kijunPeriod) + kijunSen := kijunSenHigh.Add(kijunSenLow).Div(2) + + // 先行带A(Senkou Span A) + senkouSpanA := tenkanSen.Add(kijunSen).Div(2) + + // 先行带B(Senkou Span B) + senkouSpanBHigh := ta.Highest(e.High, senkouBPeriod) + senkouSpanBLow := ta.Lowest(e.Low, senkouBPeriod) + senkouSpanB := senkouSpanBHigh.Add(senkouSpanBLow).Div(2) + + // 先行带高点和低点(位移后) + senkouSpanABack := senkouSpanA.Back(displacement - 1) + senkouSpanBBack := senkouSpanB.Back(displacement - 1) + senkouSpanH := senkouSpanABack.Max(senkouSpanBBack) + senkouSpanL := senkouSpanABack.Min(senkouSpanBBack) + + // 滞后线(Chikou Span) + chikouSpan := e.Close.Back(displacement - 1) + + // 获取当前值 + n1Val := n1.Get(0) + n2Val := n2.Get(0) + closeVal := e.Close.Get(0) + tenkanSenVal := tenkanSen.Get(0) + kijunSenVal := kijunSen.Get(0) + senkouSpanHVal := senkouSpanH.Get(0) + senkouSpanLVal := senkouSpanL.Get(0) + chikouSpanVal := chikouSpan.Get(0) + + // 多头入场条件 + longCondition := n1Val > n2Val && closeVal > n2Val && closeVal > chikouSpanVal && + closeVal > senkouSpanHVal && (tenkanSenVal >= kijunSenVal || closeVal > kijunSenVal) + + // 空头入场条件 + shortCondition := n1Val < n2Val && closeVal < n2Val && closeVal < chikouSpanVal && + closeVal < senkouSpanLVal && (tenkanSenVal <= kijunSenVal || closeVal < kijunSenVal) + + // 多头平仓条件 + closeLongCondition := n1Val < n2Val && (closeVal < n2Val || tenkanSenVal < kijunSenVal || + closeVal < tenkanSenVal || closeVal < kijunSenVal || closeVal < senkouSpanHVal || closeVal < chikouSpanVal) + + // 空头平仓条件 + closeShortCondition := n1Val > n2Val && (closeVal > n2Val || tenkanSenVal > kijunSenVal || + closeVal > tenkanSenVal || closeVal > kijunSenVal || closeVal > senkouSpanLVal || closeVal > chikouSpanVal) + + // 执行交易逻辑 + if longCondition && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + }) + } + + if shortCondition && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + }) + } + + if closeLongCondition && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseLong", + Dirt: core.OdDirtLong, + }) + } + + if closeShortCondition && len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "CloseShort", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/587.go b/llmjob/587.go new file mode 100644 index 0000000..35af686 --- /dev/null +++ b/llmjob/587.go @@ -0,0 +1,144 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +// MartingaleState 存储马丁格尔策略的状态 +type MartingaleState struct { + ContractSize float64 // 当前合约数量 + MartingaleCount int // 马丁格尔计数器 + LastTradeResult float64 // 上次交易结果 +} + +func J587(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastLength := pol.DefInt("fastLength", 15, core.PNorm(5, 30)) + maRate := pol.Def("maRate", 2.0, core.PNorm(1.5, 4.0)) + maxMartingaleCount := pol.DefInt("maxMartingaleCount", 3, core.PNorm(1, 5)) + martingaleMultiplier := pol.Def("martingaleMultiplier", 2.0, core.PNorm(1.5, 3.0)) + + // 计算慢线周期 + slowLength := int(float64(fastLength) * maRate) + signalSmoothing := 9 + + // 止盈止损比例 + longTakeProfit := 1.015 // 多头止盈1.5% + longStopLoss := 0.99 // 多头止损1% + shortTakeProfit := 1.01 // 空头止盈1% + shortStopLoss := 0.99 // 空头止损1% + + return &strat.TradeStrat{ + WarmupNum: slowLength + signalSmoothing, // 确保有足够的数据计算MACD + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"3d"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化马丁格尔状态 + s.More = &MartingaleState{ + ContractSize: 0.02, + MartingaleCount: 0, + LastTradeResult: 0, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*MartingaleState) + + // 计算MACD指标 + macdLine, signalLine := ta.MACD(e.Close, fastLength, slowLength, signalSmoothing) + + // 检测MACD交叉信号(只计算一次) + macdCross := macdLine.Cross(signalLine) + longCondition := macdCross == 1 // 上穿 + shortCondition := macdCross == -1 // 下穿 + + // 获取当前价格 + currentPrice := e.Close.Get(0) + if math.IsNaN(currentPrice) { + return + } + + // 多头信号 + if longCondition && s.Position(1, "") == 0 { + // 计算止盈止损价格 + takeProfitPrice := currentPrice * longTakeProfit + stopLossPrice := currentPrice * longStopLoss + takeProfitVal := takeProfitPrice - currentPrice + stopLossVal := currentPrice - stopLossPrice + + // 开多单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + OrderType: core.OrderTypeMarket, + Short: false, + TakeProfitVal: takeProfitVal, + StopLossVal: stopLossVal, + TakeProfitTag: "long_take_profit", + StopLossTag: "long_stop_loss", + CostRate: state.ContractSize / 0.02, // 根据合约数量调整开仓金额 + }) + + // 记录当前交易结果 + state.LastTradeResult = 0 + } + + // 空头信号 + if shortCondition && s.Position(-1, "") == 0 { + // 计算止盈止损价格 + takeProfitPrice := currentPrice / shortTakeProfit + stopLossPrice := currentPrice / shortStopLoss + takeProfitVal := currentPrice - takeProfitPrice + stopLossVal := stopLossPrice - currentPrice + + // 开空单 + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + OrderType: core.OrderTypeMarket, + Short: true, + TakeProfitVal: takeProfitVal, + StopLossVal: stopLossVal, + TakeProfitTag: "short_take_profit", + StopLossTag: "short_stop_loss", + CostRate: state.ContractSize / 0.02, // 根据合约数量调整开仓金额 + }) + + // 记录当前交易结果 + state.LastTradeResult = 0 + } + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + state, _ := s.More.(*MartingaleState) + + // 当订单完全平仓时更新马丁格尔状态 + if chgType == strat.OdChgExitFill && od.Status == ormo.InOutStatusFullExit { + // 获取当前交易结果 + currentProfit := od.ProfitRate + + if currentProfit < state.LastTradeResult { + // 上次交易亏损 + if state.MartingaleCount < maxMartingaleCount { + // 增加马丁格尔计数并加倍合约数量 + state.ContractSize *= martingaleMultiplier + state.MartingaleCount++ + } else { + // 达到最大马丁格尔次数,重置 + state.ContractSize = 0.02 + state.MartingaleCount = 0 + } + } else { + // 盈利或持平,重置马丁格尔状态 + state.ContractSize = 0.02 + state.MartingaleCount = 0 + } + + // 更新交易结果 + state.LastTradeResult = currentProfit + } + }, + } +} diff --git a/llmjob/592.go b/llmjob/592.go new file mode 100644 index 0000000..9588a19 --- /dev/null +++ b/llmjob/592.go @@ -0,0 +1,111 @@ +// 修复后的完整策略代码 +// 请勿修改package包名和任意函数名 +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +// MACD动态趋势量化交易策略 +func J592(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + fastPeriod := pol.DefInt("fastPeriod", 19, core.PNorm(5, 20)) + slowRate := pol.Def("slowRate", 2.9, core.PNorm(1.5, 4)) + signalPeriod := pol.DefInt("signalPeriod", 10, core.PNorm(5, 15)) + + // 计算慢线周期 + slowPeriod := int(float64(fastPeriod) * slowRate) + + return &strat.TradeStrat{ + WarmupNum: 100, // 预热K线数量 + EachMaxLong: 1, // 最多开1单做多 + EachMaxShort: 1, // 最多开1单做空 + DrawDownExit: true, // 启用回撤止盈 + RunTimeFrames: []string{"8h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算MACD指标 + // 使用initType=1,符合中国主流使用习惯 + macdLine, signalLine := ta.MACDBy(e.Close, fastPeriod, slowPeriod, signalPeriod, 1) + + // 检测MACD线与信号线的交叉 + // 返回值:1表示上穿(金叉),-1表示下穿(死叉),0表示重合或未知 + cross := macdLine.Cross(signalLine) + + // 计算ATR用于止损止盈 + atr := ta.ATR(e.High, e.Low, e.Close, 14) + atrVal := atr.Get(0) + + // 获取当前价格 + + // 获取当前订单数量 + longOrders := s.LongOrders + shortOrders := s.ShortOrders + + // 金叉信号:MACD线上穿信号线,且当前没有做多订单 + if cross == 1 && len(longOrders) == 0 { + // 如果有空单,先平空单 + if len(shortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "close_short", + Dirt: core.OdDirtShort, + OrderType: core.OrderTypeMarket, + }) + } + + // 开多单 + s.OpenOrder(&strat.EnterReq{ + Tag: "long_cross", + Short: false, + OrderType: core.OrderTypeMarket, + StopLossVal: atrVal * 2, // 2倍ATR止损 + TakeProfitVal: atrVal * 4, // 4倍ATR止盈 + StopLossTag: "atr_stop_loss", + TakeProfitTag: "atr_take_profit", + }) + } + + // 死叉信号:MACD线下穿信号线,且当前没有做空订单 + if cross == -1 && len(shortOrders) == 0 { + // 如果有多单,先平多单 + if len(longOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "close_long", + Dirt: core.OdDirtLong, + OrderType: core.OrderTypeMarket, + }) + } + + // 开空单 + s.OpenOrder(&strat.EnterReq{ + Tag: "short_cross", + Short: true, + OrderType: core.OrderTypeMarket, + StopLossVal: atrVal * 2, // 2倍ATR止损 + TakeProfitVal: atrVal * 4, // 4倍ATR止盈 + StopLossTag: "atr_stop_loss", + TakeProfitTag: "atr_take_profit", + }) + } + }, + + // 回撤止盈计算函数 + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + // 最大盈利超过5%后,回撤30%止盈 + if maxChg > 0.05 { + return 0.3 + } + // 最大盈利超过2%后,回撤50%止盈 + if maxChg > 0.02 { + return 0.5 + } + // 其他情况不设置回撤止盈 + return 0 + }, + } +} diff --git a/llmjob/656.go b/llmjob/656.go new file mode 100644 index 0000000..46d24ae --- /dev/null +++ b/llmjob/656.go @@ -0,0 +1,299 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func J656(pol *config.RunPolicyConfig) *strat.TradeStrat { + shortPeriod := pol.DefInt("shortPeriod", 50, core.PNorm(10, 100)) + maRate := pol.Def("maRate", 2.2, core.PNorm(1.5, 4.0)) + smoothMethod := pol.DefInt("smoothMethod", 1, core.PUniform(1, 4)) + useCurrentRes := pol.DefInt("useCurrentRes", 1, core.PUniform(0, 1)) + priceSource := pol.DefInt("priceSource", 0, core.PUniform(0, 7)) // 0=close, 1=high, 2=low, 3=open, 4=hl2, 5=hlc3, 6=hlcc4, 7=ohlc4 + + // 计算长期均线周期 + longPeriod := int(float64(shortPeriod) * maRate) + + // 确定预热数量,取长期均线周期的两倍以确保足够数据 + warmupNum := longPeriod * 2 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓模式 + RunTimeFrames: []string{"1h"}, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + // 如果不使用当前时间框架,则需要添加额外的时间框架数据 + if useCurrentRes == 0 { + // 使用周线作为额外时间框架,可根据需要调整 + return []*strat.PairSub{{Pair: "_cur_", TimeFrame: "W", WarmupNum: warmupNum}} + } + return nil + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 获取价格数据,根据用户选择的价格源 + var price *ta.Series + switch priceSource { + case 0: // close + price = e.Close + case 1: // high + price = e.High + case 2: // low + price = e.Low + case 3: // open + price = e.Open + case 4: // hl2 + price = ta.HL2(e.High, e.Low) + case 5: // hlc3 + price = ta.HLC3(e.High, e.Low, e.Close) + case 6: // hlcc4 + // 使用基本运算实现 hlcc4 = (high + low + close + close) / 4 + price = e.High.Add(e.Low).Add(e.Close).Add(e.Close).Div(4) + case 7: // ohlc4 + // 使用基本运算实现 ohlc4 = (open + high + low + close) / 4 + price = e.Open.Add(e.High).Add(e.Low).Add(e.Close).Div(4) + default: + price = e.Close + } + + // 如果不使用当前时间框架,则从More中获取其他时间框架的数据 + if useCurrentRes == 0 { + // 从More中获取其他时间框架的价格数据和移动平均线 + if s.More == nil { + return + } + moreData, ok := s.More.(map[string]interface{}) + if !ok { + return + } + if mtData, ok := moreData["W"]; ok { + if mtPrice, ok := mtData.(*ta.Series); ok { + price = mtPrice + } + if mtMA, ok := mtData.(map[string]interface{})["ma"]; ok { + if maData, ok := mtMA.(map[string]*ta.Series); ok { + if shortMA, ok := maData["short"]; ok { + if longMA, ok := maData["long"]; ok { + // 获取当前和前一个值 + shortMA0 := shortMA.Get(0) + shortMA1 := shortMA.Get(1) + longMA0 := longMA.Get(0) + longMA1 := longMA.Get(1) + close0 := e.Close.Get(0) + close1 := e.Close.Get(1) + + // 检查数据有效性 + if math.IsNaN(shortMA0) || math.IsNaN(shortMA1) || math.IsNaN(longMA0) || math.IsNaN(longMA1) || + math.IsNaN(close0) || math.IsNaN(close1) { + return + } + + // 计算均线交叉信号 + crossUp := shortMA1 <= longMA1 && shortMA0 > longMA0 + crossDown := shortMA1 >= longMA1 && shortMA0 < longMA0 + + // 计算价格趋势 + priceUp := close0 > close1 + priceDown := close0 < close1 + + // 生成交易信号 + buySignal := crossUp && priceUp + sellSignal := crossDown && priceDown + + // 执行交易 + if buySignal { + // 如果没有多头持仓,则开多单 + if len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Enter Long", + }) + } + // 如果有空头持仓,则平空单 + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Short", + Dirt: core.OdDirtShort, + }) + } + } else if sellSignal { + // 如果没有空头持仓,则开空单 + if len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Enter Short", + Short: true, + }) + } + // 如果有多头持仓,则平多单 + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Long", + Dirt: core.OdDirtLong, + }) + } + } + return + } + } + } + } + } + return + } + + // 确保有足够的数据 + if price.Len() < longPeriod { + return + } + + // 根据选择的计算方法计算短期和长期移动平均线 + var shortMA, longMA *ta.Series + + switch smoothMethod { + case 1: // SMA + shortMA = ta.SMA(price, shortPeriod) + longMA = ta.SMA(price, longPeriod) + case 2: // EMA + shortMA = ta.EMA(price, shortPeriod) + longMA = ta.EMA(price, longPeriod) + case 3: // WMA + shortMA = ta.WMA(price, shortPeriod) + longMA = ta.WMA(price, longPeriod) + case 4: // 线性回归 + shortMA = ta.LinReg(price, shortPeriod) + longMA = ta.LinReg(price, longPeriod) + default: + shortMA = ta.EMA(price, shortPeriod) + longMA = ta.EMA(price, longPeriod) + } + + // 获取当前和前一个值 + shortMA0 := shortMA.Get(0) + shortMA1 := shortMA.Get(1) + longMA0 := longMA.Get(0) + longMA1 := longMA.Get(1) + close0 := e.Close.Get(0) + close1 := e.Close.Get(1) + + // 检查数据有效性 + if math.IsNaN(shortMA0) || math.IsNaN(shortMA1) || math.IsNaN(longMA0) || math.IsNaN(longMA1) || + math.IsNaN(close0) || math.IsNaN(close1) { + return + } + + // 计算均线交叉信号 + crossUp := shortMA1 <= longMA1 && shortMA0 > longMA0 + crossDown := shortMA1 >= longMA1 && shortMA0 < longMA0 + + // 计算价格趋势 + priceUp := close0 > close1 + priceDown := close0 < close1 + + // 生成交易信号 + buySignal := crossUp && priceUp + sellSignal := crossDown && priceDown + + // 执行交易 + if buySignal { + // 如果没有多头持仓,则开多单 + if len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Enter Long", + }) + } + // 如果有空头持仓,则平空单 + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Short", + Dirt: core.OdDirtShort, + }) + } + } else if sellSignal { + // 如果没有空头持仓,则开空单 + if len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "Enter Short", + Short: true, + }) + } + // 如果有多头持仓,则平多单 + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Close Long", + Dirt: core.OdDirtLong, + }) + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // 处理其他时间框架的数据 + if useCurrentRes == 0 && tf == "W" { + // 根据用户选择的价格源获取价格数据 + var price *ta.Series + switch priceSource { + case 0: // close + price = e.Close + case 1: // high + price = e.High + case 2: // low + price = e.Low + case 3: // open + price = e.Open + case 4: // hl2 + price = ta.HL2(e.High, e.Low) + case 5: // hlc3 + price = ta.HLC3(e.High, e.Low, e.Close) + case 6: // hlcc4 + // 使用基本运算实现 hlcc4 = (high + low + close + close) / 4 + price = e.High.Add(e.Low).Add(e.Close).Add(e.Close).Div(4) + case 7: // ohlc4 + // 使用基本运算实现 ohlc4 = (open + high + low + close) / 4 + price = e.Open.Add(e.High).Add(e.Low).Add(e.Close).Div(4) + default: + price = e.Close + } + + // 计算移动平均线 + var shortMA, longMA *ta.Series + switch smoothMethod { + case 1: // SMA + shortMA = ta.SMA(price, shortPeriod) + longMA = ta.SMA(price, longPeriod) + case 2: // EMA + shortMA = ta.EMA(price, shortPeriod) + longMA = ta.EMA(price, longPeriod) + case 3: // WMA + shortMA = ta.WMA(price, shortPeriod) + longMA = ta.WMA(price, longPeriod) + case 4: // 线性回归 + shortMA = ta.LinReg(price, shortPeriod) + longMA = ta.LinReg(price, longPeriod) + default: + shortMA = ta.EMA(price, shortPeriod) + longMA = ta.EMA(price, longPeriod) + } + + // 存储计算结果到s.More中 + if s.More == nil { + s.More = make(map[string]interface{}) + } + moreData, ok := s.More.(map[string]interface{}) + if !ok { + s.More = make(map[string]interface{}) + moreData = s.More.(map[string]interface{}) + } + moreData["W"] = map[string]interface{}{ + "price": price, + "ma": map[string]*ta.Series{ + "short": shortMA, + "long": longMA, + }, + } + } + }, + } +} diff --git a/llmjob/6697.go b/llmjob/6697.go new file mode 100644 index 0000000..ada6cad --- /dev/null +++ b/llmjob/6697.go @@ -0,0 +1,60 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J6697(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + periodK := pol.DefInt("periodK", 13, core.PNorm(5, 30)) + smoothK := pol.DefInt("smoothK", 5, core.PNorm(2, 8)) + periodD := pol.DefInt("periodD", 2, core.PNorm(2, 5)) + tpTicks := pol.DefInt("tpTicks", 100, core.PNorm(10, 500)) // 固定tick止盈 + overboughtLevel := pol.DefInt("overboughtLevel", 80, core.PNorm(70, 90)) + oversoldLevel := 100 - overboughtLevel // 自动计算超卖水平 + + return &strat.TradeStrat{ + WarmupNum: periodK + smoothK + periodD + 10, // 确保足够的数据计算指标 + HedgeOff: true, // 启用单向持仓模式 + EachMaxLong: 100, // 对应pyramiding=100 + EachMaxShort: 100, // 对应pyramiding=100 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算KDJ指标,返回[K, D, RSV] + k, d, _ := ta.KDJ(e.High, e.Low, e.Close, periodK, smoothK, periodD) + + // 获取最新值 + kVal := k.Get(0) + + // 计算交叉信号 + kdCross := k.Cross(d) // 1:上穿, -1:下穿, 0:重合 + + // 使用ATR作为动态止盈参考 + atr := ta.ATR(e.High, e.Low, e.Close, 14) + + // 做多条件:K线上穿D线,且K值未达超买区 + if kdCross == 1 && kVal < float64(overboughtLevel) { + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + OrderType: core.OrderTypeMarket, + TakeProfitVal: atr.Get(0) * float64(tpTicks) / 100, // 使用ATR作为动态止盈 + }) + } + + // 做空条件:K线下穿D线,且K值未破超卖区 + if kdCross == -1 && kVal > float64(oversoldLevel) { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + OrderType: core.OrderTypeMarket, + TakeProfitVal: atr.Get(0) * float64(tpTicks) / 100, // 使用ATR作为动态止盈 + }) + } + }, + } +} diff --git a/llmjob/6969.go b/llmjob/6969.go new file mode 100644 index 0000000..b04eea2 --- /dev/null +++ b/llmjob/6969.go @@ -0,0 +1,89 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J6969(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + moveLimit := pol.Def("moveLimit", 69, core.PNorm(50, 90)) // 大幅移动阈值百分比 + maLength := pol.DefInt("maLength", 150, core.PNorm(50, 300)) // 均线过滤周期 + + return &strat.TradeStrat{ + WarmupNum: maLength + 3, // 预热数量:均线周期 + 3根K线用于形态判断 + HedgeOff: true, // 修改为单向持仓模式,与原始Pine Script行为一致 + TimeFrames: "3d", + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算均线指标 + ma := ta.SMA(e.Close, maLength) + + // 判断K线形态 + // downBar: 当前K线是否为阴线 + downBar := e.Open.Get(0) > e.Close.Get(0) + downBar1 := e.Open.Get(1) > e.Close.Get(1) + downBar2 := e.Open.Get(2) > e.Close.Get(2) + + // isThreeDown: 连续三根阴线 + isThreeDown := downBar && downBar1 && downBar2 + // isThreeUp: 连续三根阳线 + isThreeUp := !downBar && !downBar1 && !downBar2 + + // 计算大幅移动指标 + // isBigMoveDown: 当前K线实体占整个波段的百分比超过阈值 + highLowRange0 := e.High.Get(0) - e.Low.Get(0) + isBigMoveDown := ((e.Open.Get(0) - e.Close.Get(0)) / (0.001 + highLowRange0)) > moveLimit/100.0 + + // 复用 highLowRange0 变量,避免重复计算 + isBigMoveUp := ((e.Close.Get(0) - e.Open.Get(0)) / (0.001 + highLowRange0)) > moveLimit/100.0 + + // 生成交易信号 + // isLongBuy: 连续三阴且最后一根是大跌 + isLongBuy := isThreeDown && isBigMoveDown + // isLongExit: 收盘价突破前一根K线的最高价 + isLongExit := e.Close.Get(0) > e.High.Get(1) + + // isShortBuy: 连续三阳且最后一根是大涨 + isShortBuy := isThreeUp && isBigMoveUp + // isShortExit: 收盘价跌破前一根K线的最低价 + isShortExit := e.Close.Get(0) < e.Low.Get(1) + + // 执行交易逻辑 + // 多头入场 + if isLongBuy { + s.OpenOrder(&strat.EnterReq{ + Tag: "Entry Long", + Short: false, + }) + } + + // 多头出场 + if isLongExit { + s.CloseOrders(&strat.ExitReq{ + Tag: "Entry Long", + Dirt: core.OdDirtLong, + }) + } + + // 空头入场(需要满足价格低于均线的过滤条件) + if isShortBuy && e.Close.Get(0) < ma.Get(0) { + s.OpenOrder(&strat.EnterReq{ + Tag: "Entry Short", + Short: true, + }) + } + + // 空头出场 + if isShortExit { + s.CloseOrders(&strat.ExitReq{ + Tag: "Entry Short", + Dirt: core.OdDirtShort, + }) + } + }, + } +} diff --git a/llmjob/7046.go b/llmjob/7046.go new file mode 100644 index 0000000..d5ac8c3 --- /dev/null +++ b/llmjob/7046.go @@ -0,0 +1,131 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J7046(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + smaPeriod := pol.DefInt("smaPeriod", 22, core.PNorm(10, 50)) + mult := pol.Def("mult", 1.0, core.PNorm(0.8, 3.0)) + trailingLossPct := pol.Def("trailingLossPct", 0.075, core.PNorm(0.03, 0.15)) + trailingPointsPct := pol.Def("trailingPointsPct", 0.05, core.PNorm(0.01, 0.10)) + trailingOffsetPct := pol.Def("trailingOffsetPct", 0.005, core.PNorm(0.001, 0.02)) + + return &strat.TradeStrat{ + Name: "Bollinger Band Breakout", + WarmupNum: smaPeriod + 10, // 预热K线数量 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多1个多单 + EachMaxShort: 1, // 最多1个空单 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + closePrice := e.Close.Get(0) + + // 检查数据有效性 + if math.IsNaN(closePrice) { + return + } + + // 计算布林带指标 + upper, _, lower := ta.BBANDS(e.Close, smaPeriod, mult, mult) + + // 检测突破信号 + longSignal := e.Close.Cross(upper) // 上穿上轨,返回1表示上穿 + shortSignal := e.Close.Cross(lower) // 下穿下轨,返回-1表示下穿 + + // 多头入场逻辑 + if longSignal == 1 && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + Short: false, + OrderType: core.OrderTypeMarket, + }) + } + + // 空头入场逻辑 + if shortSignal == -1 && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + OrderType: core.OrderTypeMarket, + }) + } + + // 更新追踪止损 + updateTrailingStop(s, closePrice, trailingLossPct, trailingPointsPct, trailingOffsetPct) + }, + } +} + +// 更新追踪止损的辅助函数 +func updateTrailingStop(s *strat.StratJob, closePrice, lossPct, pointsPct, offsetPct float64) { + // 处理多头仓位的追踪止损 + for _, order := range s.LongOrders { + if order.Status >= 2 { // 已入场 + entryPrice := order.Enter.Average + maxLossPrice := entryPrice * (1 - lossPct) // 最大损失价格 + activationPrice := entryPrice * (1 + pointsPct) // 激活追踪的价格 + + // 获取当前止损信息 + currentStop := order.GetStopLoss() + + var newStopPrice float64 + + // 判断是否已激活追踪止损 + if closePrice >= activationPrice { + // 已激活,使用偏移量计算止损 + newStopPrice = closePrice * (1 - offsetPct) + } else { + // 未激活,使用最大损失作为止损 + newStopPrice = maxLossPrice + } + + // 只有当新止损价格更优(更高)时才更新 + if currentStop == nil || newStopPrice > currentStop.Price { + order.SetStopLoss(&ormo.ExitTrigger{ + Price: newStopPrice, + Tag: "trailing_stop", + }) + } + } + } + + // 处理空头仓位的追踪止损 + for _, order := range s.ShortOrders { + if order.Status >= 2 { // 已入场 + entryPrice := order.Enter.Average + maxLossPrice := entryPrice * (1 + lossPct) // 最大损失价格 + activationPrice := entryPrice * (1 - pointsPct) // 激活追踪的价格 + + // 获取当前止损信息 + currentStop := order.GetStopLoss() + + var newStopPrice float64 + + // 判断是否已激活追踪止损 + if closePrice <= activationPrice { + // 已激活,使用偏移量计算止损 + newStopPrice = closePrice * (1 + offsetPct) + } else { + // 未激活,使用最大损失作为止损 + newStopPrice = maxLossPrice + } + + // 只有当新止损价格更优(更低)时才更新 + if currentStop == nil || newStopPrice < currentStop.Price { + order.SetStopLoss(&ormo.ExitTrigger{ + Price: newStopPrice, + Tag: "trailing_stop", + }) + } + } + } +} diff --git a/llmjob/7050.go b/llmjob/7050.go new file mode 100644 index 0000000..ec320af --- /dev/null +++ b/llmjob/7050.go @@ -0,0 +1,96 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" +) + +type NinjaState struct { + longEntryBar int64 + shortEntryBar int64 +} + +func J7050(pol *config.RunPolicyConfig) *strat.TradeStrat { + lookbackPeriods := pol.DefInt("lookbackPeriods", 4, core.PUniform(2, 8)) + entryOffsetRatio := pol.Def("entryOffsetRatio", 0.85, core.PUniform(0.2, 0.9)) + barsToHold := pol.DefInt("barsToHold", 0, core.PUniform(0, 5)) + return &strat.TradeStrat{ + WarmupNum: lookbackPeriods + 1, + HedgeOff: true, + TimeFrames: "4h", + OnStartUp: func(s *strat.StratJob) { + s.More = &NinjaState{ + longEntryBar: -1, + shortEntryBar: -1, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state := s.More.(*NinjaState) + currentBar := e.TimeStop / e.TFMSecs + + // 修复:在K线开始时,首先检查是否需要平掉上一根K线建的仓位(对应原始策略中的 K 线收盘平仓) + if barsToHold == 0 && s.OrderNum > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "close_at_bar_close"}) + } + + // 获取历史K线数据 + low1 := e.Low.Get(1) + low2 := e.Low.Get(2) + low3 := e.Low.Get(3) + high1 := e.High.Get(1) + high2 := e.High.Get(2) + high3 := e.High.Get(3) + c := e.Close.Get(0) + + // 优化:缓存重复计算 + range1 := high1 - low1 + + // 计算做多条件:连续3根K线最低价递减且当前收盘价 <= low[1] + longCondition := low1 > low2 && low2 > low3 && c <= low1 + if longCondition && len(s.LongOrders) == 0 { + // 计算止损触发价:low[1] + (high[1]-low[1]) * entryOffsetRatio + stopPrice := low1 + range1*entryOffsetRatio + s.OpenOrder(&strat.EnterReq{ + Tag: "BarUp", + Stop: stopPrice, + }) + state.longEntryBar = currentBar + } + + // 计算做空条件:连续3根K线最高价递增且当前收盘价 >= high[1] + shortCondition := high1 < high2 && high2 < high3 && c >= high1 + if shortCondition && len(s.ShortOrders) == 0 { + // 计算止损触发价:high[1] - (high[1]-low[1]) * entryOffsetRatio + stopPrice := high1 - range1*entryOffsetRatio + s.OpenOrder(&strat.EnterReq{ + Tag: "BarDown", + Short: true, + Stop: stopPrice, + }) + state.shortEntryBar = currentBar + } + + // 平仓逻辑:达到持仓K线数限制 (当 barsToHold > 0 时的增强逻辑) + if barsToHold > 0 { + if len(s.LongOrders) > 0 && state.longEntryBar >= 0 && + currentBar-state.longEntryBar >= int64(barsToHold) { + s.CloseOrders(&strat.ExitReq{Tag: "close_long", Dirt: core.OdDirtLong}) + } + if len(s.ShortOrders) > 0 && state.shortEntryBar >= 0 && + currentBar-state.shortEntryBar >= int64(barsToHold) { + s.CloseOrders(&strat.ExitReq{Tag: "close_short", Dirt: core.OdDirtShort}) + } + } + + // 清理已平仓的记录 + if len(s.LongOrders) == 0 { + state.longEntryBar = -1 + } + if len(s.ShortOrders) == 0 { + state.shortEntryBar = -1 + } + }, + } +} diff --git a/llmjob/7096.go b/llmjob/7096.go new file mode 100644 index 0000000..2e74ef1 --- /dev/null +++ b/llmjob/7096.go @@ -0,0 +1,99 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +// fErf 实现Horner方法的误差函数 +func fErf(x float64) float64 { + nT := 1.0 / (1.0 + 0.5*math.Abs(x)) + nAns := 1.0 - nT*math.Exp(-x*x-1.26551223+ + nT*(1.00002368+nT*(0.37409196+nT*(0.09678418+ + nT*(-0.18628806+nT*(0.27886807+nT*(-1.13520398+ + nT*(1.48851587+nT*(-0.82215223+nT*(0.17087277)))))))))) + if x >= 0 { + return nAns + } + return -nAns +} + +// fPSignal 计算P-Signal指标 +func fPSignal(series *ta.Series, period int) *ta.Series { + res := series.To("_psignal", period) + if res.Cached() { + return res + } + + if series.Len() < period { + return res.Append(math.NaN()) + } + + nStDev := ta.StdDev(series, period) + nSma := ta.SMA(series, period) + + stdevVal := nStDev.Get(0) + smaVal := nSma.Get(0) + + var rawPSignal float64 + if stdevVal > 0 { + rawPSignal = fErf(smaVal / (stdevVal * math.Sqrt(2))) + } else { + rawPSignal = math.Copysign(1.0, smaVal) + } + + return res.Append(rawPSignal) +} + +func J7096(pol *config.RunPolicyConfig) *strat.TradeStrat { + nPoints := pol.DefInt("nPoints", 13, core.PUniform(4, 50)) + + return &strat.TradeStrat{ + Name: "P-Signal Strategy", + WarmupNum: nPoints + 10, // 额外预留空间确保计算准确 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多一个多头仓位 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + nIntr := nPoints - 1 + + // 1. 计算价格变化 (ohlc4的变化量) + avgPrice := ta.AvgPrice(e) + priceChange := avgPrice.Sub(avgPrice.Back(1)) + + // 2. 计算P-Signal原始值 + rawPSignal := fPSignal(priceChange, nIntr) + + // 3. 计算平滑P-Signal + nPSignal := ta.SMA(rawPSignal, nIntr) + + // 检查数据有效性 + if nPSignal.Len() < 2 { + return + } + + // 4. 计算P-Signal变化方向 + currentPSignal := nPSignal.Get(0) + prevPSignal := nPSignal.Get(1) + ndPSignal := math.Copysign(1.0, currentPSignal-prevPSignal) + + // 5. 生成交易信号 + if currentPSignal < 0 && ndPSignal > 0 && len(s.LongOrders) == 0 { + // 开多条件:nPSignal < 0 and ndPSignal > 0 + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } else if currentPSignal > 0 && ndPSignal < 0 && len(s.LongOrders) > 0 { + // 平多条件:nPSignal > 0 and ndPSignal < 0 + s.CloseOrders(&strat.ExitReq{ + Tag: "close_long", + }) + } + }, + } +} diff --git a/llmjob/7593.go b/llmjob/7593.go new file mode 100644 index 0000000..9b7436f --- /dev/null +++ b/llmjob/7593.go @@ -0,0 +1,74 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J7593(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调参数 + lengthRSI := pol.DefInt("lengthRSI", 20, core.PNorm(5, 30)) + rsiEntryThs := pol.DefInt("rsiEntryThs", 72, core.PNorm(60, 85)) + rsiExitThs := pol.DefInt("rsiExitThs", 50, core.PNorm(45, 65)) + takeProfitRate := pol.Def("takeProfitRate", 6.0, core.PNorm(1, 15)) + + return &strat.TradeStrat{ + Name: "TrendFollowingRSI", + WarmupNum: lengthRSI + 10, // 预热足够的K线用于RSI计算 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最多持有一个多单 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算RSI指标 + rsi := ta.RSI(e.Close, lengthRSI) + currentRSI := rsi.Get(0) + + // 检查是否有有效的RSI值 + if math.IsNaN(currentRSI) { + return + } + + // 入场条件:RSI > 70 + if currentRSI > float64(rsiEntryThs) && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + Short: false, + }) + } + + // 出场条件:RSI < 55 + if currentRSI < float64(rsiExitThs) && s.Position(core.OdDirtLong, "") > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "rsi_exit", + Dirt: core.OdDirtLong, + }) + } + }, + + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + // 仅对开仓状态的多头订单检查止盈 + if !od.Short || od.Status != ormo.InOutStatusFullEnter { + return nil + } + + // 基于持仓成本计算止盈目标价 + longTakeProfit := od.InitPrice * (1 + takeProfitRate/100) + + // 检查是否达到止盈条件 + if s.Env.Close.Get(0) > longTakeProfit { + return &strat.ExitReq{ + Tag: "tp_exit", + Dirt: core.OdDirtLong, + } + } + return nil + }, + } +} diff --git a/llmjob/7699.go b/llmjob/7699.go new file mode 100644 index 0000000..8882202 --- /dev/null +++ b/llmjob/7699.go @@ -0,0 +1,624 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type HullBusterState struct { + // 交易状态 + entryPrice float64 + stopPrice float64 + lastLevel float64 + maeLevel float64 + pyramidCount int + pyramidWaveID int + entryTrendDir int + consecLosses int + + // 交叉跟踪 + riseALCrossPrice float64 + riseALNetChg float64 + fallALCrossPrice float64 + fallALNetChg float64 + riseALBarsSince int + fallALBarsSince int + + // EMA交叉跟踪 + riseEMCrossPrice float64 + riseEMNetChg float64 + fallEMCrossPrice float64 + fallEMNetChg float64 + riseEMBarsSince int + fallEMBarsSince int + + // ZScore跟踪 + riseZSSince int + fallZSSince int + riseZSCount int + fallZSCount int + riseZSHigh float64 + fallZSLow float64 + aboveZSSince int + belowZSSince int + aboveZSCount int + belowZSCount int + + // 事件状态 + htfEventHigh float64 + htfEventLow float64 + lastHTFEvent int + impulseWaveDir int + impulseWaveID int + + // 状态标志 + nascentEntry bool + hemiTransit bool + feeRise bool + feeFall bool + pullback bool +} + +func J7699(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 可调参数定义 + fastCrossLen := pol.DefInt("fastCrossLen", 9, core.PNorm(5, 20)) + smaCrossRate := pol.Def("smaCrossRate", 2.6, core.PNorm(1.5, 5.0)) + trendDEMALen := pol.DefInt("trendDEMALen", 200, core.PNorm(50, 300)) + fastAlmaLen := pol.DefInt("fastAlmaLen", 10, core.PNorm(5, 30)) + almaLinregRate := pol.Def("almaLinregRate", 3.0, core.PNorm(2.0, 7.0)) + + // 固定参数 + slowCrossLen := int(float64(fastCrossLen) * smaCrossRate) + slowLinRegLen := int(float64(fastAlmaLen) * almaLinregRate) + fastLinRegLen := 30 + fastEMALen := 8 + stochastLen := 14 + minProfit := 0.50 + stopOffset := 0.50 + maxAddOns := 0 + pyramidSpan := 0.0 + tickScaler := 10.0 + chgDivisor := 1000.0 + + // 计算所需预热长度 + warmupNum := fastCrossLen + if slowCrossLen > warmupNum { + warmupNum = slowCrossLen + } + if trendDEMALen > warmupNum { + warmupNum = trendDEMALen + } + if fastAlmaLen > warmupNum { + warmupNum = fastAlmaLen + } + if slowLinRegLen > warmupNum { + warmupNum = slowLinRegLen + } + if fastLinRegLen > warmupNum { + warmupNum = fastLinRegLen + } + if fastEMALen > warmupNum { + warmupNum = fastEMALen + } + if stochastLen > warmupNum { + warmupNum = stochastLen + } + warmupNum += 100 // 为ZScore等计算预留空间 + + return &strat.TradeStrat{ + WarmupNum: warmupNum, + HedgeOff: true, // 单向持仓 + TimeFrames: "8h", + OnStartUp: func(s *strat.StratJob) { + s.More = &HullBusterState{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + // 需要两个高时间框架:信号周期和趋势周期 + return []*strat.PairSub{ + {"_cur_", "1h", warmupNum}, // 信号周期 + {"_cur_", "4h", warmupNum}, // 趋势周期 + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair string, tf string) { + // 处理高时间框架数据 + if tf == "1h" { + // 将1h数据存储到环境中 + s.Env.Data.Store("_cur_1h", e) + } else if tf == "4h" { + // 将4h数据存储到环境中 + s.Env.Data.Store("_cur_4h", e) + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*HullBusterState) + + // 当前周期指标计算 + fastCross := ta.SMA(e.Close, fastCrossLen) + slowCross := ta.SMA(e.Close, slowCrossLen) + fastEMA := ta.EMA(e.Close, fastEMALen) + + // 使用Stochastic指标而非KDJ + fastK := Stochastic(e.High, e.Low, e.Close, stochastLen) + slowD := ta.SMA(fastK, 3) + + // 交叉信号 - 只计算一次 + crossValue := fastCross.Cross(slowCross) + crossRise := crossValue == 1 + crossFall := crossValue == -1 + + // 从高时间框架获取数据 + htfSignalData, _ := s.Env.Data.Load("_cur_1h") + htfTrendData, _ := s.Env.Data.Load("_cur_4h") + + if htfSignalData == nil || htfTrendData == nil { + return + } + + signalEnv := htfSignalData.(*ta.BarEnv) + trendEnv := htfTrendData.(*ta.BarEnv) + + // HTF指标 + slowLinReg := ta.LinReg(trendEnv.Close, slowLinRegLen) + fastLinReg := ta.LinReg(signalEnv.Close, fastLinRegLen) + fastAlma := ta.ALMA(signalEnv.Close, fastAlmaLen, 6.0, 0.85) + trendDEMA := DEMA(trendEnv.Close, trendDEMALen) + + // ZScore计算 + almaMean := ta.SMA(fastAlma, 100) + almaMarks := fastAlma.Sub(almaMean) + almaStdDev := ta.StdDev(almaMarks, 100) + zScore := almaMarks.Div(almaStdDev) + + // HTF Stochastic + htfFastK := Stochastic(trendEnv.High, trendEnv.Low, trendEnv.Close, stochastLen) + htfDLine := ta.SMA(htfFastK, 3) + htfSlowD := ta.SMA(htfDLine, 3) + + // 信号条件 - 使用Get(1)获取前一根K线值以对齐HTF数据 + closePrice := e.Close.Get(0) + trendDEMAVal := trendDEMA.Get(1) + fastAlmaVal := fastAlma.Get(1) + fastLinRegVal := fastLinReg.Get(1) + slowLinRegVal := slowLinReg.Get(1) + zScoreVal := zScore.Get(1) + + // 趋势信号 + htfSignalRise := fastAlmaVal > fastLinRegVal + htfSignalFall := fastAlmaVal < fastLinRegVal + htfFastAboveTrend := fastAlmaVal > trendDEMAVal && fastLinRegVal > trendDEMAVal + htfFastBelowTrend := fastAlmaVal < trendDEMAVal && fastLinRegVal < trendDEMAVal + + // 计算ALMA事件 + updateALEvents(state, fastAlmaVal, fastLinRegVal, closePrice, tickScaler, chgDivisor) + + // 计算EMA事件 + fastEMAVal := fastEMA.Get(0) + updateEMEvents(state, fastEMAVal, fastAlmaVal, closePrice, tickScaler, chgDivisor) + + // 更新ZScore状态 + updateZScoreState(state, zScoreVal, closePrice) + + // 更新脉冲波方向 + waveDirNow := 0 + if fastLinRegVal > slowLinRegVal { + waveDirNow = 1 + } else if fastLinRegVal < slowLinRegVal { + waveDirNow = -1 + } + + if waveDirNow != state.impulseWaveDir { + state.impulseWaveDir = waveDirNow + state.impulseWaveID++ + } + + // 计算事件 + riseALEvent := checkRiseEvent(state, 10.0, 0.1) + fallALEvent := checkFallEvent(state, 10.0, 0.1) + riseEMEvent := checkRiseEMEvent(state, 3.0, 0.8) + fallEMEvent := checkFallEMEvent(state, 4.0, 0.8) + + // Stochastic事件 + fullFanCone := fastEMAVal > fastAlmaVal && fastAlmaVal > fastLinRegVal && fastLinRegVal > slowLinRegVal + downFanCone := fastEMAVal < fastAlmaVal && fastAlmaVal < fastLinRegVal && fastLinRegVal < slowLinRegVal + partialTrendRise := fastEMAVal > fastAlmaVal && fastAlmaVal > slowLinRegVal && fastLinRegVal > slowLinRegVal + partialTrendFall := fastEMAVal < fastAlmaVal && fastAlmaVal < slowLinRegVal && fastLinRegVal < slowLinRegVal + durableTrendRise := partialTrendRise && slowLinRegVal > trendDEMAVal + durableTrendFall := partialTrendFall && slowLinRegVal < trendDEMAVal + + riseStochas := (htfDLine.Get(1) > 80 && fullFanCone) || + (htfSlowD.Get(1) > 90 && durableTrendRise) || + (htfDLine.Get(1) > 90 && partialTrendRise && state.htfEventHigh > 0 && closePrice > state.htfEventHigh) + + fallStochas := (htfDLine.Get(1) < 20 && downFanCone) || + (htfSlowD.Get(1) < 10 && durableTrendFall) || + (htfDLine.Get(1) < 10 && partialTrendFall && state.htfEventLow > 0 && closePrice < state.htfEventLow) + + // 更新HTF事件高低点 + if riseALEvent { + state.htfEventHigh = closePrice + state.lastHTFEvent = 1 + } + if fallALEvent { + state.htfEventLow = closePrice + state.lastHTFEvent = -1 + } + + // Nascent Entry条件 + htfNascentRise := fastAlmaVal > slowLinRegVal && fastLinRegVal > slowLinRegVal && slowLinRegVal < trendDEMAVal + htfNascentFall := fastAlmaVal < slowLinRegVal && fastLinRegVal < slowLinRegVal && slowLinRegVal > trendDEMAVal + + // 检查是否有持仓 + positionDir := 0 + if len(s.LongOrders) > 0 { + positionDir = 1 + } else if len(s.ShortOrders) > 0 { + positionDir = -1 + } + + alreadyIn := positionDir != 0 + + // 计算盈利状态 + var currentOrder *ormo.InOutOrder + if len(s.LongOrders) > 0 { + currentOrder = s.LongOrders[0] + } else if len(s.ShortOrders) > 0 { + currentOrder = s.ShortOrders[0] + } + + profitable := false + if currentOrder != nil { + profitAmount := currentOrder.ProfitRate * currentOrder.Leverage * state.entryPrice + profitable = profitAmount >= minProfit + } + + // 更新ZScore退出条件 + if alreadyIn { + if zScoreVal > 5.0 { + state.feeRise = true + } + if zScoreVal < -5.0 { + state.feeFall = true + } + } + + // 入场逻辑(包含Nascent Entry) + if !alreadyIn { + if crossRise { + longNascent := false + if closePrice <= trendDEMAVal && htfNascentRise { + longNascent = true + state.nascentEntry = true + } + + if closePrice > trendDEMAVal || longNascent { + stopPrice := e.Low.Get(0) - stopOffset + s.OpenOrder(&strat.EnterReq{ + Tag: "WBHDrive-L", + Short: false, + StopLoss: stopPrice, + StopLossTag: "stop_loss", + }) + state.entryPrice = closePrice + state.stopPrice = stopPrice + state.lastLevel = closePrice + state.entryTrendDir = 1 + if !longNascent { + state.nascentEntry = false + } + } + } else if crossFall { + shortNascent := false + if closePrice >= trendDEMAVal && htfNascentFall { + shortNascent = true + state.nascentEntry = true + } + + if closePrice < trendDEMAVal || shortNascent { + stopPrice := e.High.Get(0) + stopOffset + s.OpenOrder(&strat.EnterReq{ + Tag: "WBHDrive-S", + Short: true, + StopLoss: stopPrice, + StopLossTag: "stop_loss", + }) + state.entryPrice = closePrice + state.stopPrice = stopPrice + state.lastLevel = closePrice + state.entryTrendDir = -1 + if !shortNascent { + state.nascentEntry = false + } + } + } + } + + // 出场逻辑 + if alreadyIn && profitable { + shouldClose := false + + if positionDir == 1 { // 多头 + // 交叉退出 + if htfSignalFall { + shouldClose = true + } + // 信号退出 + if htfSignalRise && riseALEvent && state.feeRise { + shouldClose = true + } + // 事件退出 + if riseEMEvent && fastEMAVal > fastAlmaVal && htfFastAboveTrend { + shouldClose = true + } + // Stochastic退出 + if riseStochas { + shouldClose = true + } + } else if positionDir == -1 { // 空头 + // 交叉退出 + if htfSignalRise { + shouldClose = true + } + // 信号退出 + if htfSignalFall && fallALEvent && state.feeFall { + shouldClose = true + } + // 事件退出 + if fallEMEvent && fastEMAVal < fastAlmaVal && htfFastBelowTrend { + shouldClose = true + } + // Stochastic退出 + if fallStochas { + shouldClose = true + } + } + + if shouldClose { + if positionDir == 1 { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_long", + Dirt: core.OdDirtLong, + }) + } else { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit_short", + Dirt: core.OdDirtShort, + }) + } + + // 重置状态 + if profitable { + state.consecLosses = 0 + } else { + state.consecLosses++ + } + state.entryPrice = 0 + state.stopPrice = 0 + state.lastLevel = 0 + state.pyramidCount = 0 + state.feeRise = false + state.feeFall = false + } + } + + // 完整的金字塔加仓逻辑 + if alreadyIn && maxAddOns > 0 && state.consecLosses < maxAddOns { + // 检测pullback条件 + if positionDir == 1 && slowD.Get(0) < 20 { + if fallALEvent || fallStochas { + state.pullback = true + } + } else if positionDir == -1 && slowD.Get(0) > 80 { + if riseALEvent || riseStochas { + state.pullback = true + } + } + + shouldPyramid := false + + if positionDir == 1 { + // 完整的多头金字塔逻辑 + exceedingSpan := pyramidSpan > 0 && (state.lastLevel-closePrice) > pyramidSpan + + if (crossRise && closePrice > trendDEMAVal && htfFastAboveTrend && closePrice > state.lastLevel && state.pullback) || + (closePrice < state.lastLevel && exceedingSpan && closePrice > state.entryPrice && (fallALEvent || fallEMEvent || fallStochas)) || + (closePrice < state.lastLevel && exceedingSpan && closePrice < state.entryPrice && fallALEvent && fallStochas && state.impulseWaveID > state.pyramidWaveID) { + shouldPyramid = true + } + } else if positionDir == -1 { + // 完整的空头金字塔逻辑 + exceedingSpan := pyramidSpan > 0 && (closePrice-state.lastLevel) > pyramidSpan + + if (crossFall && closePrice < trendDEMAVal && htfFastBelowTrend && closePrice < state.lastLevel && state.pullback) || + (closePrice > state.lastLevel && exceedingSpan && closePrice < state.entryPrice && (riseALEvent || riseEMEvent || riseStochas)) || + (closePrice > state.lastLevel && exceedingSpan && closePrice > state.entryPrice && riseALEvent && riseStochas && state.impulseWaveID > state.pyramidWaveID) { + shouldPyramid = true + } + } + + if shouldPyramid { + if positionDir == 1 { + stopPrice := e.Low.Get(0) - stopOffset + s.OpenOrder(&strat.EnterReq{ + Tag: "WBHDrive-L-Pyramid", + Short: false, + StopLoss: stopPrice, + StopLossTag: "stop_loss", + }) + } else { + stopPrice := e.High.Get(0) + stopOffset + s.OpenOrder(&strat.EnterReq{ + Tag: "WBHDrive-S-Pyramid", + Short: true, + StopLoss: stopPrice, + StopLossTag: "stop_loss", + }) + } + state.pyramidCount++ + state.pyramidWaveID = state.impulseWaveID + state.lastLevel = closePrice + state.pullback = false + } + } + }, + } +} + +// 实现与PineScript stoch()函数一致的Stochastic指标 +func Stochastic(high, low, close *ta.Series, length int) *ta.Series { + llowest := ta.Lowest(low, length) + hhighest := ta.Highest(high, length) + k := close.Sub(llowest).Div(hhighest.Sub(llowest)).Mul(100) + return k +} + +// 辅助函数:更新ALMA事件状态(包含tick标准化) +func updateALEvents(state *HullBusterState, fastAlma, fastLinReg, closePrice, tickScaler, chgDivisor float64) { + fxMinTick := tickScaler + + if fastAlma > fastLinReg { + if state.fallALBarsSince > 0 { // 从下跌转为上涨 + state.fallALBarsSince = 0 + state.fallALCrossPrice = 0 + state.riseALBarsSince = 1 + state.riseALCrossPrice = closePrice + state.riseALNetChg = 0 + } else { + state.riseALBarsSince++ + if state.riseALCrossPrice > 0 { + divisor := chgDivisor + if chgDivisor <= 1.0 { + divisor = state.riseALCrossPrice + } + state.riseALNetChg = ((closePrice - state.riseALCrossPrice) / fxMinTick / divisor) * 100.0 + } + } + } else if fastAlma < fastLinReg { + if state.riseALBarsSince > 0 { // 从上涨转为下跌 + state.riseALBarsSince = 0 + state.riseALCrossPrice = 0 + state.fallALBarsSince = 1 + state.fallALCrossPrice = closePrice + state.fallALNetChg = 0 + } else { + state.fallALBarsSince++ + if state.fallALCrossPrice > 0 { + divisor := chgDivisor + if chgDivisor <= 1.0 { + divisor = state.fallALCrossPrice + } + state.fallALNetChg = ((state.fallALCrossPrice - closePrice) / fxMinTick / divisor) * 100.0 + } + } + } +} + +// 辅助函数:更新EMA事件状态(包含tick标准化) +func updateEMEvents(state *HullBusterState, fastEMA, fastAlma, closePrice, tickScaler, chgDivisor float64) { + fxMinTick := tickScaler + + if fastEMA > fastAlma { + if state.fallEMBarsSince > 0 { + state.fallEMBarsSince = 0 + state.fallEMCrossPrice = 0 + state.riseEMBarsSince = 1 + state.riseEMCrossPrice = closePrice + state.riseEMNetChg = 0 + } else { + state.riseEMBarsSince++ + if state.riseEMCrossPrice > 0 { + divisor := chgDivisor + if chgDivisor <= 1.0 { + divisor = state.riseEMCrossPrice + } + state.riseEMNetChg = ((closePrice - state.riseEMCrossPrice) / fxMinTick / divisor) * 100.0 + } + } + } else if fastEMA < fastAlma { + if state.riseEMBarsSince > 0 { + state.riseEMBarsSince = 0 + state.riseEMCrossPrice = 0 + state.fallEMBarsSince = 1 + state.fallEMCrossPrice = closePrice + state.fallEMNetChg = 0 + } else { + state.fallEMBarsSince++ + if state.fallEMCrossPrice > 0 { + divisor := chgDivisor + if chgDivisor <= 1.0 { + divisor = state.fallEMCrossPrice + } + state.fallEMNetChg = ((state.fallEMCrossPrice - closePrice) / fxMinTick / divisor) * 100.0 + } + } + } +} + +// 辅助函数:更新ZScore状态 +func updateZScoreState(state *HullBusterState, zScore, closePrice float64) { + state.aboveZSSince++ + state.belowZSSince++ + state.riseZSSince++ + state.fallZSSince++ + + // 简化的ZScore跟踪 + if zScore > 0 { + if state.aboveZSSince > 0 { + state.aboveZSSince = 0 + state.aboveZSCount = 1 + } else { + state.aboveZSCount++ + } + } + + if zScore < 0 { + if state.belowZSSince > 0 { + state.belowZSSince = 0 + state.belowZSCount = 1 + } else { + state.belowZSCount++ + } + } +} + +// 辅助函数:检查上升事件 +func checkRiseEvent(state *HullBusterState, minNetChg, minROC float64) bool { + return state.riseALBarsSince > 0 && + state.riseALNetChg > 0 && + state.riseALNetChg >= minNetChg && + (state.riseALNetChg/float64(state.riseALBarsSince)) >= minROC +} + +// 辅助函数:检查下降事件 +func checkFallEvent(state *HullBusterState, minNetChg, minROC float64) bool { + return state.fallALBarsSince > 0 && + state.fallALNetChg > 0 && + state.fallALNetChg >= minNetChg && + (state.fallALNetChg/float64(state.fallALBarsSince)) >= minROC +} + +// 辅助函数:检查EMA上升事件 +func checkRiseEMEvent(state *HullBusterState, minNetChg, minROC float64) bool { + return state.riseEMBarsSince > 0 && + state.riseEMNetChg > 0 && + state.riseEMNetChg >= minNetChg && + (state.riseEMNetChg/float64(state.riseEMBarsSince)) >= minROC +} + +// 辅助函数:检查EMA下降事件 +func checkFallEMEvent(state *HullBusterState, minNetChg, minROC float64) bool { + return state.fallEMBarsSince > 0 && + state.fallEMNetChg > 0 && + state.fallEMNetChg >= minNetChg && + (state.fallEMNetChg/float64(state.fallEMBarsSince)) >= minROC +} + +// DEMA实现(双重指数移动平均) +func DEMA(series *ta.Series, period int) *ta.Series { + ema1 := ta.EMA(series, period) + ema2 := ta.EMA(ema1, period) + return ema1.Mul(2).Sub(ema2) +} diff --git a/llmjob/7801.go b/llmjob/7801.go new file mode 100644 index 0000000..2603535 --- /dev/null +++ b/llmjob/7801.go @@ -0,0 +1,72 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" +) + +// ConsecutiveState 存储每个品种的连续计数状态 +type ConsecutiveState struct { + ups float64 // 连续上涨K线数 + dns float64 // 连续下跌K线数 +} + +func J7801(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义超参数 + consecutiveBarsUp := pol.DefInt("consecutiveBarsUp", 3, core.PNorm(2, 12)) + consecutiveBarsDown := pol.DefInt("consecutiveBarsDown", 4, core.PNorm(2, 12)) + + return &strat.TradeStrat{ + Name: "Consecutive Up/Down Strategy", + WarmupNum: 5, // 需要至少5根K线来确保前值有效 + HedgeOff: true, // 单向持仓模式 + EachMaxLong: 1, // 最大持仓1个多单 + EachMaxShort: 1, // 最大持仓1个空单 + TimeFrames: "8h", + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的状态 + s.More = &ConsecutiveState{ + ups: 0, + dns: 0, + } + }, + + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*ConsecutiveState) + + // 获取当前和前一根K线的收盘价 + currentPrice := e.Close.Get(0) + prevPrice := e.Close.Get(1) + + // 更新连续上涨计数器 + if currentPrice > prevPrice { + state.ups = state.ups + 1 + state.dns = 0 // 重置连续下跌计数 + } else if currentPrice < prevPrice { + state.dns = state.dns + 1 + state.ups = 0 // 重置连续上涨计数 + } else { + // 价格持平,重置两个计数器 + state.ups = 0 + state.dns = 0 + } + + // 检查买入条件:连续上涨达到阈值且没有多单 + if state.ups >= float64(consecutiveBarsUp) && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "ConsUpLE", + }) + } + + // 检查卖出条件:连续下跌达到阈值且没有空单 + if state.dns >= float64(consecutiveBarsDown) && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "ConsDnSE", + Short: true, + }) + } + }, + } +} diff --git a/llmjob/7854.go b/llmjob/7854.go new file mode 100644 index 0000000..ae4934c --- /dev/null +++ b/llmjob/7854.go @@ -0,0 +1,102 @@ +package llmjob + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +// COG 自定义重心振荡器指标 +func COG(src *ta.Series, period int) *ta.Series { + res := src.To("_cog", period) + if res.Cached() { + return res + } + + data := src.Range(0, period) + if len(data) < period { + return res.Append(math.NaN()) + } + + var sum1, sum2 float64 + center := float64(period-1) / 2.0 + + for i := 0; i < period; i++ { + weight := float64(i) - center + sum1 += data[i] * weight + sum2 += weight * weight + } + + if sum2 != 0 { + cogVal := -sum1 / sum2 + return res.Append(cogVal) + } + return res.Append(0) +} + +func J7854(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + length := pol.DefInt("length", 9, core.PNorm(3, 50)) // COG计算周期 + length5 := pol.DefInt("length5", 3, core.PNorm(2, 20)) // RMA平滑周期 + windowsize := pol.DefInt("windowsize", 6, core.PNorm(2, 20)) // ALMA窗口周期 + offset := pol.Def("offset", 0.88, core.PNorm(0.5, 0.99)) // ALMA偏移量 + sigma := pol.Def("sigma", 7, core.PNorm(1, 10)) // ALMA高斯系数 + + return &strat.TradeStrat{ + WarmupNum: length + 50, // 确保有足够的历史数据 + HedgeOff: true, // 单向持仓 + EachMaxLong: 1, // 最多1个多单 + EachMaxShort: 0, // 禁止做空 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 检查数据长度是否足够 + if e.Close.Len() < length { + return + } + + // 计算COG指标 + cog1 := COG(e.Close, length) + + // 使用RMA平滑COG(对应原代码中的smooth选项,这里默认使用RMA) + cogSmoothed := ta.RMA(cog1, length5) + cog := cogSmoothed + + // 计算ALMA触发线 + trigger := ta.ALMA(cog, windowsize, sigma, offset) + + // 获取最新值 + cog1Val := cog1.Get(0) + triggerVal := trigger.Get(0) + + // 检查有效数据 + if math.IsNaN(cog1Val) || math.IsNaN(triggerVal) { + return + } + + // 检测COG1与触发线的交叉状态,只计算一次 + crossState := cog1.Cross(trigger) + + // 检测COG1上穿触发线 + if crossState == 1 { + // 产生买入信号 + s.OpenOrder(&strat.EnterReq{ + Tag: "COG_Enter", + }) + } + + // 可选:添加退出逻辑 + // 这里可以根据需要添加退出条件,比如COG1下穿trigger线 + if crossState == -1 && len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "COG_Exit", + Dirt: core.OdDirtLong, + }) + } + }, + } +} diff --git a/llmjob/8179.go b/llmjob/8179.go new file mode 100644 index 0000000..6ef45ac --- /dev/null +++ b/llmjob/8179.go @@ -0,0 +1,61 @@ +package llmjob + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func J8179(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + lookback := pol.DefInt("lookback", 20, core.PNorm(5, 25)) // 随机振荡器计算周期 + kSmoothing := pol.DefInt("kSmoothing", 4, core.PNorm(1, 10)) // %K线平滑周期 + maLookback := pol.DefInt("maLookback", 19, core.PNorm(5, 30)) // %K线均线周期 + + return &strat.TradeStrat{ + WarmupNum: lookback + kSmoothing + maLookback + 10, // 确保有足够的历史数据 + EachMaxLong: 1, // 每个品种最多1个多单 + EachMaxShort: 1, // 每个品种最多1个空单 + TimeFrames: "8h", + OnBar: func(s *strat.StratJob) { + e := s.Env + + // 计算随机振荡器 KDJ + // KDJ返回[K, D, RSV],其中K就是我们需要的%K线 + stochK, _, _ := ta.KDJ(e.High, e.Low, e.Close, lookback, kSmoothing, 3) + + // 计算%K线的移动平均线 + maStochK := ta.SMA(stochK, maLookback) + + // 检测交叉信号 + // Cross返回: 1=上穿, -1=下穿, 0=重合或未知 + crossSignal := stochK.Cross(maStochK) + + // 根据交叉信号生成交易决策 + if crossSignal == 1 { + // %K线上穿其均线,买入做多 + s.OpenOrder(&strat.EnterReq{ + Tag: "long_cross", + Short: false, + }) + // 同时平掉空单(如果有) + s.CloseOrders(&strat.ExitReq{ + Tag: "close_short", + Dirt: core.OdDirtShort, + }) + } else if crossSignal == -1 { + // %K线下穿其均线,卖出做空 + s.OpenOrder(&strat.EnterReq{ + Tag: "short_cross", + Short: true, + }) + // 同时平掉多单(如果有) + s.CloseOrders(&strat.ExitReq{ + Tag: "close_long", + Dirt: core.OdDirtLong, + }) + } + }, + } +} diff --git a/llmjob/845.go b/llmjob/845.go new file mode 100644 index 0000000..a5f533c --- /dev/null +++ b/llmjob/845.go @@ -0,0 +1,226 @@ +package llmjob + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/utils" + ta "github.com/banbox/banta" + "math" +) + +/* +需要多品种综合信息,品种数不得少于30 +做多:RSI超卖、RSI轻度超买跟随 +做空:全市场超卖动能枯竭做空 +*/ + +// State845 存储每个品种的RSI动态区间策略状态 +type State845 struct { + position int // 当前持仓状态: 0=无持仓, 1=多头, -1=空头 + overBoughtDyn float64 // 动态超买水平 + overSoldDyn float64 // 动态超卖水平 + rsi float64 + maSub float64 + atr float64 +} + +// RsiDynamic RSI动态区间反转策略 +func J845(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + rsiPeriod := pol.DefInt("rsiPeriod", 12, core.PNorm(7, 21)) + overBought := pol.Def("overBought", 76, core.PNorm(70, 90)) + overSold := pol.Def("overSold", 26, core.PNorm(20, 40)) + sensitivity := pol.Def("sensitivity", 2.5, core.PNorm(1.0, 5.0)) + + openRate, closeRate := float64(1), float64(0) + prevRsiAvg, prevRsiBig, rsiDelta := float64(0), float64(0), float64(0) + prevOverBuy, prevStamp, prevOpenRate := 0, int64(0), float64(0) + stopLongUntil := int64(0) + dayMS := int64(utils.TFToSecs("1d") * 1000) + return &strat.TradeStrat{ + WarmupNum: 91, // 预热K线数量 + EachMaxLong: 1, // 最多开1个多单 + EachMaxShort: 1, // 最多开1个空单 + RunTimeFrames: []string{"1d"}, + BatchInOut: true, + OnStartUp: func(s *strat.StratJob) { + // 初始化每个品种的状态 + s.More = &State845{ + position: 0, + overBoughtDyn: overBought, + overSoldDyn: overSold, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*State845) + + rsi := ta.RSI(e.Close, rsiPeriod) + rsiValue := rsi.Get(0) + state.rsi = rsiValue + state.atr = ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + ma20 := ta.SMA(e.Close, 20).Get(0) + ma90 := ta.SMA(e.Close, 90).Get(0) + state.maSub = ma20 - ma90 + + // 计算动态超买超卖水平 + calculateDynamicLevels(rsi, sensitivity, overBought, overSold, state) + + // 生成交易信号 + if state.position == 0 && openRate > 0 && e.TimeStop >= stopLongUntil { // 无持仓 + if rsiValue < state.overSoldDyn { + // RSI跌破超卖水平,建立多头仓位 + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + CostRate: openRate, + Log: true, + }) + state.position = 1 + } + } else if state.position == -1 { // 持有空头仓位 + if rsiValue <= state.overBoughtDyn { + // RSI从超买区域回落,平掉多头仓位 + s.CloseOrders(&strat.ExitReq{ + Tag: "exit2", + Dirt: core.OdDirtLong, + EnterTag: "long2", + Log: true, + }) + state.position = 0 + } + } else if state.position == 1 { // 持有多头仓位 + if rsiValue >= state.overSoldDyn { + // RSI从超卖区域回升,平掉多头仓位 + s.CloseOrders(&strat.ExitReq{ + Tag: "exit1", + Dirt: core.OdDirtLong, + EnterTag: "long", + Log: true, + }) + state.position = 0 + } + } + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + rsiAvg, rsiBig := float64(0), float64(0) + overBuyNum, curStamp := 0, int64(0) + var st *strat.TradeStrat + for _, s := range jobs { + state, _ := s.More.(*State845) + rsiAvg += state.rsi + if state.rsi >= 68 { + rsiBig += 1 + } + if state.rsi > state.overBoughtDyn { + overBuyNum += 1 + } + curStamp = s.Env.TimeStop + st = s.Strat + } + numFlt := float64(len(jobs)) + rsiAvg /= numFlt + rsiBig /= numFlt + if prevRsiAvg >= 70 && rsiAvg < prevRsiAvg && prevRsiBig > 0.5 { + // 极端过热转向,平仓,禁止做多2周 + openRate, closeRate = -2, 1 + if prevRsiBig > 0.65 && prevOpenRate < 0 { + openRate = -3 + stopLongUntil = curStamp + dayMS*14 + } + } else if rsiAvg > 77 && rsiBig > 0.75 || rsiAvg > 70 && rsiBig > 0.5 && (rsiAvg-prevRsiAvg)*5 < rsiDelta { + // 严重过热,平仓一半,禁止开仓 + openRate, closeRate = -1, 0.5 + if curStamp < stopLongUntil { + stopLongUntil = max(stopLongUntil, curStamp+dayMS*10) + } + } else if rsiAvg > 70 && rsiBig > 0.5 { + // 中度过热 + openRate, closeRate = 0.6, 0 + if curStamp < stopLongUntil { + stopLongUntil = max(stopLongUntil, curStamp+dayMS*6) + } + } else if rsiAvg > 60 && rsiBig > 0.3 { + // 轻度过热 + openRate, closeRate = 0.8, 0 + } else { + openRate, closeRate = 1, 0 + } + if closeRate > 0 { + for _, s := range jobs { + if s.OrderNum > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "cut", + Dirt: core.OdDirtLong, + ExitRate: closeRate, + Log: true, + }) + } + } + } + if overBuyNum >= 2 || overBuyNum+prevOverBuy >= 3 { + // 超买跟随,仅适用于多数品种一同上涨,所以要限制数量 + for _, s := range jobs { + state, _ := s.More.(*State845) + if state.rsi > state.overBoughtDyn { + // RSI突破超买水平,追涨买入 + s.OpenOrder(&strat.EnterReq{ + Tag: "long2", + CostRate: openRate, + Log: true, + }) + state.position = -1 + } + } + } + // 超买做空机制,仅对特定品种表现良好,对于动态品种表现更差 + //if openRate <= -3 { + // for _, s := range jobs { + // state, _ := s.More.(*State845) + // if state.maSub < 0 || state.rsi < 76 || len(s.ShortOrders) > 0 { + // continue + // } + // // 牛市中全市场超买时做空;不可在ObBatchJobs中开单,会变差 + // s.OpenOrder(&strat.EnterReq{ + // Tag: "short1", + // //Short: true, + // TakeProfitVal: state.atr * 2, + // StopLossVal: state.atr * 2, + // Log: true, + // }) + // } + //} + if openRate < 1 { + st.WriteOutput(fmt.Sprintf("num: %d, rate: %.3f, rsi: %.3f, rsiBig: %.3f", + len(jobs), openRate, rsiAvg, rsiBig), true) + } + if curStamp > prevStamp { + rsiDelta = rsiAvg - prevRsiAvg + prevRsiAvg = rsiAvg + prevRsiBig = rsiBig + prevStamp = curStamp + prevOpenRate = openRate + prevOverBuy = overBuyNum + } + }, + } +} + +// calculateDynamicLevels 计算动态超买超卖水平 +func calculateDynamicLevels(rsi *ta.Series, sensitivity, overBought, overSold float64, state *State845) { + // 计算近期RSI波动率(标准差) + stdDev, _ := ta.StdDevBy(rsi, 14, 1) + rsiVolatility := stdDev.Get(0) + + // 计算动态调整因子 + adjustmentFactor := sensitivity * rsiVolatility / 10 + + // 计算动态超买超卖水平 + state.overBoughtDyn = overBought + adjustmentFactor*(100-overBought)/100 + state.overSoldDyn = overSold - adjustmentFactor*overSold/100 + + // 确保动态水平在合理范围内 + state.overBoughtDyn = math.Max(70, math.Min(95, state.overBoughtDyn)) + state.overSoldDyn = math.Max(5, math.Min(40, state.overSoldDyn)) +} diff --git a/llmjob/README.md b/llmjob/README.md new file mode 100644 index 0000000..14465b6 --- /dev/null +++ b/llmjob/README.md @@ -0,0 +1,66 @@ + +## 不可用的 +3746: 仅适用于BTC,且订单太少 +1408: bug导致的完美曲线:触发价入场时止盈止损可能错误 +1662: 因止盈止损触发价未检查是否合理的bug导致虚假净值曲线 +5689: 因止损未检查bug导致表现看起来很好,不可用 +5609: 仅对ETH有效 +3746: 仅对BTC生效,订单不多 +5115: 波动太大,不可用 +2439: 已失效,波动太大 +3127: 不稳定,且仅针对BTC +5129: 15m,不可用,且过于复杂 +4316: 15m,不可用,且过于复杂 +3121: 5m 净亏 +199: 4h 波动较大 +4670: 仅适用BTC,波动大 +3614: 仅适用BTC,1h波动大 +3707: 仅适用BTC +656: 近期回撤较大 + +## 不够好 +4854: 3d波动略大,不止损 +4267: 一般 +3644: 不够好,波动大 +3827: 不够好 +2391: 波动大 +3583: 15m,不够稳定,闲置较多 +4067: 4h,较复杂,不太稳定 +4994: 4h,上升趋势明显,但局部波动较大 +5743: 8h,趋势,最近回撤明显 +559: 8h,经典MACD金叉死叉 +128: 4h,较复杂,最近回撤明显 +2251: 4h,近期回撤明显 +592: 8h, 最近略有回撤 +2874: 4h,最近略有回撤 +3137: 15m,long之前曾有回撤 +3623: 网格马丁,和已有重合 +4627: 15m, 2个信号不错,但大部分时间无信号 +3955: 8h,趋势策略,局部波动略大 +4009: 1d,Sell不错,但大部分时间信号少 +1357: 8h, 趋势策略,近期横盘 +3956: 8h, 趋势策略,近期横盘 +3683: 8h, 趋势策略,近期横盘 +5763: 8h, 趋势策略,局部波动略大 +3464: 4h, 趋势策略,局部波动稍微大点 +4526: 8h,马丁&布林带,和已有重合 +4513: 8h, 趋势策略,局部波动略大 +2775: 8h, 最近横盘 +1665: 8h, 较复杂,波动稍大 +2876: 15m波动略大,资金闲置多,不够好;251010亏不少 +3087: 分批加仓,固定止盈;适合波动大的小币种,不如1525稳定,原理类似,暂不使用 +2625: 待定,1h,波动略大;好像有bug + +2984: 还可以,和3992比较像 +4583: 还可以,近期回撤 +3992: 还可以 +4225: 还可以 +1569: 1h还可以,近期有回撤 +4034: 8h还可以,近期不佳 +2523: 3d,胜率高,可能过拟合,失效回撤大 + +## 可用的 +3782: 15m网格,很好 +3762: 15m很好,超跌反弹 +845: 1d RSI超卖超买动能跟随,不错 +1525: 5m很稳定,但瞬时压力大,加仓太多,使用不当可能爆仓 \ No newline at end of file diff --git a/llmjob/his/3762.go b/llmjob/his/3762.go new file mode 100644 index 0000000..fb50e4a --- /dev/null +++ b/llmjob/his/3762.go @@ -0,0 +1,214 @@ +package his + +import ( + "fmt" + "github.com/banbox/banbot/com" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg" + "github.com/banbox/banexg/log" + "github.com/banbox/banexg/utils" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +/* +超跌反弹策略。30个品种为宜 +做空部分没有利润,禁用 +*/ + +type State3762 struct { + NoLongUntil int64 + NoShortUntil int64 +} + +func J3762(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义策略参数 + p := pol.DefInt("p", 20, core.PNorm(5, 50)) + d := pol.Def("d", 30.0, core.PNorm(5, 50)) + mult := pol.Def("mult", 1.5, core.PNorm(1.0, 3.0)) + slRate := pol.Def("slRate", 25, core.PNorm(0, 50)) + needlong := pol.Def("needlong", 1.0, core.PUniform(0, 1)) > 0.5 + needshort := pol.Def("needshort", 0.0, core.PUniform(0, 1)) > 0.5 + + dayMS := int64(utils.TFToSecs("1d") * 1000) + + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + return &strat.TradeStrat{ + WarmupNum: baseAtrLen * 2, // 预热K线数量,确保有足够数据计算指标 + RunTimeFrames: []string{"15m"}, + OnStartUp: func(s *strat.StratJob) { + s.More = &State3762{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*State3762) + // 计算基础布林带指标 + sma := ta.SMA(e.Close, p) + ma7 := ta.SMA(e.Close, 7).Get(0) + ma30 := ta.SMA(e.Close, 30).Get(0) + multCur := mult + if ma7 < ma30 { + // 熊市下移下限 + multCur *= 1.35 + } + stddev := ta.StdDev(e.Close, p).Get(0) * multCur + upperBB := sma.Get(0) + stddev + lowerBB := sma.Get(0) - stddev + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + // 计算百分比偏离轨道 + atrRt := d + if ma7 < ma30 { + // 熊市下移下限 + atrRt *= 1.35 + } + upex2 := sma.Get(0) + atrBase*atrRt + dnex2 := sma.Get(0) - atrBase*atrRt + + // 计算最终轨道 + upexPrice := max(upperBB, (upex2+upperBB)/2) + dnexPrice := min(lowerBB, (dnex2+lowerBB)/2) + + // 获取当前价格 + closePrice := e.Close.Get(0) + openPrice := e.Open.Get(0) + + // 生成交易信号 + exitSignal := closePrice > openPrice // 绿色K线信号 + + if math.IsNaN(upexPrice) || math.IsNaN(dnexPrice) { + return + } + slRange := min(atrBase*slRate, closePrice*0.25) + + // 执行交易逻辑 + if !s.IsWarmUp { + s.Strat.WriteOutput(fmt.Sprintf("%s upp: %.6f, low: %.6f upex2: %.6f, dnex2: %.6f upex: %.6f, dnex: %.6f slRange: %.6f", + e.Symbol, upperBB, lowerBB, upex2, dnex2, upexPrice, dnexPrice, slRange), true) + } + if needlong { + if len(s.LongOrders) == 0 { + // 使用限价单在下轨价格做多,不依赖当前价格是否突破轨道 + if e.TimeStop >= m.NoLongUntil { + curPrice := com.GetPrice(e.Symbol, banexg.OdSideBuy) + if math.Abs(curPrice/closePrice-1) > 0.01 { + log.Info("bad price from GetPrice", zap.String("pair", e.Symbol), + zap.Float64("val", curPrice), zap.Float64("close", closePrice)) + } + s.OpenOrder(&strat.EnterReq{ + Tag: "Long", + Limit: dnexPrice, // 以下轨价格作为限价单 + StopLossVal: slRange, + Log: true, + }) + } + } else { + for _, od := range s.LongOrders { + if od.Status == ormo.InOutStatusInit { + err := od.SetEnterLimit(dnexPrice) + if err != nil { + log.Error("SetEnterLimit fail", zap.Error(err)) + } else { + err = od.SetStopLoss(&ormo.ExitTrigger{ + Price: dnexPrice - slRange, + }) + if err != nil { + log.Error("SetStopLoss fail", zap.Error(err)) + } + } + } else if od.Status == ormo.InOutStatusFullEnter { + barNum := e.BarCount(od.Enter.UpdateAt) + if barNum > 2 && od.ProfitRate < 0 { + // 持仓2个k线,出现亏损,平仓 + s.CloseOrders(&strat.ExitReq{ + Tag: "Exit bad Long", + Dirt: core.OdDirtLong, + Log: true, + }) + m.NoLongUntil = e.TimeStop + dayMS + } + } + } + } + } + + if needshort { + if len(s.ShortOrders) == 0 { + // 使用限价单在上轨价格做空,不依赖当前价格是否突破轨道 + if e.TimeStop >= m.NoShortUntil { + s.OpenOrder(&strat.EnterReq{ + Tag: "Short", + Short: true, + Limit: upexPrice, // 以上轨价格作为限价单 + StopLossVal: slRange, + Log: true, + }) + } + } else { + for _, od := range s.ShortOrders { + if od.Status == ormo.InOutStatusInit { + err := od.SetEnterLimit(upexPrice) + if err != nil { + log.Error("SetEnterLimit fail", zap.Error(err)) + } else { + err = od.SetStopLoss(&ormo.ExitTrigger{ + Price: upexPrice + slRange, + }) + if err != nil { + log.Error("SetStopLoss fail", zap.Error(err)) + } + } + } else if od.Status == ormo.InOutStatusFullEnter { + barNum := e.BarCount(od.Enter.UpdateAt) + if barNum > 2 && od.ProfitRate < 0 { + // 持仓2个k线,出现亏损,平仓 + s.CloseOrders(&strat.ExitReq{ + Tag: "Exit bad short", + Dirt: core.OdDirtShort, + Log: true, + }) + m.NoShortUntil = e.TimeStop + dayMS + } + } + } + } + } + + // 平仓条件:绿色K线 + if exitSignal { + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Exit Long", + Dirt: core.OdDirtLong, + Log: true, + }) + } + } else { + if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "Exit Short", + Dirt: core.OdDirtShort, + Log: true, + }) + } + } + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + if chgType == core.OdChgExitFill && od.ExitTag == core.ExitTagStopLoss { + m, _ := s.More.(*State3762) + if od.Short { + m.NoShortUntil = od.ExitAt + dayMS + } else { + m.NoLongUntil = od.ExitAt + dayMS + } + } + }, + } +} diff --git a/llmjob/his/3782.go b/llmjob/his/3782.go new file mode 100644 index 0000000..ae296ef --- /dev/null +++ b/llmjob/his/3782.go @@ -0,0 +1,270 @@ +package his + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "github.com/sasha-s/go-deadlock" + "math" +) + +/* +需要多品种综合市场信息,品种数不可少于30 +*/ + +// ZoneTradeState 用于存储每个交易区间的状态信息 +type ZoneTradeState struct { + LastTradeTime2 int64 // 买入区间2的最后交易时间 + LastTradeTime3 int64 // 买入区间3的最后交易时间 + LastTradeTime4 int64 // 买入区间4的最后交易时间 + LastSellTime int64 // 最后卖出时间 + atrBase float64 + score float64 + upFlag float64 +} + +func J3782(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可调超参数 + maPeriod := pol.DefInt("maPeriod", 100, core.PNorm(20, 200)) + baseRetracement := pol.Def("baseRetracement", 0.08, core.PNorm(0.05, 0.2)) + retracementRate := pol.Def("retracementRate", 1.8, core.PNorm(1.5, 2.5)) + profitRate := pol.Def("profitRate", 0.03, core.PNorm(0.015, 0.06)) + tradeDelay := pol.Def("tradeDelay", 40, core.PNorm(10, 100)) + slRate := pol.Def("slRate", 63, core.PNorm(5, 35)) + + // 计算各回撤比例 + retracement2 := baseRetracement + retracement3 := baseRetracement * retracementRate + retracement4 := baseRetracement * retracementRate * 1.33 + + lenAtr := 20 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + rates := make([]float64, 0, 10) + + // 最大交易数量 + maxTrades2 := 2 + maxTrades3 := 3 + maxTrades4 := 4 + + openPairs := 0.0 + var pairLock deadlock.Mutex + addThres, firstRate := 1.0, 1.0 + avgAtrBase := 2.5 + maxPair := 3.0 + return &strat.TradeStrat{ + WarmupNum: maPeriod, + BatchInfo: true, + BatchInOut: true, + RunTimeFrames: []string{"15m"}, + OnStartUp: func(s *strat.StratJob) { + // 初始化状态信息 + s.More = &ZoneTradeState{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", baseAtrLen * 2}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + state, _ := s.More.(*ZoneTradeState) + + // 计算MA100 + ma100 := ta.SMA(e.Close, maPeriod) + ma100Val := ma100.Get(0) + + // 计算交易价格水平 + curRate := max(addThres, 1+state.score/10) + itemRate := max(0.7, min(math.Sqrt(state.atrBase/avgAtrBase), 1.4)) + buyPrice2 := ma100Val * (1 - retracement2*curRate*itemRate*firstRate) + buyPrice3 := ma100Val * (1 - retracement3*curRate*itemRate) + buyPrice4 := ma100Val * (1 - retracement4*curRate*itemRate) + sellPrice := ma100Val * (1 + profitRate) + + // 获取当前价格 + lowPrice := e.Low.Get(0) + highPrice := e.High.Get(0) + + // 检查交易间隔条件 + currentBarTime := e.TimeStop + + lastEnt := max(state.LastTradeTime2, state.LastTradeTime3, state.LastTradeTime4) + canTrade := e.BarCount(lastEnt) >= 10 + canTrade2 := canTrade && e.BarCount(state.LastTradeTime2) >= tradeDelay + canTrade3 := canTrade && e.BarCount(state.LastTradeTime3) >= tradeDelay + canTrade4 := canTrade && e.BarCount(state.LastTradeTime4) >= tradeDelay + canSell := e.BarCount(state.LastSellTime) >= tradeDelay + + // 获取当前总开仓数量,与原始策略的strategy.opentrades保持一致 + openTrades := len(s.LongOrders) + len(s.ShortOrders) + + if lowPrice <= buyPrice4 && openTrades < maxTrades4 && canTrade4 { + pairLock.Lock() + allow := false + if openPairs < maxPair { + openPairs += 3 + allow = true + } + pairLock.Unlock() + if allow { + s.OpenOrder(&strat.EnterReq{ + Tag: "BuyZone4", + CostRate: 3, + Log: true, + }) + state.LastTradeTime4 = currentBarTime + } + } + if lowPrice <= buyPrice3 && openTrades < maxTrades3 && canTrade3 { + pairLock.Lock() + allow := false + if openPairs < maxPair { + openPairs += 1.5 + allow = true + } + pairLock.Unlock() + if allow { + s.OpenOrder(&strat.EnterReq{ + Tag: "BuyZone3", + CostRate: 1.5, + Log: true, + }) + state.LastTradeTime3 = currentBarTime + } + } + if lowPrice <= buyPrice2 && openTrades < maxTrades2 && canTrade2 { + pairLock.Lock() + allow := false + if openPairs < maxPair { + openPairs += 1 + allow = true + } + pairLock.Unlock() + if allow { + s.OpenOrder(&strat.EnterReq{ + Tag: "BuyZone2", + CostRate: 1, + Log: true, + }) + state.LastTradeTime2 = currentBarTime + } + } + + // 检查卖出条件 + if s.OrderNum > 0 { + curPrice := e.Close.Get(0) + slRange := slRate * itemRate * curPrice / 100 + avgPrice := s.PositionAvgPrice(core.OdDirtLong, "") + if curPrice+slRange <= avgPrice { + s.CloseOrders(&strat.ExitReq{ + Tag: "StopLoss", + Log: true, + }) + state.LastSellTime = currentBarTime + } else { + exitPrice := avgPrice*(1+profitRate)*0.4 + sellPrice*0.6 + if openTrades > 0 && highPrice >= exitPrice && canSell { + s.CloseOrders(&strat.ExitReq{ + Tag: "SellAll", + Log: true, + }) + state.LastSellTime = currentBarTime + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + score := float64(0) + if ta.RSI(e.Close, 14).Get(0) < 30 { + score += 1 + } + roc := ta.ROC(e.Close, 20).Get(0) + if roc < -20 { + score += 2 + } else if roc < -10 { + score += 1 + } + plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + if minusDi.Get(0) > plusDi.Get(0) { + if adx > 40 { + score += 2 + } else if adx > 25 { + score += 1 + } + } + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + rates = append(rates, atrBase/(e.Close.Get(0)*0.01)) + //openRate := 0.006 * closePrice / atrBase + //if openRate < 1 { + // openRate = math.Sqrt(openRate) + //} + //openRate = max(0.7, min(1.5, openRate)) + ma7 := ta.SMA(e.Close, 7).Get(0) + ma20 := ta.SMA(e.Close, 20) + ma20V := ma20.Get(0) + ma99 := ta.SMA(e.Close, 99).Get(0) + if ma99 > ma20V { + score += 1 + } + state, _ := s.More.(*ZoneTradeState) + state.score = score + state.upFlag = 0 + state.atrBase = atrBase / (e.Close.Get(0) * 0.01) + if ma20V > ma99 && ma20V > ma20.Get(2) { + state.upFlag = 1 + } else if ma7 < min(ma20V, ma99) { + state.upFlag = -1 + } + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + pairLock.Lock() + openPairs = 0 + pairLock.Unlock() + }, + OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) { + if len(jobs) < 3 { + return + } + downNum, downHeavyNum := float64(0), float64(0) + atrRates := make([]float64, 0, len(jobs)) + for _, j := range jobs { + state, _ := j.Job.More.(*ZoneTradeState) + if state.score >= 5 { + downHeavyNum += 1 + } + if state.score >= 3 { + downNum += 1 + } + atrRates = append(atrRates, state.atrBase) + } + jobNum := float64(len(jobs)) + downRate := downNum / jobNum + downHeavyRate := downHeavyNum / jobNum + avgAtrBase = atrRates[len(jobs)/2] + if downHeavyRate >= 0.8 { + // 极端下跌 + addThres, firstRate = 1.6, 1.2 + maxPair = jobNum * 0.5 + } else if downRate >= 0.85 && downHeavyRate >= 0.6 { + // 严重下跌 + addThres, firstRate = 1.45, 1.15 + maxPair = jobNum * 0.55 + } else if downRate >= 0.7 && downRate <= 0.85 { + // 市场中度下跌 + addThres, firstRate = 1.3, 1.1 + maxPair = jobNum * 0.6 + } else if downRate >= 0.5 && downRate <= 0.7 { + // 轻度下跌 + addThres, firstRate = 1.15, 1.05 + maxPair = jobNum * 0.7 + } else { + // 轻度或未下跌 + maxPair = jobNum + addThres, firstRate = 1, 1 + } + }, + } +} diff --git a/llmjob/his/3782.yml b/llmjob/his/3782.yml new file mode 100644 index 0000000..bdf4939 --- /dev/null +++ b/llmjob/his/3782.yml @@ -0,0 +1,66 @@ +env: dry_run +market_type: linear +leverage: 10 +time_start: "20250101" +time_end: "20251112" +stake_amount: 30 +concur_num: 3 +# stake_pct: 30 +max_stake_amt: 5000 +pairs: [] +relay_sim_unfinish: false +stake_currency: [USDT] +wallet_amounts: + USDT: 1400 +run_policy: + - name: llmjob:H3762 + stake_rate: 5 + refine_tf: 1m + - name: llmjob:845 + stake_rate: 5 + refine_tf: 1m + - name: llmjob:H3782 + stake_rate: 1 + refine_tf: 1m +# - name: llmjob:1525 +# run_timeframes: [5m] +# stake_rate: 0.4 +# params: +# max_open_pair: 5 +# max_pair: 30 +# - name: llmjob:2523 +# stake_rate: 20 +pairmgr: + cron: '0 0 0 1 */2 *' # 秒 分钟 小时 日 月 星期 + offset: 0 + limit: 999 + force_filters: false + pos_on_rotation: close +pairlists: + - name: VolumePairList + limit_rate: 0.7 + limit: 360 + min_value: 100000 + cache_secs: 7200 + back_period: 30d + - name: PriceFilter # 价格过滤器 + max_unit_value: 200 # 最大允许的单位价格变动对应的价值(针对定价货币,一般是USDT)。 + precision: 0.0015 # 按价格精度过滤交易对,默认要求价格变动最小单位是0.1% + min: 0.0001 # 最低价格 + max: 500000 # 最高价格 + - name: RateOfChangeFilter # 波动性过滤器 + back_days: 10 # 回顾的K线天数 + min: 0.03 # 最小价格变动比率 + max: 3 # 最大价格变动比率 + cache_secs: 1440 # 缓存时间,秒 + - name: SpreadFilter # 流动性过滤器 + max_ratio: 0.005 # 公式:1-bid/ask,买卖价差占价格的最大比率 + - name: AgeFilter # 按标的的上市天数过滤 + min: 180 + - name: VolatilityFilter # 波动性过滤器,公式:std(log(c/c1)) * sqrt(back_days) + back_days: 10 # 回顾的K线天数 + max: 1 # 波动分数最大值,此值越大,允许一些在1d级别上变化非常剧烈的标的 + min: 0.03 # 波动分数最小值,此值越小,允许一些在1d级别上变化非常小的标的 + - name: OffsetFilter + rate: 0.5 + limit: 30 \ No newline at end of file diff --git a/llmjob/main.go b/llmjob/main.go new file mode 100644 index 0000000..bffacf7 --- /dev/null +++ b/llmjob/main.go @@ -0,0 +1,81 @@ +package llmjob + +import ( + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/llmjob/his" +) + +func init() { + strat.AddStratGroup("llmjob", map[string]strat.FuncMakeStrat{ + "H3762": his.J3762, + "H3782": his.J3782, + "32": J32, + "419": J419, + "570": J570, + "1665": J1665, + "2876": J2876, + "3087": J3087, + "2625": J2625, + "2984": J2984, + "3782": J3782, + "3762": J3762, + "4583": J4583, + "5609": J5609, + "3992": J3992, + "2523": J2523, + "4225": J4225, + "4267": J4267, + "3644": J3644, + "3746": J3746, + "5689": J5689, + "5115": J5115, + "4067": J4067, + "1525": J1525Sim, + "4994": J4994, + "5743": J5743, + "845": J845, + "3127": J3127, + "1662": J1662, + "4034": J4034, + "5129": J5129, + "559": J559, + "128": J128, + "2251": J2251, + "4316": J4316, + "3121": J3121, + "199": J199, + "4670": J4670, + "3614": J3614, + "592": J592, + "3707": J3707, + "2874": J2874, + "1569": J1569, + "3137": J3137, + "2391": J2391, + "3955": J3955, + "1357": J1357, + "3956": J3956, + "3939": J3939, + "656": J656, + "3683": J3683, + "4217": J4217, + "5763": J5763, + "3464": J3464, + "4513": J4513, + "2775": J2775, + "4009": J4009, + "6697": J6697, + "6969": J6969, + "7699": J7699, + "7096": J7096, + "7050": J7050, + "3827": J3827, + "7593": J7593, + "8179": J8179, + "7801": J7801, + "7046": J7046, + "7854": J7854, + + "587": J587, + }) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c3fb4db --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "github.com/banbox/banbot/entry" + "github.com/banbox/banbot/opt" + _ "github.com/banbox/banstagy/adv" + _ "github.com/banbox/banstagy/arb" + _ "github.com/banbox/banstagy/code" + _ "github.com/banbox/banstagy/demo" + _ "github.com/banbox/banstagy/demo/freqtrade" + "github.com/banbox/banstagy/ent" + _ "github.com/banbox/banstagy/fac" + _ "github.com/banbox/banstagy/filters" + _ "github.com/banbox/banstagy/flux/grid" + _ "github.com/banbox/banstagy/llmjob" + _ "github.com/banbox/banstagy/old" + _ "github.com/banbox/banstagy/org/day" + _ "github.com/banbox/banstagy/org/digo" + _ "github.com/banbox/banstagy/org/fmz" + _ "github.com/banbox/banstagy/org/ft" + _ "github.com/banbox/banstagy/org/jesse" + _ "github.com/banbox/banstagy/org/llm" + _ "github.com/banbox/banstagy/org/sf" + _ "github.com/banbox/banstagy/ptn/all" + _ "github.com/banbox/banstagy/rpc_fea" + _ "github.com/banbox/banstagy/surge" + _ "github.com/banbox/banstagy/tmp" + _ "github.com/banbox/banstagy/trend" + _ "github.com/banbox/banstagy/trend/20250208" + "os" +) + +func main() { + RegPairPickers() + args := os.Args[1:] + if len(args) >= 2 && args[0] == "my" { + err := ent.RunMyEntry(args[1:]) + if err != nil { + panic(err) + } + return + } + entry.RunCmd() +} + +func RegPairPickers() { + opt.PairPickers["profit"] = func(r *opt.BTResult) []string { + var pairs = make([]string, 0, len(r.PairGrps)) + for _, p := range r.PairGrps { + if p.ProfitSum > 0 { + pairs = append(pairs, p.Title) + } + } + return pairs + } + opt.PairPickers["sharpe1"] = func(r *opt.BTResult) []string { + var pairs = make([]string, 0, len(r.PairGrps)) + for _, p := range r.PairGrps { + if p.Sharpe > 1 { + pairs = append(pairs, p.Title) + } + } + return pairs + } +} diff --git a/old/CorrMeanRev.go b/old/CorrMeanRev.go new file mode 100644 index 0000000..14cc51f --- /dev/null +++ b/old/CorrMeanRev.go @@ -0,0 +1,117 @@ +package old + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "gonum.org/v1/gonum/floats" + "strconv" +) + +type CoMeSta struct { + corr float64 + atrBs float64 +} + +func CorrMeanRev(pol *config.RunPolicyConfig) *strat.TradeStrat { + corrHisLen := 70 + avgCorr := float64(0) + rsiUpp := 70.0 + rsiLow := 30.0 + overBuy, overSell := 0.0, 0.0 + avgMaOff := 0.0 + lenAtr := int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + BatchInOut: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &CoMeSta{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*CoMeSta) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + m.atrBs = atrBase + //ma := ta.SMA(e.Close, 10).Get(0) + //maOff := (e.Close.Get(0) - ma) / m.atrBs + rsi := ta.RSI(e.Close, 14).Get(0) + if m.corr >= 0.9 && avgCorr > 0.7 && s.OrderNum == 0 { + // 全品种趋同70%以上,当前品种趋同90% + dirt := 0 + if rsi > 70 { + dirt = 1 + } else if rsi < 30 { + dirt = -1 + } + //if avgMaOff > 0.2 && maOff > 0 { + // dirt = 1 + //} else if avgMaOff < -0.2 && maOff < 0 { + // dirt = -1 + //} + if dirt != 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(dirt), + Short: dirt < 0, + StopLossVal: atr.Get(0) * 2, + TakeProfitVal: atr.Get(0) * 5, + }) + } + } else if overBuy > 0.7 && rsi > 85 { + s.CloseOrders(&strat.ExitReq{Tag: "closeL", Dirt: core.OdDirtLong}) + } else if overSell > 0.7 && rsi < 15 { + s.CloseOrders(&strat.ExitReq{Tag: "closeS", Dirt: core.OdDirtShort}) + } + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + envs := make([]*ta.BarEnv, 0, len(jobs)) + dataLen, barNum := 0, 0 + for _, j := range jobs { + dataLen = j.Env.Close.Len() + barNum = j.Env.BarNum + break + } + if dataLen < corrHisLen+3 { + return + } + batchNumFlt := float64(len(jobs)) + overBuy, overSell = 0.0, 0.0 + avgMaOff = 0 + for _, j := range jobs { + e := j.Env + m, _ := j.More.(*CoMeSta) + envs = append(envs, e) + rsi := ta.RSI(e.Close, 14).Get(0) + if rsi > rsiUpp { + overBuy += 1 + } else if rsi < rsiLow { + overSell += 1 + } + ma := ta.SMA(e.Close, 10).Get(0) + avgMaOff += (e.Close.Get(0) - ma) / m.atrBs + } + overBuy /= batchNumFlt + overSell /= batchNumFlt + avgMaOff /= batchNumFlt + if barNum%10 > 0 { + // 每10个bar计算一次相关性 + return + } + _, corrs, err := utils.CalcEnvsCorr(envs, corrHisLen) + if err != nil { + log.Warn("CalcEnvsCorr fail", zap.String("e", err.Error())) + return + } + avgCorr = floats.Sum(corrs) / float64(len(corrs)) + for i, j := range jobs { + m, _ := j.More.(*CoMeSta) + m.corr = corrs[i] + } + }, + } +} diff --git a/old/boll_back_com.go b/old/boll_back_com.go new file mode 100644 index 0000000..846e0be --- /dev/null +++ b/old/boll_back_com.go @@ -0,0 +1,44 @@ +package old + +import ( + ta "github.com/banbox/banta" + "math" + "slices" +) + +type MoreType struct { + lastCalc int + BollSlops []float64 + MaSlops []float64 + BollUpSlop float64 + BollDownSlop float64 + MaUpSlop float64 + MaDownSlop float64 +} + +func (m *MoreType) UpdateSlops(e *ta.BarEnv, bollSlop, maSlop float64, sampleNum int) { + m.BollSlops = append(m.BollSlops, math.Abs(bollSlop)) + m.MaSlops = append(m.MaSlops, math.Abs(maSlop)) + if len(m.BollSlops) < sampleNum || e.BarNum-m.lastCalc < sampleNum/2 { + return + } + if len(m.BollSlops) > sampleNum { + m.BollSlops = m.BollSlops[len(m.BollSlops)-sampleNum:] + m.MaSlops = m.MaSlops[len(m.MaSlops)-sampleNum:] + } + thres := CutScore(m.BollSlops, 0.25) + m.BollUpSlop = thres + m.BollDownSlop = -thres + thres = CutScore(m.MaSlops, 0.25) + m.MaUpSlop = thres + m.MaDownSlop = -thres + m.lastCalc = e.BarNum +} + +func CutScore(vals []float64, rate float64) float64 { + arr := make([]float64, len(vals)) + copy(arr, vals) + slices.Sort(arr) + idx := int(float64(len(vals)) * rate) + return arr[idx] +} diff --git a/old/boll_back_v1.go b/old/boll_back_v1.go new file mode 100644 index 0000000..c9ba317 --- /dev/null +++ b/old/boll_back_v1.go @@ -0,0 +1,78 @@ +package old + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +const ( + v1SlopLen = 2 + v1BollLen = 26 + v1MaLen = 50 + v1AtrLen = 14 + v1SampleNum = 300 +) + +func BollBackV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 1000, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreType{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + + _, bollMid, _ := ta.BBANDS(e.Close, v1BollLen, 2, 2) + ma := ta.SMA(e.Close, v1MaLen) + + atr := ta.ATR(e.High, e.Low, e.Close, v1AtrLen) + //baseAtrLen := int(math.Round(float64(v1AtrLen) * 4.3)) + //atrBase := ta.Lowest(ta.Highest(atr, v1AtrLen), baseAtrLen).Get(0) + + bollSub := bollMid.Get(0) - bollMid.Get(v1SlopLen) + maSub := ma.Get(0) - ma.Get(v1SlopLen) + if math.IsNaN(maSub) || math.IsNaN(bollSub) { + return + } + + bollSlop := bollSub / bollMid.Get(0) + maSlop := maSub / ma.Get(0) + more, _ := s.More.(*MoreType) + + if e.BarNum%v1SlopLen == 0 { + more.UpdateSlops(e, bollSlop, maSlop, v1SampleNum) + } + + if s.OrderNum > 0 || bollSlop*maSlop < 0 || more.BollUpSlop == 0 { + // 已开单,或者布林中轨和均线方向不一致,或者上下限尚未确定,跳过 + return + } + if bollSlop >= more.BollDownSlop && bollSlop <= more.BollUpSlop || maSlop >= more.MaDownSlop && maSlop <= more.MaUpSlop { + // 布林中轨或者均线未突破斜率阈值 + return + } + + dirt := 1 + if bollSlop < 0 { + dirt *= -1 + } + atrVal := atr.Get(0) //*0.5 + atrBase*0.5 + stopLoss := e.Close.Get(0) - atrVal*float64(dirt) + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + StopLoss: stopLoss, + Short: dirt < 0, + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } + }, + } +} diff --git a/old/boll_back_v2.go b/old/boll_back_v2.go new file mode 100644 index 0000000..22b5d2d --- /dev/null +++ b/old/boll_back_v2.go @@ -0,0 +1,75 @@ +package old + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +const ( + v2SlopLen = 5 + v2BollLen = 26 + v2AtrLen = 14 + v2SampleNum = 300 + v2RsiLen = 15 +) + +func BollBackV2(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 300, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreType{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + + rsi := ta.RSI(e.Close, v2RsiLen).Get(0) - 50 + + _, bollMid, _ := ta.BBANDS(e.Close, v2BollLen, 2, 2) + + atr := ta.ATR(e.High, e.Low, e.Close, v2AtrLen) + //baseAtrLen := int(math.Round(float64(v2AtrLen) * 4.3)) + //atrBase := ta.Lowest(ta.Highest(atr, v2AtrLen), baseAtrLen).Get(0) + + bollSlop := bollMid.Get(0)/bollMid.Get(v2SlopLen) - 1 + if math.IsNaN(bollSlop) { + return + } + more, _ := s.More.(*MoreType) + + if e.BarNum%v2SlopLen == 0 { + more.UpdateSlops(e, bollSlop, 0, v2SampleNum) + } + + if s.OrderNum > 0 || bollSlop*rsi > 0 || more.BollUpSlop == 0 || math.Abs(rsi) <= 20 { + // 已开单,或者布林中轨和均线方向不一致,或者上下限尚未确定,跳过 + return + } + if bollSlop >= more.BollDownSlop && bollSlop <= more.BollUpSlop { + // 布林中轨或者均线未突破斜率阈值 + return + } + + dirt := 1 + if rsi < 0 { + dirt = -1 + } + atrVal := atr.Get(0) * 3 //*0.5 + atrBase*0.5 + stopLoss := e.Close.Get(0) - atrVal*float64(dirt) + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + StopLoss: stopLoss, + Short: dirt < 0, + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } + }, + } +} diff --git a/old/breakout_v1.go b/old/breakout_v1.go new file mode 100644 index 0000000..3c8b227 --- /dev/null +++ b/old/breakout_v1.go @@ -0,0 +1,75 @@ +package old + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" +) + +/* +适合4h,收益很不稳定 +*/ + +func BreakOutV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + periodBig := 200 + periodSml := 30 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: periodBig, + MinTfScore: 0.75, + //DrawDownExit: true, + OnBar: func(s *strat.StratJob) { + e := s.Env + hhBig := ta.Highest(e.Close, periodBig).Get(1) + //llSml := ta.Lowest(e.Close, periodSml).Get(1) + llBig := ta.Lowest(e.Close, periodBig).Get(1) + //hhSml := ta.Highest(e.Close, periodSml).Get(1) + maSml := ta.SMA(e.Close, periodSml) + maBig := ta.SMA(e.Close, periodBig) + maX := maSml.Cross(maBig) + c := e.Close.Get(0) + openTag := "" + closeTag := "" + if len(s.LongOrders) == 0 && c >= hhBig { + openTag = "long" + } else if len(s.ShortOrders) == 0 && c <= llBig { + openTag = "short" + } + if maX == 1 { + closeTag = "xshort" + } else if maX == -1 { + closeTag = "xlong" + } + //if len(s.LongOrders) > 0 && c <= llSml { + // closeTag = "xlong" + //} else if len(s.ShortOrders) > 0 && c >= hhSml { + // closeTag = "xshort" + //} + if openTag != "" { + err := s.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: openTag == "short", + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } + } + if closeTag != "" { + dirt := core.OdDirtLong + if closeTag == "xshort" { + dirt = core.OdDirtShort + } + err := s.CloseOrders(&strat.ExitReq{ + Tag: closeTag, + Dirt: dirt, + }) + if err != nil { + log.Error("close fail", zap.Error(err)) + } + } + }, + } +} diff --git a/old/common.go b/old/common.go new file mode 100644 index 0000000..0d1b99f --- /dev/null +++ b/old/common.go @@ -0,0 +1,26 @@ +package old + +import ( + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/old/rev" + "github.com/banbox/banstagy/old/rsi_ptn_rev" + "github.com/banbox/banstagy/old/s01" + "github.com/banbox/banstagy/old/t01" +) + +func init() { + strat.AddStratGroup("old", map[string]strat.FuncMakeStrat{ + "boll_back0": BollBackV1, + "boll_back": BollBackV2, + "breakout": BreakOutV1, + "ped": Ped, + "corrMean": CorrMeanRev, + "macd": MACD, + "rev": rev.S01V1, + "rsi_ptn_rev": rsi_ptn_rev.MainV1, + "s01v1": s01.MainV1, + "s01v2": s01.MainV2, // 建议版本 + "xem4o": xem40, + "t01": t01.Main4HV1, + }) +} diff --git a/old/eat.go b/old/eat.go new file mode 100644 index 0000000..815ef41 --- /dev/null +++ b/old/eat.go @@ -0,0 +1,165 @@ +package old + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" + "sort" +) + +var ( + lenAtr = 9 + rangeRate = 0.6 // 预计bar利润区间比率:60% + ShakeTop = 0.2 // 只对分数前20%的标的进行震荡交易 + minProfitPct = 0.01 // 最小目标盈利百分比 +) + +type TMore struct { + symbol string + shakeScore float64 // 震荡分数:(1-平均实体比率)*平均bar波动比率 + btmPrice float64 // 平均向下突破比率 + topPrice float64 // 平均向上突破比率 + shakeOK bool // 是否允许震荡中开单 + shakeHit float64 // 震荡首先触发位置:-1触发顶部,1触发底部 +} + +func Eat(pol *config.RunPolicyConfig) *strat.TradeStrat { + baseAtrLen := int(math.Round(float64(lenAtr) * 5)) + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(30, baseAtrLen), + MinTfScore: 0.75, + BatchInfo: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &TMore{ + symbol: s.Symbol.Symbol, + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {Pair: "_cur_", TimeFrame: "8h", WarmupNum: 30}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + h, l, c := e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + m, _ := s.More.(*TMore) + if m.shakeOK { + // 允许震荡开单 + if m.shakeHit == 0 { + if h >= m.topPrice { + m.shakeHit = -1 + } else if l <= m.btmPrice { + m.shakeHit = 1 + } + } + if s.OrderNum == 0 && m.shakeHit != 0 { + slRange := (m.topPrice - m.btmPrice) + tag := "long" + trigPrice := m.btmPrice + takeProfit := m.topPrice + if m.shakeHit < 0 { + tag = "short" + trigPrice = m.topPrice + takeProfit = m.btmPrice + } + stopLoss := trigPrice - slRange*m.shakeHit + slDst := (c - stopLoss) * m.shakeHit / slRange + tpDst := (takeProfit - c) * m.shakeHit / slRange + if slDst > 0.5 && tpDst > 0.7 { + // 到止损的距离不低于50% + err := s.OpenOrder(&strat.EnterReq{ + Tag: tag, + Short: m.shakeHit < 0, + StopLoss: stopLoss, + TakeProfit: takeProfit, + }) + //date := btime.ToDateStr(btime.TimeMS(), "") + //log.Info(fmt.Sprintf("%s %s open %s c:%.3f sl:%.3f, tp:%.3f", s.Symbols.Symbols, + // date, tag, c, stopLoss, takeProfit)) + if err != nil { + log.Error("open fail", zap.Error(err)) + } + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // ma := ta.EMA(e.Close, 5) + rsi := ta.RSI(e.Close, 9) + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + atrRate := atr.Get(0) / atrBase + + // 计算平均实体比率 + o, h, l, c := e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + ph, pl, pc := e.High.Get(1), e.Low.Get(1), e.Close.Get(1) + solidTop, solidBtm := max(o, c), min(o, c) + tRange, pRange := h-l, ph-pl + //tr := e.GetSeries("tRange") + //tr.Set(math.Sqrt(tRange * 100 / c)) + //maTRate := ta.EMA(tr, 3) + solRate := e.Close.To("solRate", 0) + solRate.Set(math.Abs(o-c) / tRange) + maSRT := ta.EMA(solRate, 3).Get(0) + + // 计算上下行比率 + dnRate := e.Close.To("dnRate", 0) + dnRate.Set((l - pc) / pRange) + maDnRate := ta.EMA(dnRate, 5) + upRate := e.Close.To("upRate", 0) + upRate.Set((h - pc) / pRange) + maUpRate := ta.EMA(upRate, 5) + + m, _ := s.More.(*TMore) + m.shakeScore = (1 - maSRT) // * maTRate.Get(0) + top := c + maUpRate.Get(0)*tRange + btm := c + maDnRate.Get(0)*tRange + edgeOff := (top - btm) * (1 - rangeRate) * 0.5 + m.btmPrice = btm - edgeOff + m.topPrice = top + edgeOff + //m.maChg = (ma.Get(0) - ma.Get(1)) / pRange + topLineRate := (h - solidTop) / tRange + btmLineRate := (solidBtm - l) / tRange + lineDiff := math.Abs(topLineRate-btmLineRate) / max(topLineRate, btmLineRate) + rsiVal, pRsi := rsi.Get(0)-50, rsi.Get(1)-50 + m.shakeOK = (top-btm)*rangeRate/c >= minProfitPct && lineDiff < 0.35 && maSRT < 0.3 + m.shakeHit = 0 + //log.Info(fmt.Sprintf("h: %.3f l:%.3f c:%.3f maSRT: %.3f upRate:%.3f dnRate:%.3f %v", h, l, c, + // maSRT.Get(0), maUpRate.Get(0), maDnRate.Get(0), m.shakeOK)) + if math.Abs(rsiVal) > 20 || math.Abs(pRsi) > 20 { + // (ATR较大/反向影线占比大)和RSI都极端时,容易反转,禁止震荡开单 + revLineRate := btmLineRate + if rsiVal > 0 { + revLineRate = topLineRate + } + if atrRate > 2.5 || atrRate > 1.3 && revLineRate > 0.75 { + m.shakeOK = false + } + } + }, + OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) { + // 筛选出所有币中shakeScore降序的前n个,然后在子周期中低买高卖 + var mores = make([]*TMore, 0, len(jobs)) + for _, job := range jobs { + m, _ := job.Job.More.(*TMore) + mores = append(mores, m) + } + var shakeTopN = int(math.Ceil(float64(len(jobs)) * ShakeTop)) + sort.Slice(mores, func(i, j int) bool { + return mores[i].shakeScore > mores[j].shakeScore + }) + for _, m := range mores { + if shakeTopN > 0 && m.shakeOK { + shakeTopN -= 1 + } else { + m.shakeOK = false + } + } + }, + } +} diff --git a/old/macd_v1.go b/old/macd_v1.go new file mode 100644 index 0000000..c28de5d --- /dev/null +++ b/old/macd_v1.go @@ -0,0 +1,127 @@ +package old + +import ( + "fmt" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "go.uber.org/zap" +) + +func MACD(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 300, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: onStartUp, + OnBar: onBar, + } +} + +type MoreMACD struct { + *com.ExmTrace + max1 *com.ExmItem + max2 *com.ExmItem + slope float64 // 斜率 + delta float64 // 直线偏移值 +} + +/* +val: 当前指标值 +pNum: 前一个索引 +ptime:前一个时间戳 +*/ +func (m *MoreMACD) update(val float64, pNum int, ptime int64) bool { + if m.ExmTrace.Update(val, pNum, ptime) { + var exm *com.ExmItem + last := m.Hist[len(m.Hist)-1] + if last.Valid && m.ExmTrace.Expect < 0 { + exm = last + } else if len(m.Hist) > 1 && m.ExmTrace.Expect > 0 { + exm = m.Hist[len(m.Hist)-2] + } else { + return false + } + // 更新最高点和次高点 + if m.max1 == nil || exm.Value >= m.max1.Value { + m.max1 = exm + m.max2 = nil + } else if m.max1 != nil && pNum-exm.BarNum > 500 { + // 最高点超过500个bar过期 + if m.max2 != nil && m.max2.Value > exm.Value { + exm = m.max2 + } + m.max1 = m.max2 + m.max2 = nil + } else if m.max2 == nil || exm.Value > m.max2.Value { + m.max2 = exm + return true + } + } + return false +} + +func onStartUp(s *strat.StratJob) { + s.More = &MoreMACD{ + ExmTrace: &com.ExmTrace{}, + } +} + +func onBar(s *strat.StratJob) { + e := s.Env + macd, _ := ta.MACD(e.Close, 12, 26, 9) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + + m, _ := s.More.(*MoreMACD) + + c := e.Close.Get(0) + + if m.slope > 0 && m.max1 != nil && m.max2 != nil { + point := m.slope*float64(e.BarNum) + m.delta + tag := "" + stopLoss := float64(0) + if c-atr*2 > point && len(s.LongOrders) == 0 { + // 突破压力线,做多 + tag = "long" + stopLoss = point - atr + } else if c+atr*0.5 > point && c <= point+atr && len(s.ShortOrders) == 0 { + // 触发压力线,开空 + tag = "short" + stopLoss = max(c, point) + atr*2 + } + if tag != "" && !s.IsWarmUp { + price1 := e.Close.Get(e.BarNum - m.max1.BarNum) + price2 := e.Close.Get(e.BarNum - m.max2.BarNum) + date := btime.ToDateStr(e.TimeStop, "") + date1 := btime.ToDateStr(m.max1.TimeMS, "") + date2 := btime.ToDateStr(m.max2.TimeMS, "") + log.Info(fmt.Sprintf("%s open %s, %s %s %.5f - %s %.5f", date, tag, s.Symbol.Symbol, + date1, price1, date2, price2)) + err := s.OpenOrder(&strat.EnterReq{ + Tag: tag, + StopLoss: stopLoss, + Short: tag == "short", + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } + } + + } + + if m.update(macd.Get(0), e.BarNum-1, e.TimeStart-e.TFMSecs) { + price1 := e.Close.Get(e.BarNum - m.max1.BarNum) + price2 := e.Close.Get(e.BarNum - m.max2.BarNum) + // 更新斜率和偏移值 + m.slope = (price2 - price1) / float64(m.max2.BarNum-m.max1.BarNum) + m.delta = price1 - m.slope*float64(m.max1.BarNum) + //date1 := btime.ToDateStr(m.max1.TimeMS, "") + //date2 := btime.ToDateStr(m.max2.TimeMS, "") + //log.Info(fmt.Sprintf("update pt2, %s %s %.5f - %s %.5f", s.Symbols.Symbols, + // date1, price1, date2, price2)) + } +} diff --git a/old/ped.go b/old/ped.go new file mode 100644 index 0000000..28837fe --- /dev/null +++ b/old/ped.go @@ -0,0 +1,71 @@ +package old + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type pedSta struct { + bigDirt int + midRsi float64 +} + +func Ped(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "15m", 100}, + {"_cur_", "1h", 205}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &pedSta{} + }, + OnBar: func(s *strat.StratJob) { + m, _ := s.More.(*pedSta) + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + maBig := ta.SMA(e.Close, 10).Get(0) + rsi := ta.RSI(e.Close, 14).Get(0) + if s.OrderNum > 0 { + return + } + if m.bigDirt > 0 && m.midRsi < 70 && c > maBig && rsi > 70 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: atrBase, + TakeProfitVal: atrBase * 7, + }) + } else if m.bigDirt < 0 && m.midRsi > 30 && c < maBig && rsi < 30 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + StopLossVal: atrBase, + TakeProfitVal: atrBase * 7, + }) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*pedSta) + if tf == "1h" { + maSml := ta.SMA(e.Close, 20).Get(0) + maBig := ta.SMA(e.Close, 100).Get(0) + if maSml > maBig { + m.bigDirt = 1 + } else { + m.bigDirt = -1 + } + } else { + // 15m + m.midRsi = ta.RSI(e.Close, 14).Get(0) + } + }, + } +} diff --git a/old/rev/01.go b/old/rev/01.go new file mode 100644 index 0000000..a342134 --- /dev/null +++ b/old/rev/01.go @@ -0,0 +1,119 @@ +package rev + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +type MoreRev struct { + thres float64 // 阈值 + dirt float64 // 1rsi高于0,-1rsi低于0 + hitAt int // 进入极值的BarNum + exmVal float64 // 最大极值 + bigRsi float64 // 大周期rsi +} + +func (m *MoreRev) update(rsi50 float64, barNum int) { + rsiAbs := math.Abs(rsi50) + if rsiAbs < m.thres { + m.dirt = 0 + m.hitAt = 0 + m.exmVal = 0 + return + } + if m.dirt == 0 { + if rsi50 > 0 { + m.dirt = 1 + } else { + m.dirt = -1 + } + m.hitAt = barNum + m.exmVal = rsiAbs + } else if m.exmVal < rsiAbs { + m.exmVal = rsiAbs + } +} + +func S01V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + var lenRsi = 22 + var lenRsi2 = 140 + var thresRsi = 20.0 + var hitRsi = 22.0 + var minKeep = 3 + + lenAtr := 15 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: lenRsi * 2, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreRev{thres: thresRsi} + }, + OnPairInfos: com.MakePairInfos(lenRsi2), + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*MoreRev) + rsi := ta.RSI(e.Close, lenRsi) + rsiMa := ta.SMA(rsi, 3) + rsiVal := rsi.Get(0) + rsiX := rsi.Cross(rsiMa) + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + atrRate := math.Sqrt(atr.Get(0) / atrBase) + + c := e.Close.Get(0) + bigRsiFit := math.Abs(m.bigRsi) > 5 && m.bigRsi*m.dirt > 0 + rsiFit := m.dirt != 0 && m.exmVal >= hitRsi && e.BarNum-m.hitAt >= minKeep + if s.OrderNum == 0 && bigRsiFit && rsiFit && (rsiX == 1 || rsiX == -1) { + // rsi处于极值范围,且触发最大极值,且满足最少极值数量,且和rsi均值交叉 + slVal := atrBase * atrRate * 5 + slLong, slShort := c-slVal, c+slVal + //entPrice := c - slVal*0.3 + //if m.dirt > 0 { + // entPrice = c - slVal*0.3 + //} + com.FastOpen(s, -m.dirt, slLong, slShort, 0) + } else if rsiFit && s.EnteredNum > 0 && m.bigRsi*m.dirt > 0 { + if m.dirt == 1 && len(s.LongOrders) > 0 && m.bigRsi > 2 { + // 平多 + _ = s.CloseOrders(&strat.ExitReq{ + Tag: "exitL", + Dirt: core.OdDirtLong, + }) + } else if m.dirt == -1 && len(s.ShortOrders) > 0 && m.bigRsi < -2 { + _ = s.CloseOrders(&strat.ExitReq{ + Tag: "exitS", + Dirt: core.OdDirtShort, + }) + } + } + m.update(rsiVal-50, e.BarNum) + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*MoreRev) + m.bigRsi = ta.RSI50(e.Close, lenRsi2).Get(0) + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + sl := od.GetStopLoss() + if sl == nil || sl.Range == 0 { + return 0 + } + bestRate := maxChg * od.InitPrice / sl.Range + if bestRate > 2 { + return 0.3 + } else if bestRate > 1 { + return 0.5 + } + return 0 + }, + } +} diff --git a/old/rsi_ptn_rev/v1.go b/old/rsi_ptn_rev/v1.go new file mode 100644 index 0000000..f03b004 --- /dev/null +++ b/old/rsi_ptn_rev/v1.go @@ -0,0 +1,57 @@ +package rsi_ptn_rev + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "github.com/banbox/banstagy/ptn" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +const ( + v1RsiLen = 9 + v1RsiThres = 25 + v1AtrLen = 14 +) + +func MainV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 30, + MinTfScore: 0.75, + DrawDownExit: true, + OnBar: func(s *strat.StratJob) { + e := s.Env + + rsi := ta.RSI(e.Close, v1RsiLen).Get(0) - 50 + atr := ta.ATR(e.High, e.Low, e.Close, v1AtrLen) + baseAtrLen := int(math.Round(float64(v1AtrLen) * 4.3)) + atrBase := ta.Lowest(ta.Highest(atr, v1AtrLen), baseAtrLen).Get(0) + atrVal := atr.Get(0) + + if math.IsNaN(atrBase) || math.Abs(rsi) <= v1RsiThres || s.OrderNum > 0 || atrVal < atrBase { + return + } + ptn := ptn.Recognize(e) + if ptn == nil { + // 未识别到蜡烛形态,或者形态与RSI相反,忽略 + return + } + ptnDirt := float64(ptn.Dirt) + if rsi*ptnDirt > 0 { + return + } + stopLoss := e.Close.Get(0) - atrVal*ptnDirt + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + StopLoss: stopLoss, + Short: ptnDirt < 0, + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } + }, + } +} diff --git a/old/s01/main.go b/old/s01/main.go new file mode 100644 index 0000000..1350852 --- /dev/null +++ b/old/s01/main.go @@ -0,0 +1,163 @@ +package s01 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +/* +此策略对应python原始策略名为PivotRev + +【s01_base】 +是最初版本的策略,所有标的使用固定的RSI差阈值:300周期9, 50周期20 + +【s01_adp.50】~【s01_adp.100】 +增加了标的自适应的前20个高于固定阈值50%~100%的所有RSI值的均值,作为实际阈值。 +开单数量倍增,曲线稍微平滑一些,收益更高。 +测试最佳值是54% + + +【s01_adpb.55】~【s01_adp.70】 +将bigTmp和smlTmp同时满足时才记录,前面是各自达到阈值就记录。 +改了后订单减少40%左右,收益减少30%左右。 +不采用 + +后续考虑记录RSI300和RSI50的历史峰值,然后峰值排序,取Top50%的均值作为阈值。 +*/ + +var ( + RsiBig = 300 + RsiSml = 50 + ThsBig = float64(9) + ThsSml = float64(20) + AtrLen = 14 + + bigMin = float64(ThsBig) * 0.55 + smlMin = float64(ThsSml) * 0.55 + + AvgLastNum = 20 +) + +type MoreType struct { + hit bool + bigThsHis []float64 + smlThsHis []float64 + bigThs float64 + smlThs float64 + bigTmp float64 + smlTmp float64 +} + +func (m *MoreType) close() { + if m.bigTmp > bigMin { + m.bigThsHis = append(m.bigThsHis, m.bigTmp) + if len(m.bigThsHis) > AvgLastNum { + m.bigThsHis = m.bigThsHis[len(m.bigThsHis)-AvgLastNum:] + } + sumVal := float64(0) + for _, val := range m.bigThsHis { + sumVal += val + } + m.bigThs = sumVal / float64(len(m.bigThsHis)) + } + if m.smlTmp > smlMin { + m.smlThsHis = append(m.smlThsHis, m.smlTmp) + if len(m.smlThsHis) > AvgLastNum { + m.smlThsHis = m.smlThsHis[len(m.smlThsHis)-AvgLastNum:] + } + sumSml := float64(0) + for _, val := range m.smlThsHis { + sumSml += val + } + m.smlThs = sumSml / float64(len(m.smlThsHis)) + } + m.bigTmp = -1 + m.smlTmp = -1 + m.hit = false +} + +func (m *MoreType) check(big, sml float64) { + if m.bigTmp < 0 { + m.bigTmp = math.Abs(big) + } else { + m.bigTmp = max(m.bigTmp, math.Abs(big)) + } + if m.smlTmp < 0 { + m.smlTmp = math.Abs(sml) + } else { + m.smlTmp = max(m.smlTmp, math.Abs(sml)) + } +} + +func MainV0(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: RsiBig * 10, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreType{ + bigThs: ThsBig, + smlThs: ThsSml, + bigTmp: -1, + smlTmp: -1, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.EMA(e.Close, 5) + ma20 := ta.EMA(e.Close, 20) + maX := ma5.Cross(ma20) + + rsiB := ta.RSI(e.Close, RsiBig).Get(0) - 50 + rsiS := ta.RSI(e.Close, RsiSml).Get(0) - 50 + + atr := ta.ATR(e.High, e.Low, e.Close, AtrLen) + baseAtrLen := int(math.Round(float64(AtrLen) * 4.3)) + atrB := ta.Lowest(ta.Highest(atr, AtrLen), baseAtrLen).Get(0) + + if math.IsNaN(rsiB) || math.IsInf(rsiB, 0) { + // 过滤无效值 + return + } + + m, _ := s.More.(*MoreType) + + if math.Abs(float64(maX)) == 1 { + // 均线交叉,顶底切换,允许开单 + m.close() + } + m.check(rsiB, rsiS) + if m.hit { + // 已开单,禁止重复开单 + return + } + + if math.Abs(rsiB) <= m.bigThs || math.Abs(rsiS) <= m.smlThs { + // 未触发阈值,不开单 + return + } + + m.hit = true // 禁止同方向再次开单 + isShort := rsiB > 0 + odDirt := float64(1) + if isShort { + odDirt = -1 + } + stopLoss := e.Close.Get(0) - max(atrB*3, atr.Get(0)*1.5)*odDirt + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + Short: isShort, + StopLoss: stopLoss, + }) + if err != nil { + log.Error("open order fail", zap.String("pair", s.Symbol.Symbol), + zap.String("stagy", s.Strat.Name), zap.Error(err)) + } + }, + } +} diff --git a/old/s01/v1.go b/old/s01/v1.go new file mode 100644 index 0000000..e450ef4 --- /dev/null +++ b/old/s01/v1.go @@ -0,0 +1,71 @@ +package s01 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +/* +此策略对应python原始策略名为PivotRev +*/ + +func MainV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + RsiBig = 60 + RsiSml = 14 + ThsBig = 20 + ThsSml = 30 + AtrLen = 14 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: RsiBig, + MinTfScore: 0.75, + DrawDownExit: true, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.EMA(e.Close, 5) + ma20 := ta.EMA(e.Close, 20) + maX := ma5.Cross(ma20) + + rsiB := ta.RSI(e.Close, RsiBig).Get(0) - 50 + rsiS := ta.RSI(e.Close, RsiSml).Get(0) - 50 + + atr := ta.ATR(e.High, e.Low, e.Close, AtrLen) + basev1AtrLen := int(math.Round(float64(AtrLen) * 4.3)) + atrB := ta.Lowest(ta.Highest(atr, AtrLen), basev1AtrLen).Get(0) + + if math.Abs(float64(maX)) == 1 { + // 均线交叉,顶底切换,允许开单 + s.More = nil + } else if s.More != nil { + // 禁止开单 + return + } + + if math.Abs(rsiB) <= ThsBig || math.Abs(rsiS) <= ThsSml { + // 未触发阈值,不开单 + return + } + + s.More = true // 禁止同方向再次开单 + isShort := rsiB > 0 + odDirt := float64(1) + if isShort { + odDirt = -1 + } + stopLoss := e.Close.Get(0) - max(atrB*3, atr.Get(0)*1.5)*odDirt + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + Short: isShort, + StopLoss: stopLoss, + }) + if err != nil { + log.Error("open order fail", zap.String("pair", s.Symbol.Symbol), + zap.String("stagy", s.Strat.Name), zap.Error(err)) + } + }, + } +} diff --git a/old/s01/v2.go b/old/s01/v2.go new file mode 100644 index 0000000..1f69e3a --- /dev/null +++ b/old/s01/v2.go @@ -0,0 +1,120 @@ +package s01 + +import ( + "fmt" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +type S1More struct { + JumpRates map[int64]float64 + HitNum int // 记录当前是否已开过单。对于单边行情,为避免重复开单,只有当rsi转折时才开单 +} + +func MainV2(pol *config.RunPolicyConfig) *strat.TradeStrat { + RsiSml = 14 + ThsSml = 40 + AtrLen = 14 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: RsiBig, + MinTfScore: 0.1, + DrawDownExit: true, + BatchInOut: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &S1More{ + JumpRates: make(map[int64]float64), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + rsi := ta.RSI(e.Close, RsiSml) + rsiS := rsi.Get(0) - 50 + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + + m, _ := s.More.(*S1More) + + // 检查是否RSI多次触发,且首次转折 + openOK := m.HitNum > 1 && math.Abs(rsiS) < math.Abs(rsi.Get(1)-50) + if math.Abs(rsiS) < ThsSml && !openOK { + // rsi低于阈值,且不属于多次触发后转折 + m.HitNum = 0 + return + } + if rsiS > 0 && len(s.ShortOrders) > 0 || rsiS < 0 && len(s.LongOrders) > 0 { + // 已有入场订单,不重复开单 + return + } + m.HitNum += 1 + if m.HitNum > 1 && !openOK { + // 重复触发,只允许rsi调头时第二次入场 + return + } + c := e.Close.Get(0) + stopLoss := c - atr*2 + tag := "long" + if rsiS > 0 { + stopLoss = c + atr*2 + tag = "short" + } + err := s.OpenOrder(&strat.EnterReq{ + Tag: tag, + Short: rsiS > 0, + StopLoss: stopLoss, + }) + if err != nil { + log.Error("open order fail", zap.String("pair", s.Symbol.Symbol), + zap.String("stagy", s.Strat.Name), zap.Error(err)) + } + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + openNum := float64(0) + for _, j := range jobs { + if len(j.Entrys) > 0 { + openNum += 1 + } + } + openRate := openNum / float64(len(jobs)) + // 这里无需限制openRate的上限,前面已禁止单个趋势重复多次开单 + if openRate < 0.2 { + for _, j := range jobs { + j.Entrys = nil + } + return + } + log.Info(fmt.Sprintf("%s open orders, num: %v, total: %v, rate: %.2f", + btime.ToDateStr(btime.TimeMS(), ""), openNum, len(jobs), openRate)) + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + more, _ := s.More.(*S1More) + jumpRate, ok := more.JumpRates[od.ID] + if !ok { + e := s.Env + jumpRate = (e.Close.Get(RsiSml)/e.Close.Get(0) - 1) * 0.3 + more.JumpRates[od.ID] = jumpRate + } + var rate float64 + switch { + case maxChg > jumpRate*15: + rate = 0.15 + case maxChg > jumpRate*10: + rate = 0.17 + case maxChg > jumpRate*3: + rate = 0.25 + case maxChg > jumpRate*1: + rate = 0.37 + case maxChg > jumpRate*0.5: + rate = 0.5 + default: + rate = 0 + } + return rate + }, + } +} diff --git a/old/s01/v3.go b/old/s01/v3.go new file mode 100644 index 0000000..cfa2c48 --- /dev/null +++ b/old/s01/v3.go @@ -0,0 +1,64 @@ +package s01 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "slices" +) + +func MainV3(pol *config.RunPolicyConfig) *strat.TradeStrat { + RsiSml = 100 + watchBig := 5 + watchSml := 3 + threshold := float64(17) + return &strat.TradeStrat{ + Version: 1, + WarmupNum: RsiBig, + MinTfScore: 0.75, + //DrawDownExit: true, + OnBar: func(s *strat.StratJob) { + e := s.Env + rsi := ta.RSI(e.Close, RsiSml) + sml := ta.SMA(rsi, watchSml) + big := ta.SMA(rsi, watchBig) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + crx := sml.Cross(big) + if crx != 1 && crx != -1 { + return + } + rsiVal := float64(0) + if rsi.Get(watchBig) > 50 { + rsiVal = slices.Max(rsi.Range(0, watchBig*2)) + } else { + rsiVal = slices.Min(rsi.Range(0, watchBig*2)) + } + if math.Abs(rsiVal-50) < threshold { + return + } + c := e.Close.Get(0) + if crx == -1 { + _ = s.CloseOrders(&strat.ExitReq{ + Tag: "x_short", + Dirt: core.OdDirtShort, + }) + _ = s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLoss: c - atr*10, + }) + } else if crx == 1 { + _ = s.CloseOrders(&strat.ExitReq{ + Tag: "x_long", + Dirt: core.OdDirtLong, + }) + _ = s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + StopLoss: c + atr*10, + }) + } + }, + } +} diff --git a/old/t01/1h.go b/old/t01/1h.go new file mode 100644 index 0000000..3add544 --- /dev/null +++ b/old/t01/1h.go @@ -0,0 +1,23 @@ +package t01 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" +) + +func Main1HV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 最适合1h的参数,测试20240101-20240324,50币,无需大周期趋势 + maSml = 10 + openOffRate = -1.5 + badRate = 1.5 + goodRate = 1 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 300, + MinTfScore: 0.75, + //OnPairInfos: onPairInfos, + OnStartUp: onStartUp, + //OnInfoBar: onInfoBar, + OnBar: onBar, + } +} diff --git a/old/t01/20240322_24_1h.xlsx b/old/t01/20240322_24_1h.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..89bbbb5beea401458d825f53b33114fdb63278cc GIT binary patch literal 10323 zcmeHtg;yNe_I0Dd-QB%$4<6hlNFX>(aCfIefFQx$U4jLN;O-XOH3WwQ*I!R&-ZwLu z`Tm0Us@JN!dez-$Rn{I7lRRtJW8~{830RR9{0!)rGEDWFkfM{3%02_b+t@py- z&IM%WV))Y20pzUD>S1d`kqZmWkPU!_%>UoW1Lc>i#SnOo z!2MWEY9bvyaebwxdYP7%PnqxUFeGwtt@u7+jW2t1o{d^m+u7EGL;F6fql5{Hd7{`vSnE> z=+Bf?5#VD2`xOlgHu86<9GQ~%*cDDb+lH08e!{uL+-v^PA%QX2K}q8qaV##yzA5U~qLg7Jh?0(JY8Uagv3S3u23XdIOXS2gfu@5ha&`OhU5 z?xBi?3Md{Nn>9GB^*&A#y0t>r!&&hYHvL+t)Ia5@MMCof)wDAYemva*PC@(3ZwG=xIvkT>ID z!{%=9WNl(^Z~dEYm1*kRr?KPsX4O6g+$(pAA!Nrg!RAWs7nU9^CdnqiC|5xz6Mms7 zyF)LFywQaTC`D9b1(6a+$9=7(86~W=go~lYr;TC9Va_XoqkR>p z!8F`erTB$MzKf|=2BAlW{n}|zo#;m`(319w0*ogK{XqA|^-EsDp3J&D)vRrKQA-(= zNhcDWJ1N7|5O;Dp6YD@OU_no4q8^$R|8YL3Z(kccF{nThZ$o0{TN^l2Ju1)ZvOX|r0nJKJLZHHghA1za|Rdv?cCXj$~Fx3_np(Ax# zIF$0>CLEf)AGld;vT&Nsi=zeacPrE7l!w>JTjbPf=K;OmT6cF>`+?~QUQhaRIh_wI zZ9yc<(J})F+rrOr4pHh!me4hspzlv{XcKu-=COszqHI?_`6!V0+m+F~ZQo7KyKR+R zp_l{_=cm_liqhzbhYSRcWCr=g+LCvgPgEbi>x=We-YfN=JOA{CR)^HuJ;E*(U#~c) zzV(e}A0I{MnXqczLH}h3c%v|UwMOx)5lhwZ*#i2ImVjy>S-MwPT|x1?Z;XU0Au+;L z7nYonE^jP8H=ig43x3ca=4hRdco3Hv^yn>lE*_{iXRWz=OYB-|!e6SncEmX)nLUe9 zgW~?`&`?1x20W}er*!_w^2Q2&WoTI1O_(48O878&D`)R{bILr-${B@trm5a-&--#S zcEwE^XfI#Qrx53=AD$Ebx;Vq>2%gNc&B5qXJo?i4P%=kwP=@SgKNr;}RVum*4psF;la?&a46(jD4`@$!NdZ6%X1b4E9AQb8#3JtQWS;$#XdS z(I2BQ2qs76WMCOMY^=lmgO5kZXsOQFO(L-aESWg`U#}Tx2}AMVx#@Jms zt(+%Fhall~xOO|>Hb~>J99-MbK#|I5zVp=dV(Ah-3+zcg`GXKqP6*W#F!^nolKHw* z>Js65%e}dh=a_Y8-Yv=w$wvjUQXXNSU>Ayz0U2_C##rZ_+?3CnCmSvjcSDTrOW#?B z&ri&^2m5E%AS?aP=<2L&z&!s90Jx_E00B>dZ^{7_`|Q>$r+8UA-HJR5@H?*d%2e16Ns zO7-h+o$9lF*z;k_ToS53g4^yHMC8H#( z4W0M5cuB0xO4*inG>RJV)oclJr-Wj*K{R>9eXHjK;7~suF$ZZ zoshN_x#9y1O^xYz(w1n|GrH{*7l9{Df=mu{y&2N==T_%UiPxhkJYFFMSwSnA#w#b^ zZ=^Nh9{AeRl~0|*Qo@h&&71sjfeMYg6eVsh3dDBnd`A**D&iu<3Ua6L9O zxbQH|FPwc6xfv4qA}U;xjYmxC-?ZYc`jIY|K+Tut`#yUJ1xH>nSw6x8L>Hf)ey(r4 z?vmXWFV1~J$BCL`*feMZbwVexjanAw+)}UXt?k`a!Kgqre+bJ69NzpGi8@i-Cdwcg zo4=2r-{^_qNo9YQ|Kr2?kGoEiOjiHar^X@JXnqgwKPnZmZr|sB$pJ+D~P6XnR1dp0ncfJb0(Kv3|eLV#99xGaI-o3pKZ9-Z& z=Dj?RZU_~rnMFto9(uE$kMV87`&JmA-FJMGYLXAmxJ@Ft9dT<9UyYWfOP9a_3*LnQ z_#lRy+(v3SO|_BwJX3MHO+shf&^kR2Q)@`IJQ#(a5k}BM0UD2ewWeD3`m0C?Hpm#x z%Y%m~moqcs=bU&bdXU&%jw#4!d0>cYgGiLtAIaId3`1}=NY}uesw_xaM@4y^2B(-@ z*FdwwMHnb$P2=Ni*cMBf&J?etMu=`4lu(Ue4eF31RGo`z$qe|EDRRYwB?T%hKO4F% zEH|~yiTmiwfq$MY7poJBSDolyYTvYPuLRHb2XOaAL_rt?F4KQ8}p|j@Y;fnkIbJ!j4 zTU+h(owQ5J8g`yw@~|JV#y46DtW9k$y|H|2yEC>icx^?FhMhtzO>~F^$LR0n#379*3?eIX+%l(Llfd42OlLe z$J}hPDdZ;9IAm-|W7G~NS@h4U-_oOeDb5FfhbeDjzV+fpcj2)k46BI^6#Z!XRZcg5 z=&O(q9zV4X1CU^(8*E)x!>mJpn!Y|2tk;~&fyW2nWPn0(V*Y8?bl3Rd1E47+vcEY@E}0nawnbm)sZe8nF6&>{-A zr9gk69~l_Dd7s2I2d`^tHvJ&GDq7&r_7gkOQ+aHgx%gz20CV1`a3lc3HF(4-WL@MQ#CPHs10kgcYeVLq3u zY(4mF^r2q?eqqMLjA6_#w8NQ6fs9c<2857)CYw_$1NNMz){m44-fY(M-?0eBVU0gwpFdDYX=j=f&GkcfTj!df*At=anZ-2=F{d zo6uZo!GF!|qArVM#CdfxueI;k$JH#J;Vl1VC7*S}l)9)sm+k6lVO#WP^!<6V`Ot|5 zX2+29FIYgw$RWa#kVAR4+1X<#rlU*cl2&bX?~RP;6}>dpayYV0?G;Ij)=fj?rK9B# zK=stzWm>czZ>9-7*-k!O7Pf;YH+33ohe;UvTK1fo&FRem)z#=s_y?9OJva^ozvnNF zN@(sRMK8>DWwotarVXy#uwdMb&;63Xr>(pPquY2_2%FL)+!aGcE4f(-K@*ZKVbR2` z18eke2YMlz|Ich7x9SPq0CHuN;Wq-!AWX6a1ioPI9SMQSHBO#&OY9wM{tVn`HPIO`mj=bTmsys4~yB7U{Z6$VM zIOA;)*_A)O|)?p7RHh41wnBv5aIdnPlXy*hIrN)$hXT5%bA3UlRSD z`5Cx)hp`~H4j0w@~mRIkYrhKWM$7&z(xy{1GE z6%!;Q0{kE#2;a&9)RHgI<&dU1YlZF2zFahukeOOA#u$XWgE3`6cy!0c%tEysGsAV38pPb;-y_Q z&o=xmecUi?UkI)+eHK|69V(c;xK8_)Ct%3+b^B`Ng*CG;QGhhHj-^$2d5ysyJmD;$ zfSk11`@*2kW&`zMh9_+)=eF*YD1IM(_Vledev8Eh4?}N=^>x*k!*iPlqWDc~>uax) z73BRyKh!V=IkdTnf-0>eR_)JVMS>N_t~6uzVr|YH0iJmBrz3;mmodO*k_CGh2{byP%J_CI#jd4 z@xqw|j+}A8Cr*nC)PXITDCOfP52vT1ICN2=rYC*g1})IwQMQ@Y(8I+=k-z_0;MFd= z;p4@6jm!QQ597@&pS#JpEPvlrFCE9@Fi1l2wB3I~E_%NmWlzRm+aMswzVQOfbVI+l ze-#z48=CZT3+B-Ts~Z7R0a_}1DW{o@8rj1&uo!u+vmV*r(TEWLbn3nybl}&s8_RFr zhSiJb5XG@*yu25{Y{lB$-R;Tgsxr1qcMLBfQ_=L9;S=<7)_8!ydj$|*S7OQ>qdZOO zCai*rZ$BD`TiLO%1WWsI4CWiKQvQ|5nP4ctJ}1pHEvg-f=QP2MEU)1uih*l~E8hhp znzdHCWq7gb;d0ToLXvguT7te}^|B_sz|r`!@mW@ymHG}Kq~EY#b?<{MtN|kKHim4 ziEP(<;(1AMIK0Hx%q9-sUWI~j=X_@#TFfJ!$^HTsE;!syya&mjHFr!C$m>3&bf}aT z0)<5VarM32_mR!rmkZdB&p}b20|efJ`^&F2qb0*TV8iwz&b^PHZf3~yQj-I{p$Sp6b$*R9q^`p&Sw$m zRf|3tg6SiMmAKb+fH#;vBSeaLc0)0VXZ&#|n1n|%7Vzac)pkb8QiWbjEri$;98=xR zd0{Ghv%W>1E&Y9#MK!Or^hc-PK@a-0;8gVGD9~!SlxMo1Xzja-x|Dt{Q-Fy-3@0#F zG$&SbB~UX&lfcO|4>DLt(V2A7nN;0R%F&f}+(BZu#l`j5xm=fO-3bw26c08hiQhHF zEvijviKsvtOOf`9<0?skQZ-@YqKI5IjdrvUhwB&=BaKfXY2dl55MX9e}Z_vz02w zY@h0g;qrXIgR_&`GI)hREmN61a6_ZQ!S7?QAz2yC0!ddZd1O>8jNmRGM!t>?8}qXd z?*qc$I_-~g1g2Ht#_ZgYs=P*$!iGzj#;)w}!()S}%9XYLVZBsjYCQau zwk$TQ?u!TCrp@Md^3pWVHEJ70dfSEN?mhE?M8X5&18 z#$9h2B9hvVE7`O5I95A$4H(n%L-*1(s_0A!ZmRZtx8ipOT74Zu8n**JP{%WFwvg7q zzsPZBTU94K79Xm4s$fXrqMUKfYHDogmo9+qXd?3+PifHFJ5G2cw)albiSk%>8ui|j zzyM`wf%e+StDMFVeSFypDW(xmGE)oVw8IswP?ur6$`u;S4I4(d1PZd5U{F0C=lqmb zWpe<+?W69Aa6eSESF`C$e3j63xa-h(+O^C>o%#ZQidOa16|q#WT5>s9)e}Yswx>HG zO1)8zM_2E=yCwW@?H`zR%Lw@t>qpA#@9+as=AQ8g>nZl3RY~u*;$!d>!M$>Jt`m(o zGk!??Y6HyP`K)&`FP#EU)faN{4k+kv<~yL=$xCK)#D*6_a`SX|aN9eA^9D&<~=1rEDYB%_f;Ek_L zvtX%DXwHS{Nj=)QNMb&5k)cY^qdpjk5RHv>aUb@Qi8#-DRh%@KF2R#4qhiN%HGQ!v zQe8u$>bzv5S`u z=-ZO;WeZArZsd-kB7{o%^fm&Dj(bmI7SWXJfY!H%i96enyb^&!UR!0@V{L_cO)I+FA%ZfYxqBy+#k3a;~LtcH~>TMjpsx#;M z5`B?C#6RMMm91i_)Ho+q-%-qyiGK$^`J>|F#pUC}8J16*NRfx>a#@wxtp2U73Nl8~ zFmShGKgISP8Vc2Ej)k^y`7+wr7`D>bi%?h$|jL4f+G(He@o0h$?MD*fg>+6DM@%I zKsYSF01Fny@8K!7vqT1&#OC(CLMHMZiM>&_ehykFm!H_cDbt)}2nm|0LzN?6ie?}3 z_xBUNe>i*EweOT8xANt;Bklj1mwt(gL&EC@5AzH$B#8Mf-c*BC4pkeJ5Gl+R3e0gp zO&OGfhx5psiXvOKwHBtlV29t*9k{VjN_r_} z#CnHURpg=tuX$+=h5WVhINeQ1`j}uT;^Te74mD(DelN;EK?5NI_V3%b{c`V|L9o#YUS73)}K}wAgKc6QGTs-{c7OXZ2V6H4a7eU{F$5o3jH-N z`4jq$?04v|S<0^#{w}Y7;sF3t3IO0AV*4xn?=kVO@FS|f!2gJlstRzB)dc{MAin^J Lp10Ee_V51yBlZI6 literal 0 HcmV?d00001 diff --git a/old/t01/20240322_24_4h.xlsx b/old/t01/20240322_24_4h.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..484d1e1809c715a842f400ba3d506cf6e4ec91e0 GIT binary patch literal 10407 zcmeHtg;$)(^7deZySo!4Ft`S{Ai>==xI+jI6M_?*;O-vW-2wrEyF+kGaQ!CPy}!Mi zz29GOZ_ky4gd&10001#0F&c%O9LnXAQ~0`zy=^d>xw(r zyPDg(8mf6Xn!D(;ytT8X$bp5X&jLV0=Krt#7q38B{IEg~D~9CN%X{%HX4$1`5d^Ly z@BkL0vQTGlTz`qFZibcBQ%2|=hFCVPHP0uku_Z6|vk}W`d%JpYNPmk8N_fCvr@8?# z52$b8khTM#FxFW|`y>m8ScHwx&@l3KDj>0VCZi?kQ+I4J_-c@3==8ULlGX(-FyXR~9 z0~r#Ed_0U`-@?Jqjl7+TN2Vm+_66hcTdhS1W`+1%Cz#PYNMA07XTIrwj{ zUKaaGv4<5U=;Y;n@Zi<-ax{jRoV)1D7D_c=f0-rpy2xA_;^mHKBp7N$?_i|;+I;Va z7MEW{?hjI4Zg7@`W8nx;*SnVoeY|yYgJ-03PLy&gTkpkuGkr0AnJg{qLI0*Lim|k@ zC|mCHDz(()nZzfIF&0fCMC^Q$AUwh30G<9<+AAj46;RV+sz>EPRSi5@`*CBbezS=M zd#J)8eDa4A$+&|qCKmG*-h;N37dJ%es+N3KRVEqE{8S!>X11L_BvL!DAH10r(gszi zII*tT$D{{nvo8E}YB|q_(>;255PM1oPd|gh$%|GX9_pVY$;+@&e1s5W3PK@t$jZF6 z1-Uyo+n6{w*!=WcW$KIexvUsIhE`8dH|ZwWEKpppokM=E?pV&-4jD1NX90k=&+P`4ec2P#d;OiMQGN)28r~vL>9Zell=W5i- zTd~M;iwJ1shOb{a>=0AVB2&%)0r3|Ab6NFl^J$rWB%RFAummG*ByA-)xu)uUS@x6} zLer8#W@6JZMtaaV8D%_q7sfEH;+zD{t1j6(_?YK~NV~IymHTMrFNL5rm744sIWQ&R zFpx7`>TE<~lxfy&)sbZI`5N13D?TMSdYnEZ0JaDq%0H9FeG!v`C+7zII+Jl8`UI;R z*oxS7$q6JEcYgkvtZ8&TCgEJ@^N@gv2gnH4k1*1|R;N4=qg64dN!p&9C5Vd<9|U;7cU5aQtNB747&p`+BF(o{2- zqW%FD*3%TrY5pT231o?=-n=k^OZP)_D6i1`6n!Bt^vxV})K%H#)kzz?GyCDWdww3& zRn2GGt+gN_F)50Zy=)^34Ce_idThej;AgXY1NFj}BQe;o{%YS96^M_@d{rCl#BIp2G&CDjHh8SnnB|`q7eIBWM%srrWY<2JHw=42M=d|WE)?;@8GIQ`?cg^Ns5q=ead1` zTXy03n@U7k@8 zzAr+lvu4#V{C3}!kMDC5i(ke3wD!G?BMeZ9Nur&^t6P*xdl`b=S>qqoYteP zZtT`BUrC1`=5@Gs%l|e|^{^aV+t5If!ti?gsY!hC5||10ARqrhh$!;{)dMiTJ4MNK z-6eU6@V)i^^(&82o37kjlx>nyIkFNiLGK_}ir_(MazBPxm+hS77WI>LSFyVxhK|MW z&xg-XUT=LKm|lhK^gmT5NP4K0335nar3U~AAT$27t}d45=B_TFUjy4uGn*Ex8@|Yj z@qQKg0bT6mL(W{wJeO0V8Fm9QbNtE*t_Ml@?uP783&Wj3 zK~@{GE6iDfYLODtuoiKeYM1)bLPw%3UgK;|oPmwqPg{4VPj+(&J_At(jVu$h;{5rf zUxy02tS7!6Dg4XV&&Fmqg{!6Mw%)r6KD{EyU_%#})?Z_?J{OX^Kkg=K>z;7$J_c1C zH+r|LtBXFd^w-Sm4}C<;1WnSU3g&%I0}bctMeOWV%X5roK8^3?sl4r3R_hhl|4KL} zY;HPK5GN$kfqU4Knj-P(7K+FW7H2(PV%9f+U+CS8KF7Caxx=BRSF^X^)F67S+e8J4 z!J;>g3y)lqJ?n~JNET%J2^QKdh~s`jCs5?Z|=C84xViQ z`8^7?`L(wB-Q9bxlHNc3m|P^fl@5C~D1Cc1wsW(0w)10hQ};IRb+f__QGu>$1Ae0d zzy7mjvg>N;7aOP_hyH+{OxgO9M5jjyA;#?C8-q z(e4DrwrnkFjcHj%lh#j_2Dc(CkmC5_w9a13I5I0M34hSZ#F(5T$8o`=BeC!JOqDJE zuG(0&U^X+WOx{t0u2F-oD2lD*%X;=F0s3!LUmSNQvJMAcuFp}pE(KnCjw5}M#Z=(K z(PP5pbX6|DlMG?t9m-~v0!{{;cc2%_1n%`Qup*1|B4X?j!VlA(%7Zhb*e zY$$5g0oAzm*YA5i%WLD@2cVjgcWIXq12^8K)*1V2YnCjW{uGedKAW9D8)@mI{;?zY z!1(|c92+7v+g}q7iyU_Qa(5F$nRQ9^L5|<3M#F~6FuTu)&`+qh8WDAB0f8D zS+G`DvObB^IGQzmWkNPYv7ohTrQ7SDO^#kM>{}~NWk2Mbo$p>d#e=rYoIkni8FdFx zuxKM+*(?-+X9AwX)ZG%`aOAZ5+kBGl0Dhz;p}its=P~69{$H+!5;~vUqHRageN381TrU?Da0ahW%8Cq0jJ+jzhLbIS- zBGZAV*pc<)9&!6#%G_x`=jW0%Tq=WAlvy9ln!qrjb4M z-AN(r2&P;6Kp5+3RCBElrm&+(rI+qvynBn4LB&FS{=3k1cXxW<*cLXzS576@ z%FyvU%y0-qkc=a;Rd*J_P(R8lIWCjseV7$?QJ=w%-a4w5x*IJ6*}}u$RmgXJp;TJ0 zM`ypN(}Q`tbQ0@@0GXg)dNE!3j_ppTaTi++7_m2_=9e%JuYD^PKH|_d^eus~UC*?F zTX8Tnnu8LV@*@d@tx)aUJ&~=!G)`Xq4S0&%Fxqn!m&Ql=4wMR#(pg2(wG|@ZMo&xJ zB^z5sjntjtkV3&;P63CS9s^Czv%aGfnTP2{2oXr3r*Y}H7u-qgx1g+LyW9>FCGn2A zHlYt`%$@JeR^1wD37!tk8s-9dO(h5}cw*t?(_G`wyEW!aF1o5b*V4ofm{@^d6XUOk z)KP&`VT4Mr*|2rK2*GI%bFGzq-3PPzACHbwh>^Q~Mx|@cK*5Y}sJIjlUoTv1mg>^Z zy`Ut(W+2@m{h(~v7!XQYKJjpHVi%ak8P)Lg;RooK75~-Qepp5Vi(tGrG*5;fT*c_c z?MB|C+QpTZ)}J6HEX-+ez%2`JP5t5HIBG`i91MrnQ)ca{HI5;k;Ztm4%azt$i_&$s zBO041T5Ulk%uMY!);ro;dgi<7!F^$-Ebn7J9h=~iO9`MoLFCe692APN5PwsgR)Pk? zb?+aw?wk=kBF9p)ZT4+>Uywj1y8JS*pZP+IYfvt%lR3t=>%tSXakoS*`}iM^Z#h*@ z=mzirzzD_9)Z?!J!`0H<&K&fs{}o#tYU_p*^WgY`&V^Ba9A9&94r4i-aV?thAZkC$ zkJ8b-q%S2Tlv#;H42Ts<5X+8EY{ii^yj7A#X6CF#e*i7VejSe5jd^_-Gvi#58k@MR zj~8{Ho}26PK`5QC`Fb=x$x}KBxjQz|uwCi9U|Pgn63t|yp9?QNr?=wE_y|mMMEe`$ z=lAaQtG)+R3$d6VKEZzvofBggj&-hAe;$U3LhTeV;t0L^iXbW`P+Exj10O%Ijg47D zHeZKLisq~hwlC{)!AwkQ?(Sh6vU&=X=g5`W*^P91-yM0Hw@b!j4Svo zldOym70g;xr+LfeKjfy@u~K|W{_AFjT-cR&J4WpMqoBf(! zrE$cf*#ed)Sa#}8HD)c+WZ&jPsy0s0w+=&;+bYltAGhm=2Plbz(a5}LN?J!G5@mPC zq=ip=+hRr}x-p{E4U6)TF<-P8>1o;_OL#9NhdBbp${b&dY9=UNFoVE}J&yT_-SPr; za8o);;rQvT^HX6Qy6}spCw=Y)4fD@OS!UKl4;L4Oetu^GS3Bs2j~8n-uKSa3jW;sA z@5bXY{d`h9wVaN_1~UDgwgyhfh3~hb9LRWU8~FHH*Tu0+*Y*1bR#5SJph+(`VIEDe zdJr(>pe3^wvztNG$Zy>OijZf!>X99sj0o{hC+_Rb4}5!fVtHS;WA))VMzJjzFYWm= zS+n%?^mwqlDUR;Y9RtOrE1Fv9KS3{Lj`2z{)B544|>SDLeKN zd){#zgJ}VNm3Jj{#vj6~&rSocLA5O=KoivXTn{K##JqaA{GC6dS!20Jnj5PgE(dKh zI7!F8HSil&A4`Hbj_Txk%S$O1>MMM2vyEFZvAn`0_DbP?u|~5BC#_1%bEZz#l>!j$SBQ;vS!HBc`P> zI&i8;MS+Y5xX9t#66Ua6()^v1=re>Ct%4ELaQ($J-kMyFOr7Gka5l*S%ZN|zKlFp) zNV#jgGl+fopf%>ZDtenE4)xT>5b@?F-A4E&kFkvN4}~(SQ7;df6)?x3Y1uYXIStIoArJS#LtlG#Z4-Qqvy3;F>?fXtVE(s2Y7eUP+ zQD8?E3dWtw-RqDdF3}8Daag#ZaC^~SBtMp%QFUf+_n}vZuTq1dkf=*nLS?>x+1OE= z$9@zrk81Jfdj}pUzgCw`e-&TtQqnmec!!?ICCI5*Vah8r!*|eH!dGd1|Ky`yV+G2b zW#6g$;=#P>dFRPoCvVU$Vjk!2;X3wgW$t@2hV0cgctbzCMF@JuvLA+E>WF?hPOlDN zf$9B)NFL9AC?@fYH|_+J@JQMcSe{*NZ=@hu;K|rZi2apqqNh1GOmT0&n zMo&Y3Wa>TiXYW>=ivDalT2mG(bo>qt3mBeB}y;=bL!T$60u4i;Sy4SG!yzhjD9Sex7$QGqm?Eae%; zQJf5=WCG%#h;mh(Z+7yqcC#uGl+I(Oj&QAsWSw9Ivnc3e+@Erd+dB~0LM{C+K2^<_Z~vgc>|9~P(u9UJ?IHVObIRufOcyJ2<`?!? z+N}ybVKp_%i?tlj5l|GTxUcQpq<96-s-FoDrjP2li9A1$O~1bVw$wWP8onXmg{Pw* zZ=uSrQg=F+w9 zEb-2`Y3e}jxT}jQw~?fv;S#2?J1hL~*dVHMd39h|HwBp*4?np*6J*_U@!-?6(cD2^ zlIpQaEyC|^lOS5Na5#lDV(q1b;F&I!JzwsI)TYz0LVVtAoXgj^<0VZ*Qd_#5HRFI| zy=~usF(o^6FIA(2&Y18<$${rq^v*z|zjH|ScF-H@c>0aqA1A@zOAZH88Hds zl2QL~$1biOw&pHB5Bas~Bi4%|814ACXu~VG4|C3T)C|2&umLzoGTF0rwbdUFqqNHP z`SKeKZfOKB$UOHZc!n8P!*+NVDoe1Mr`+u*h7EV!7=gg;RLx*4%NU@zjSW(k7bC%-abyK*IqK<} zr-|ZYcxJyo+#|tGTVQPv4S0x6Z5T&ExuJa_u~SbhreVE%Jh$OvitPi%p`asiuuQEI ze{jKqjB(9MN^HnvH^6Qrk?D@BByjaTI}iyJ>MWHgi)F7`?={ZsuOP+OQ5$)c-59Ko z|FS}o@rwtUsikr1;WAc;>o8vBG7aXsEdyKv1=)0vc|9Kc+=NDDvp>S^qt1!o093SR zv#A=sV#pfYbx1tzYQ~{feZC(>n@Y;ENQ!4IxeTn*34Fx!k?g-nXo7dglBK~g2 z2PT~|LLT}0FXi=j`2NYW@LYnr^8IL4Qaf$<7+i&L+Ac12!Vzc24~gGwnX|TAbWi4_ zl7UqH!58nD`Tfj%zAosgeT_Mlv>7qE-?P1Z%`PklV{tgrKk6tVZho#zN>d}W#mOMOCfF8GY}Z94}^ z%qI>qRIz8M4@N>nqoduNhkay1E_0q0Ck>{HKyn3C?0Al*N$WzTRV1peOAyUcI@a+z zi0kH8>g0FAL`mJR$Uq`nY)I)G=TE{|8atbtsk%B_*<1YLOZHcoi!p+bJ*38A0R6075x3@A0QSflD^x zl>?$>nS=NMlxO;3F@Z}sNUsUo+xhoMjJI0-#ioE}LHHBvl&hhy46)Y_2c3v#B<(5M>tP{#~dqzm67-B6LOEFVogTV zHomwpv$&0f?35LQ!J0QyITQZEOkh?i-*H$FD>Y0DB$SFM>(T98uuw^43rS{^lz)_x z=zCTlec3wAhNNmEyFD(7VrtvxZTk(Q=i1lr-b2@(%L!?$~a^B{5W082>py)IuR*@wZNuez91W;A^|ELYPgf=+i@IJ z5l0lbmrA1>ykTdRvjH)eW5|&cE<;P@wI{V77A=rh!qUC0evE3VtE-`=5D582oUni@ zQcH|0_Vt}av>AD~fk{lSE-x-0AI`A6+l2}nc`7uuSPxhp>Mi&}CZ`i!G%vmsh#>

zR&EI9f0h{knI`AgKgUdgs3Hc$MqvEADjGXF{!bGjOZI0=i|w*s{8_GsyG08+j!d(s z4-(N(&SUbNs{+V)YMS%oG0K5^i9TnTeDSX6bO@!Ob{R@v}g zoGW{u6yXk+s^FX$Fxk#&uUd7nzrTe&jK!$QctDC{3m=+3yh;5jL`XyehJhOSBa#Mn z5aEu@?AIbr%J~?Tn>qSCPW#46#@zLZgF7I}_sJK+n^lsIwDHf39R`5|+TkZ$%x%Zl z6C>962wGv`7-i!~RvcUt(_*Yy`~b%Voc=A_iEWZPPFX;aTZH1Khe=G0*4*q+I^Dig>u!Hyu{QoI<{x0YDy4PQl zh*19TA^uSX`(4WK$*sSnctK(X$gBLG>iS*4@8S4g0+2|43HU2G{~h{!VDcAKfcy{W z?_tXC68`RA|H10{{RrfYE8Tg&qU|5C;taU;^MFb;RuLTukj; z^i@3_Or3R^J#1}A3!ous^8k?G^Z&Q~7q38N(wIUYE1Ja3yGOCFOfu`W!th)tfkPPd zN`l?}iG$_FIysh>FFD~4XrlSpR@`4Orq(~OpG{cQ+SxV+h7GnUBS!`dcdO~W=4Kff zI;QHv#ZPe3)&k{Wy%uJ}*Vm6W%LJr3HuWiUitmWXR1#QV;|TiiWv>pYqi+qiAeU>G z@(!={Y%8d5qVtTF&7tkhV9r}v(tXgo*kh80jwJuEj^n0R3?$Q3p)*o$ak@{H?R2JX z=2$Q=hTLQgiR*Z^F}VfX*2zsQ&q+{Q7I9gwJs4-r(B=K!+pCKv2iaCR90w2rLDsbaUA^63}CE|eSl*-N_dK(&KO*uVAKq%|it&O`x^+?H{>cqm6) zk(Zl3(6?lGq?xB%@x++W+wRj0+*fFc+ZU{B^n=!mZc((6ZZb-r(39=q1O5-k)B8F> zr)Mb#e(cY%0Km%&6ae@)S~jS$Qe1*o2{I+7Mkx`Ff*^h7e7aAP7pzufz9obZz5p z^x-hs^)6>+BnB2gMWcIFNb0?#D;zzwQ;MWx5FCN@0PR6}txcobYKR3Da^0M&>Kk-orLzS9b(zDi*w!HAXp3eB_?`CN|v{;+b8TPu@%lS;NZYoEW$4 zQ&K}zc~^eg^_*v8*`EE}2z?d9KSu&1NlG`t5$fMbBCAXne+wqa1eijo;Fa;PVR5&2 zvNp1}xBeBkD%EuDGg+~Ga_e9G9~FAMVSv)f(9`s`DhqBWvPB~hG@20OQI;5LPoOqx zvZrC$C1&zB-!k@`K>{~UzF(?jP68rQ&iF)c$HfV%ICAQ|u=Dks%1$Y&l8Yv#N303* ziEL*EZjVwgP;if6zFVUJ4&R7=Op>?2vsIsts5fuV=h94huPWZFTp87fp7hmLq^we= zk6nJM6ux4Hm<$Wlvpq&NW-(q@UC^)|1h0*OaJd%9b>gpX3JRzQ({zu{IV*<{>N#>=w7>!8%NI+7M|$YB zIlOk%5jJQa1Nu-Y_1*_k22j|(K#<|ZoCRLdf;Cm}wh!8bfK%P{t@ntGS5s=WWX3m| zGSyEqMaf0U(vy@E6r6!nThkh-;!AS;>VXRcD=D#2XG)VAqGJaMF2(IIWT-!Z_&KUkn$+;_B;`^)u;Nao&glYD^D+hbO;e8~^}%|yn>Td%9SOu??&Nhb&~Y|*MRke2 zvKxjnfj-Y6LA=HgJ$><&%hFj|8wF}}OFi~Y7!QgJuIx}1|#Ql7iP)@qr1m6x)5owLBZ<&Q|X z99zFp<0g-klDX+M<)kG(?Doaa`#QQ-4I=3a>WRXaMyv*S0qR5-&IVl_#YC5Keu~%R z+f$N!F#=<3Ps>qIO(iDn+w)Bd+ly1$8)HwB4Jyb2+7tBc4lJUsA+?Y`JufQstk41p=JsfvNW2y;Um9%l5OPbs#;5vbtxwVWY3Ou#g|C9ihUH)YGj| zlX3W7ZoQ^&)zw^-Wr4B}@dQl_x!rKR+J(Zr>R~92z<~C`v;pzM*iT$cK*1ZGR1pVC z!C+z>A0AblAic_P#%vKx7-r1D;`w$a|h(3(cb*-HD*2>`$YpYf+(b+#}yb#Z3-#q%hGB(*d9Gkw#=r#C3X>W+itI=F7ak9 zf{v;$W+)7rDtPPDm9!m7(FjdJqClSdb+2CCGZ=os{)VJ6PgoJ`@)n6Is{JNB9W%!P4v0>kt z(M&Dv#jd0wc#`Q(`C*2XjQ1$G;@-wY2$_d(>1v+wGu}}iKSLhHjG5}FeY*r1ab|8% zdE=kmv4_vDh+$0J)rLh3SmZE9Qtm$!eB@r2mmpN-%|1KkB~p5meF<@2WdIVI zrwm(Qu@Ht8MpuxoIdL!T#pIM6(|S&WI!UP_eJC^Td&_mr#G zRH2llt17f;4kwX&P7;#@47bJfU47yP9`WTRW0`|`6O)HvJi^N)4nzW{U;fbZAG;*& zHM@;BXph)^pp=2Jco)baVO!nKJ_nRbIyxf?+n3I_tNE{ZW%&&K0VBtZBWKEA<1YIi z^zH3QF2;t&_WEK$FYAxDUe@EPl`2T^(JWJ`cBEYwoAl zXFFG-PZSPN=fux17mnV|FL$>NF9gpaDKXz{#GG+Y1^o+WE@pmg?x>io8$(Yak0Uc8 zS0dXW#G!0(AqzOfirjGi;50j5b|$Fvv{5i-j%BhO|Nez5I{SP%WiZ5Pz&psMD>aAD z6Qdpk?}w9BG#;o2SEu2HK95XO@Hr1P8#E`B)J4|CfnU+}{8*=)OTd}yHW5<6ScAjF z6N!@~fn*h3(oG*Q<1#-P(nIO-$=YI?D~y@>Q@3{NcBL$Ek*c(TLQ!pcYU3d5nQS#A ze?c|G^sp(l(ow^*#S79pIj~MfM2jlJEl0fy`|M-!LCsNan z*mIJnE}33icOhLda&kJYST`s`HwXJ`F&G>X`Nf50!Roa)EsLES4_|TN!%@5rou%;* z$%Rf53`iCXs5<&wZL+1V9u$*ZPE9VD-r3TLLt}hgZ4bPMX8DomKO5?1aU*rHm{S^r z9iOPi4@2JB7?y$4hB=(-$wTK8h5VWn? zZdm!b_IMPwV!lFfH|WwTA`C;*jl*E=xjz$zJU`>OWiPE9GV|25|D#2K1BXkA;g}w5 z4SjBnEl9eL08_GIi9%U`eg%gGb9jG6>w^6qaDm|iehpHnmpEd{Jx`I&o_x!qPAR7V3c^!9IUi}bO%rB5gsQ%|OY4rDjsh@1A5)Kg zAR|vygNrPTm4VTXOh))x+q>Fhbn zx_V#&e19Q}%w}1sVA> zmfJ6^LS!Iw*&7S@jtY`0tQ}dacMbe;=cOT;kT`D3y?O<81nC3xn|T;7v&O6)*{ZjV zEI#}3@q|Vy4Ohnfxo}+d&3HHIkt2bJTn`+$;C1`;QRJbnBAv0W5w5WrPs5R|8;Ju8 zRpjw0pUAfbcRBX{A(H5*g!(eOQ`_X~is=n?T%;_m0}~p==2v6>c5Y_)+r)jbotcKt zFJak~YsSo928M1UVj=Y6!f+|d$!?3hA&#@X{CN4UM&pS?F>xRlSt zV>z%BxRcmnSd-X}?;4yx+B%)h{;P}U09Jr$S4Sm1DGrzw=kWI`CVxTaZgr8|T>hQcM zng%>cQD~k%E0H_ZhL&4NL6<#tWvF-N2h??rfnWv0k9$8PWKsJtUIB5o)On7nvJ%=A zNbf;=jGy;xA}IqXGbvFhb>CekrhZ&7i3`0x6juzH$_~bZ?qZT*RgL#7xPVI6GJE|o zb+D|*gUyH}XA{JZW(V}@NI#w>7FiEV|HNf!!{_2GLzWh{gyvR=+tRia_TKl8+x@*P zQ&97Qss{&t;w1f5E&U@iaN;{T}{yk6ziHWlw^@(L8VR|g~N4(i_{GwBJ zW7%Yc447&j9@lz>+NKAnwL}>Qg1?vekbr;AS-G)jdCu<&zXmo(_8UfQWUx= zg54bw19KkuubO{jt)LVh23rE+`4!?COk#P2#YAanu= z_yrjmf;GdK_czfCm`FaouG=H)r0|I)C)Fw>1@2wTt8W3VTnJI0ii! ziY-8sOI%5f63ANGpmERTKkE9vYxDD!HG>a5Lemo`rXNxRn8w_ar%NQWuDC=s;M zua>5ZYt&DeHQEB@@HQNKGYwfwHQ4ug5o-;Tb*&;0WWRoTA30+?2nPTPM^H+?ZAsff zAP`}9N2h|z@@O+55ZRpo_Ch14(ie-AA-*iw=LsEz6);61Tbkl(k}rlN3FP28vL`aV zuv=WA4DU(BDxAJ}IK7l4q6)ojdC`5-q;5KLl4oKy`gCy8+P@s94m&?X8_A0*NB z9@MiDMjt%7ETlx_W>b`V75d zbE`b{5oPQ;jYl^RlrOpwJmU-F(PgKEQzzdS<);j3W_%ATTFSI_ym8JK)vCVHC-nxS z5vJhPUTB)OU3>61i~;6kF)Wq2owj$9%oI1c-X^>EqM}75Y3!ec21T1qsvR{yqhB&~ zv!2#~W+ZGVS|~%Av)jVhFjtI2H{b_2N+u}7<&N8S(Uh?~m(pE&Zqho$<~jNys9yK^ zL>e%xPf-VE3Ri!U_6#hw|F(`kCY!eM;3z@^-=_I#!Z=cQEsLkFkRw;StRs?5V#p%u zi~EIcC=Ah?I`5p$`1AoW96A(!&DN?E9+<48P1IkVSFu6RoN zuSBtO%!2h+(luvXG-rT^srlNHj{AtLw%FJn``6nN9s8jot0Ez0gh}6xu}kXH+oP%x zC(|Xp5;@A!A%I3K9Hg->Dl4syo>s1wYjQkTR4_bq*>7n!N ziY-Phubns!qnFUFCCE#f1E{H0ru+Hr?i#G%QyC~Yt?w{@;Pnl8z_w7#>);lsH2dI7 z=h)q*8m8dm)`p5F@x4z}7=>kvPy3M6&!o@fmyE(WLs($Tpy7|UskxUS%wV74h~~mS z?7`kcVHvW4r<9{W60oCE?ciH6pif*$xkA|6K@uI?0mb*78?8uFlZ62;Cfq+V$mwu` zEg-W7JAVJp{m@|4VyQi1mNzMef-e&2R*+_r)5{fJJwBtzK!T^ZlF^fY#QZj`9O^n* zal@QsVbmGc@|*dm0s6yvm&Ai3{;wz%=VJ4<^u=~h3QSJb*32!hFcv&z-tNub11 zGUbl5%WJhO^hMOwDXrCWFv24%&cC^}b(Q21IIE=*8qS{7b`@qklF7ck|F+(~UVw^*D)+=VYE)%`&VO|*iXw&wiI;zj?e*Eh|>8>4fVCD z$FZ7Ky1d0rdiRt9SHxZib4izMd<-LvqIs4WUSHhI-MiB5JJTIq-Xq1lQPq$e!F!M* zmQ*#PH+RC{yU{4)7%XKM{}#+3ZQNdrEvTT^6+vIOf9WJqCITRLYxLrCM1pAw+dWM4;)VVKb)tZo;uY^zf9} zx!4lYspID>j`)!3Pp{4P`=nKAE$NfW8aUZrkpeRV4;oZSOs!&tcTUwY)VM9gcr?9_ zsg*u8&kp;$#I+2Phv%I{w}JFQzK``yh~#)%jf;7njpVI?7#8ucV%FA(c^~NUh7F^u zQL0dWZhILiK1b#b>cTwZeeVda51|YkWm6r)Qc!B@TuteIFB;#p(>qhxl$v4lM0zaX z@H$kw9>^E^X;s>=ZZjhxY_1nzJCVZhz*Qc+^^qMGktN(oGDQZ%PNng~43octByU%J z^i6(qs4niiY6<#rPhw*W!_4Cij4+omoX;DS=sPxaFv+CE3n8YBIPA-_>YrQv;qRZd zK>|Y%abB&)s&_EEK1Ix?9TQZ#qwrAH5 z4-ad&Ke}QVv@7wsGU8Xs`|)0g151a#yEU)4x{@4!XlDuK~*c5V=gIx~Dq`DVkE zx8J4%T9!@xlms|ilcqz3`pdOF1~fHA(G*C3r-s))8r##b0Cc1fn`2GX}^;K6@E|D zFWHmQErheoW{=w`Qe;br%M|S)?`z^V@4$@8!Ucw{`j7vV;d}cn6t2vf_#`A z)P6JtyBs?2KCB$#eQe<>KL0@*!q#BK%Ox9T$T}M0(yeLVg;x+SNpb&l7XuVMxHcsc zd%5#?-l)7`>GZi@?d+U+rp>w4?~O^F_IUrJFZpLJRISIR=MQtvxL}rve^l9ie`d(8 zIeJzFuem9>0FMqXz?;|`1D)(0oLP+RolO6nulQfx9c+wI2_j&_!xF!Fw@!90_$3j* zq@E$;-&%lyAwlYv^0X#N8Dsi-mN6%;#45u zjTW60Ei(MM@VT;y6PqY^UAT~V&KEhvlvE5!3l!MsxxS!ahEZ7*jaJn>Ti8KoN=sS| ziu!tWr6?4A(H^;N!rAZ(W!&-BLOp|lTPPhs3_NcSj3#xOX*2A7IYj}A^3PXg#IBnF z=%F|TIQAI^rgGhA!8)FZ3p|V zDhpF49SpB%<)KU7L)8^4bZu^S3F);@Eq#Z+8N5PtHaniT{g4yDqXXi(Q5&+MuQx!; zL6TTQ)Y*R{*p(&^B#8epO!I{#dve&HI5a8l;r=Z9!FAXTXhZ&($W!DfwYXH(-8m4?W^s<5d1S2`7sYI~HC~m83F5Se1_0gXc)>P~%{dSk6ly|+? zmMcG|MY+RdDmbMC%yn|wsnlE@9)86f!=TroKO(}ifeX(b+oSjrCMYZpMMr^@il|Nz zg8yB5X}Xk?Y$aa#Zke`-)2{h5ec{gR(E}{u`P^Hfy&4Gzsw5gi`(fB2tw<0TQ^)D; z?1U9Ayk4KmcJe_bVngNO6@ zlA;_-$IqMxYT70WLlPghQ|79^QGNR05?f-Nfm(HR8NP1cwTHmwPZE-A>EqV>Z-9bV z?KrJ#TgW8u6{e`~LbE3M$`PI)llLjWn)!!4LqIZtL)U*EU;C&2{(1Zl!)!p=zYF;L zpv^ylzmBP3lKjiq&F{e9hY$XOwu8qJej7#j9sKvE>|am-U= 0 { + // 当前止损立刻满足,跳过 + return + } + tag := "long" + if dirt < 0 { + tag = "short" + } + err := s.OpenOrder(&strat.EnterReq{ + Tag: tag, + Limit: price, + StopLoss: stopLoss, + Short: dirt < 0, + }) + if err != nil { + log.Error("open order fail", zap.Error(err)) + } +} + +func onStartUp(s *strat.StratJob) { + s.More = &MoreType{ + bigSlop: com.NewSlop(sampleNum, 0.25), + hitGoods: make(map[int64]bool), + } +} + +func onInfoBar(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + sma := ta.SMA(e.Close, maParent) + prevSma := sma.Get(sampleStep) + if e.BarNum%sampleStep > 0 || math.IsNaN(prevSma) { + return + } + more, _ := s.More.(*MoreType) + curSma := sma.Get(0) + slop := (curSma - prevSma) / curSma + more.bigSlop.Update(e.BarNum, slop) +} + +func onBar(s *strat.StratJob) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, atrLen) + baseAtrLen := int(math.Round(float64(atrLen) * 4.3)) + atrBase := ta.Lowest(ta.Highest(atr, atrLen), baseAtrLen).Get(0) + c := e.Close.Get(0) + smaBig := ta.SMA(e.Close, maBig) + smaVal := smaBig.Get(0) + // smaSml := ta.SMA(e.Close, maSml) + // maX := smaSml.Cross(smaBig) + // rsiVal := ta.RSI(e.Close, 20).Get(0) + + more, _ := s.More.(*MoreType) + prevSma := smaBig.Get(sampleStep) + if e.BarNum%sampleStep == 0 && !math.IsNaN(prevSma) { + slop := (smaVal - prevSma) / smaVal + more.bigSlop.Update(e.BarNum, slop) + } + + var longOd, shortOd *ormo.InOutOrder + if len(s.LongOrders) > 0 { + longOd = s.LongOrders[0] + } + if len(s.ShortOrders) > 0 { + shortOd = s.ShortOrders[0] + } + + atrVal := atr.Get(0) + slRange := max(atrBase*badRate, atrVal*1.5) + + // 检查是否触发盈利阈值,若触发更新止损 + var orders = []*ormo.InOutOrder{longOd, shortOd} + for _, od := range orders { + if od != nil { + odDirt := 1.0 + if od.Short { + odDirt = -1 + } + isGood, ok := more.hitGoods[od.ID] + if !ok && (c-od.Enter.Average)*odDirt >= atrBase*goodRate { + more.hitGoods[od.ID] = true + isGood = true + } + if isGood { + // 已触发盈利阈值,每个bar更新止损 + sl := od.GetStopLoss() + if sl == nil || (smaVal-sl.Price)*odDirt > 0 { + od.SetStopLoss(&ormo.ExitTrigger{Price: smaVal}) + } + } else if od.Status == ormo.InOutStatusInit { + // 尚未入场,更新入场价格,更新止损 + _ = od.SetEnterLimit(smaVal) + od.SetStopLoss(&ormo.ExitTrigger{Price: smaVal - odDirt*slRange}) + } + } + } + if atrVal < atrBase { + // && more.bigSlop.Dirt > 0 + if longOd == nil && more.bigSlop.Dirt >= 0 { + // 开多单 && c-smaVal+slRange > atrBase*0.5 + openOrder(s, c, 1, 0, c-slRange) + } + // && more.bigSlop.Dirt < 0 + if shortOd == nil && more.bigSlop.Dirt <= 0 { + // 开空单 && smaVal+slRange-c > atrBase*0.5 + openOrder(s, c, -1, 0, c+slRange) + } + } +} diff --git a/old/t01/v1.go b/old/t01/v1.go new file mode 100644 index 0000000..4dd392a --- /dev/null +++ b/old/t01/v1.go @@ -0,0 +1,27 @@ +package t01 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" +) + +func MainV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 最适合5m的参数,测试20240322-20240324,50币,无需大周期趋势 + maSml = 10 + maBig = 50 + maParent = 30 + openOffRate = 0.5 + badRate = 3 + goodRate = 3 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 300, + MinTfScore: 0.75, + //DrawDownExit: true, + OnStartUp: onStartUp, + OnBar: onBar, + OnPairInfos: com.MakePairInfos(sampleNum*sampleStep + maParent), + OnInfoBar: onInfoBar, + } +} diff --git a/old/xem4o.go b/old/xem4o.go new file mode 100644 index 0000000..639f4ce --- /dev/null +++ b/old/xem4o.go @@ -0,0 +1,89 @@ +package old + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/exg" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "strconv" +) + +/* +这是针对XEM/USDT:USDT的策略,其最小价格变动0.6%,挂单等待成交盈利很稳定。 +但实盘时实际成交量很小,挂单十几分钟才吃进去。 +*/ + +func xem40(p *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := 14 + return &strat.TradeStrat{ + WarmupNum: 300, + MinTfScore: 0.3, + EachMaxLong: 1, + EachMaxShort: 1, + OnStartUp: func(s *strat.StratJob) { + pip, err := exg.Default.PriceOnePip(s.Symbol.Symbol) + if err != nil { + panic(err) + } + s.More = pip + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + pip := s.More.(float64) + if pip/c < 0.002 { + // 仅针对XEM,最小价格变动是0.6% + return + } + volMa := ta.SMA(e.Volume, 14).Get(0) + maSml := ta.SMA(e.Close, 50).Get(0) + maBig := ta.SMA(e.Close, 300).Get(0) + rsi := ta.RSI50(e.Close, 50).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr).Get(0) + + if s.OrderNum > 0 { + var od *ormo.InOutOrder + if len(s.LongOrders) > 0 { + od = s.LongOrders[0] + } else { + od = s.ShortOrders[0] + } + if od.Status == ormo.InOutStatusPartEnter && (e.TimeStop-od.RealEnterMS())/e.TFMSecs > 3 { + s.CloseOrders(&strat.ExitReq{Tag: "cancel", UnFillOnly: true}) + } + return + } + sub1, sub2 := c-maSml, maSml-maBig + dirt := 0 + if sub1*sub2 <= 0 && math.Abs(sub2) < atr { + // 震荡行情 + if rsi > 3 { + dirt = -1 + } else if rsi < -3 { + dirt = 1 + } + } + if dirt == 0 && math.Abs(sub2) < pip { + // 波动很小,直接开单 + dirt = 1 + if c > maSml { + dirt = -1 + } + } + if dirt != 0 { + amt := min(volMa*0.1, config.StakeAmount/c) + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(dirt), + Short: dirt < 0, + Limit: c, + Amount: amt, + StopLossVal: pip * 2, + TakeProfitVal: pip, + StopBars: 20, + }) + } + }, + } +} diff --git a/org/day/01.go b/org/day/01.go new file mode 100644 index 0000000..9deb266 --- /dev/null +++ b/org/day/01.go @@ -0,0 +1,36 @@ +package day + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func D01(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + o, c, h, l := e.Open.Get(0), e.Close.Get(0), e.High.Get(0), e.Low.Get(0) + solid := math.Abs(o - c) + solidRate := solid / math.Abs(h-l) + atr := ta.ATR(e.High, e.Low, e.Close, 15).Get(0) + ma := ta.SMA(e.Close, 20).Get(0) + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "closeL", Dirt: core.OdDirtLong}) + } else if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "closeL", Dirt: core.OdDirtShort}) + } else { + if solidRate > 0.7 && solid/atr > 1 { + if o > c && c < ma { + s.OpenOrder(&strat.EnterReq{Tag: "long"}) + } else if c > ma { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true}) + } + } + } + }, + } +} diff --git a/org/day/02.go b/org/day/02.go new file mode 100644 index 0000000..72e756f --- /dev/null +++ b/org/day/02.go @@ -0,0 +1,62 @@ +package day + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +视频号:期市萧何:最简单的交易系统 2025/02/18 + +80均线向上只开多,向下只开空 +当20均线和80均线多头排列,且价格上穿20均线开多;反之做空 +前期压力支撑止损,价格再次触碰20均线止盈 + +最小8h才盈利,且21年到35年间几次回撤时间长达1年左右; +弃用 +*/ + +func MaDirt(p *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := 20 + baseAtrLen := 86 + slRate := 2.0 + bigThres := 0.2 + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + maSml := ta.SMA(e.Close, 20) + maBig := ta.SMA(e.Close, 80) + atr := ta.ATR(e.High, e.Low, e.Close, 14) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + bigChg := (maBig.Get(0) - maBig.Get(3)) / atrBase + smlChg := (maSml.Get(0) - maSml.Get(3)) / atrBase + cSmlX := e.Close.Cross(maSml) + if len(s.LongOrders) > 0 { + if cSmlX == -1 { + // 价格下穿均线止盈 + s.CloseOrders(&strat.ExitReq{Tag: "exitL", Dirt: core.OdDirtLong}) + } + } else if len(s.ShortOrders) > 0 { + if cSmlX == 1 { + // 价格上穿均线止盈 + s.CloseOrders(&strat.ExitReq{Tag: "exitS", Dirt: core.OdDirtShort}) + } + } else if bigChg > bigThres { + // 大均线向上 + if cSmlX == 1 && maSml.Get(0) > maBig.Get(0) && smlChg > 0 { + // 价格上穿20均线,且均线多头排列 + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLossVal: atr.Get(0) * slRate}) + } + } else if bigChg < -bigThres { + // 大均线向下 + if cSmlX == -1 && maSml.Get(0) < maBig.Get(0) && smlChg < 0 { + // 价格下穿20均线,且均线空头排列 + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, StopLossVal: atr.Get(0) * slRate}) + } + } + }, + } +} diff --git a/org/day/03.go b/org/day/03.go new file mode 100644 index 0000000..1d9bcc4 --- /dev/null +++ b/org/day/03.go @@ -0,0 +1,47 @@ +package day + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/utils" + ta "github.com/banbox/banta" +) + +/* +在大周期4h的前一个小周期入场,检查是否有套利机会。 +无法盈利 +*/ + +func D03(p *config.RunPolicyConfig) *strat.TradeStrat { + tfMSecs := int64(utils.TFToSecs("4h") * 1000) + return &strat.TradeStrat{ + WarmupNum: 100, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", 10}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = 0 + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + dirt, _ := s.More.(int) + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } else if len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } else if (e.TimeStop+e.TFMSecs)%tfMSecs == 0 { + s.OpenOrder(&strat.EnterReq{Tag: "ent", Short: dirt < 0}) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + ma := ta.SMA(e.Close, 10).Get(0) + if e.Close.Get(0) > ma { + s.More = 1 + } else { + s.More = -1 + } + }, + } +} diff --git a/org/day/04.go b/org/day/04.go new file mode 100644 index 0000000..69bd1bb --- /dev/null +++ b/org/day/04.go @@ -0,0 +1,73 @@ +package day + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +视频号:山居听雨楼:渣男战法 +5日和10日均线,10日均线向上向下确定方向 +收盘价小均线金叉,且多头排列,做多;反之做空 + +效果较差 +*/ + +func D04(p *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := 20 + baseAtrLen := 86 + //slRate := 2.0 + bigThres := 0.2 + upNum, dnNum, zeroNum := 0, 0, 0 + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + maSml := ta.SMA(e.Close, 5) + maBig := ta.SMA(e.Close, 10) + atr := ta.ATR(e.High, e.Low, e.Close, 14) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + bigChg := (maBig.Get(0) - maBig.Get(2)) / atrBase + smlChg := (maSml.Get(0) - maSml.Get(2)) / atrBase + cSmlX := e.Close.Cross(maSml) + ph := ta.Highest(e.High, 2).Get(0) + pl := ta.Lowest(e.Low, 2).Get(0) + if len(s.LongOrders) > 0 { + if cSmlX == -1 { + // 价格下穿均线止盈 + s.CloseOrders(&strat.ExitReq{Tag: "exitL", Dirt: core.OdDirtLong}) + } + } else if len(s.ShortOrders) > 0 { + if cSmlX == 1 { + // 价格上穿均线止盈 + s.CloseOrders(&strat.ExitReq{Tag: "exitS", Dirt: core.OdDirtShort}) + } + } else if bigChg > bigThres { + // 大均线向上 + if cSmlX == 1 && maSml.Get(0) > maBig.Get(0) && smlChg > 0 { + // 价格上穿20均线,且均线多头排列 + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLossVal: pl}) + } + } else if bigChg < -bigThres { + // 大均线向下 + if cSmlX == -1 && maSml.Get(0) < maBig.Get(0) && smlChg < 0 { + // 价格下穿20均线,且均线空头排列 + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, StopLossVal: ph}) + } + } + if bigChg > bigThres { + upNum++ + } else if bigChg < -bigThres { + dnNum++ + } else { + zeroNum++ + } + }, + OnShutDown: func(s *strat.StratJob) { + fmt.Printf("up: %d, dn: %d, zero: %d\n", upNum, dnNum, zeroNum) + }, + } +} diff --git a/org/day/05.go b/org/day/05.go new file mode 100644 index 0000000..4666fa8 --- /dev/null +++ b/org/day/05.go @@ -0,0 +1,58 @@ +package day + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +视频号:漫谈期事:高手能稳定盈利的交易系统到底有多简单? +截断亏损让利润奔跑 +任意阳K第二根开多,开仓k前一根k最低点止损,反之做空 +没止损就拿30根bar后平仓 + +无法盈利 +*/ + +func D05(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + o, c := e.Open.Get(0), e.Close.Get(0) + _ = o + ma := ta.SMA(e.Close, 30).Get(0) + ph := ta.Highest(e.High, 2).Get(0) + pl := ta.Lowest(e.Low, 2).Get(0) + rsi := ta.RSI(e.Close, 4).Get(0) + if len(s.LongOrders) > 0 { + for _, od := range s.LongOrders { + sl := od.GetStopLoss() + if pl > sl.Price { + sl.Price = pl + } + num := e.BarCount(od.EnterAt) + if num > 10 { + s.CloseOrders(&strat.ExitReq{Tag: "exitL", OrderID: od.ID}) + } + } + } else if len(s.ShortOrders) > 0 { + for _, od := range s.ShortOrders { + sl := od.GetStopLoss() + if ph < sl.Price { + sl.Price = ph + } + num := e.BarCount(od.EnterAt) + if num > 10 { + s.CloseOrders(&strat.ExitReq{Tag: "exitS", OrderID: od.ID}) + } + } + } else if rsi < 10 && c < ma { + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLoss: pl}) + } else if rsi > 90 && c > ma { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, StopLoss: ph}) + } + }, + } +} diff --git a/org/day/06.go b/org/day/06.go new file mode 100644 index 0000000..47a870b --- /dev/null +++ b/org/day/06.go @@ -0,0 +1,40 @@ +package day + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +bilibili 生财茶馆pro 98%胜率的5分钟终极剥头皮交易策略-测试 + +很差,一路向下 +*/ + +func d06(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxLong: 1, + EachMaxShort: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 6) + utBot := ta.UTBot(e.Close, atr, 2).Get(0) + stc := ta.STC(e.Close, 80, 27, 50, 0.5) + stcVal := stc.Get(0) + stcUp := stcVal > stc.Get(1) + hma := ta.HMA(e.Close, 55).Get(0) + hh := ta.Highest(e.High, 3).Get(0) + ll := ta.Lowest(e.Low, 3).Get(0) + if utBot > 0 && stcVal < 25 && stcUp && c > hma { + slVal := c - ll + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLossVal: slVal, TakeProfitVal: slVal * 1.5}) + } else if utBot < 0 && stcVal > 75 && !stcUp && c < hma { + slVal := hh - c + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, StopLossVal: slVal, TakeProfitVal: slVal * 1.5}) + } + }, + } +} diff --git a/org/day/07.go b/org/day/07.go new file mode 100644 index 0000000..7635a13 --- /dev/null +++ b/org/day/07.go @@ -0,0 +1,162 @@ +package day + +import ( + "fmt" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "sort" + "sync" +) + +func D07(p *config.RunPolicyConfig) *strat.TradeStrat { + symVols := make(map[int32]float64) // 成交量记录 + allowSyms := make(map[int32]bool) // 允许交易的品种sid + timeDirts := make(map[int32]*TimeDirt) // 记录每个品种的触发时间和方向,只做顺势信号 + holds := make(map[int32]int) // 各个品种持仓情况 + var lock sync.Mutex + var bigTime int64 + var allowConfirm bool // allowSyms是否已刷新 + const volRateThs = 1.1 // 成交量最低倍数 + const topNRate = 0.1 // 交易前n个品种比例 + return &strat.TradeStrat{ + WarmupNum: 100, + DrawDownExit: true, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 1}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + if !allowConfirm && e.TimeStart > bigTime { + // 确定当前小时,可交易的品种 + allowConfirm = true + topNum := int(math.Round(float64(len(symVols)) * topNRate)) + allowSyms = GetTopNKeys(symVols, topNum) + } + sid := s.Symbol.ID + _, allow := allowSyms[sid] + hold, hasPos := holds[sid] + if hasPos { + if hold > 0 { + if len(s.LongOrders) == 0 { + delete(holds, sid) + } + } else if len(s.ShortOrders) == 0 { + delete(holds, sid) + } + } + // 要求最近3bar成交量加速,实体颜色一致,且实体至少70%;最新成交量是平均成交量2倍 + volMa := ta.SMA(e.Volume, 100).Get(0) + v0, v1, v2 := e.Volume.Get(0), e.Volume.Get(1), e.Volume.Get(2) + prevVolRate := v1 / v2 + if v0 > volMa*2 && prevVolRate > volRateThs && v0/v1 > prevVolRate*volRateThs { + // 成交量爆发 + flag0, solid0 := BarStat(e, 0) + flag1, solid1 := BarStat(e, 1) + flag2, solid2 := BarStat(e, 2) + if flag0 == flag1 && flag1 == flag2 && min(solid0, solid1, solid2) > 0.7 { + // 颜色一致,且实体至少70% + lock.Lock() + timeDirts[sid] = &TimeDirt{Time: e.TimeStart, Dirt: flag0} + lock.Unlock() + if allow && expDirt(timeDirts, e.TimeStart-e.TFMSecs) == flag0 { + dateStr := btime.ToDateStr(e.TimeStart, core.DefaultDateFmt) + fmt.Printf("[%s] enter %s dirt: %d\n", dateStr, s.Symbol.Symbol, flag0) + // 只交易成交量top,且顺应大趋势的 + if len(s.ShortOrders) == 0 && len(s.LongOrders) == 0 && len(holds) <= 1 { + holds[sid] = flag0 + s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + Short: flag0 < 0, + }) + } + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + lock.Lock() + if e.TimeStop > bigTime { + symVols = make(map[int32]float64) + bigTime = e.TimeStop + allowConfirm = false + } + // 计算所有品种过去1小时成交量 + avgPrice := ta.AvgPrice(e).Get(0) + symVols[s.Symbol.ID] = e.Volume.Get(0) * avgPrice + lock.Unlock() + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + return 0.4 + }, + } +} + +func expDirt(dirts map[int32]*TimeDirt, minStamp int64) int { + up, dn := 0, 0 + for k, d := range dirts { + if d.Time < minStamp { + delete(dirts, k) + continue + } + if d.Dirt > 0 { + up++ + } else { + dn++ + } + } + if up+dn >= 3 { + if up >= dn*2 { + return 1 + } else if dn >= up*2 { + return -1 + } + } + return 0 +} + +func BarStat(e *ta.BarEnv, i int) (int, float64) { + o, h, l, c := e.Open.Get(i), e.High.Get(i), e.Low.Get(i), e.Close.Get(i) + solid := float64(0) + if h > l { + solid = math.Abs(c-o) / (h - l) + } + flag := 1 + if o > c { + flag = -1 + } + return flag, solid +} + +type TimeDirt struct { + Time int64 + Dirt int +} + +type kv struct { + key int32 + value float64 +} + +func GetTopNKeys(symVols map[int32]float64, n int) map[int32]bool { + pairs := make([]kv, 0, len(symVols)) + for k, v := range symVols { + pairs = append(pairs, kv{k, v}) + } + + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].value > pairs[j].value + }) + + keys := make(map[int32]bool) + for i := 0; i < n && i < len(pairs); i++ { + keys[pairs[i].key] = true + } + return keys +} diff --git a/org/day/main.go b/org/day/main.go new file mode 100644 index 0000000..f399f20 --- /dev/null +++ b/org/day/main.go @@ -0,0 +1,15 @@ +package day + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("day", map[string]strat.FuncMakeStrat{ + "01": D01, + "02": MaDirt, + "03": D03, + "04": D04, + "05": D05, + "06": d06, + "07": D07, + }) +} diff --git a/org/digo/common.go b/org/digo/common.go new file mode 100644 index 0000000..d8b107a --- /dev/null +++ b/org/digo/common.go @@ -0,0 +1,33 @@ +package digo + +import ( + "github.com/banbox/banbot/strat" + "math" +) + +func init() { + strat.AddStratGroup("digo", map[string]strat.FuncMakeStrat{ + "v2": v2, + "v4": v4, + }) +} + +var ( + trendFilter = true // 是否启用趋势过滤(逆趋势开单降低权重) + goodWeight = float64(1) // 顺趋势开单权重,默认1 + badWeight = float64(0) // 逆趋势开单权重,默认0不开单 + badMaxRate = -0.05 // 开单后允许趋势相反最大价格比率 + goodMaxRate = 0.005 + stopLossRate = 0.02 +) + +func calcFactor(pFac float64) float64 { + odNum := math.Log2(pFac) + 1 + return math.Pow(2, odNum) +} + +type MoreType struct { + bigDirt int // 1牛市,0不确定,1熊市 + maBig float64 // 大周期MA,用于判断趋势 + signal float64 // 最新bar是否有信号,1开多,-1开空 +} diff --git a/org/digo/v1.go b/org/digo/v1.go new file mode 100644 index 0000000..1bca17d --- /dev/null +++ b/org/digo/v1.go @@ -0,0 +1,209 @@ +package digo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" + "slices" +) + +func openOrder(s *strat.StratJob, m *MoreType, signal float64) { + odWeight := goodWeight + if trendFilter && m.bigDirt != int(signal) { + // 启用趋势过滤,且是逆趋势 + odWeight = badWeight + } + if odWeight == 0 { + return + } + allOrders := s.GetOrders(signal) + orders := make([]*ormo.InOutOrder, 0, len(allOrders)) + for _, od := range allOrders { + if od.EnterTag != core.EnterTagThird { + orders = append(orders, od) + } + } + enteredAmt := float64(0) + for _, od := range orders { + enteredAmt += od.Enter.Filled + } + c := s.Env.Close.Get(0) + basePrice := max(c, m.maBig) + if signal > 0 { + basePrice = min(c, m.maBig) + } + stopPrice := basePrice * (1 - signal*stopLossRate) + if enteredAmt == 0 { + // 没有打开的订单,直接下单 + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + Short: signal < 0, + CostRate: odWeight, + StopLoss: stopPrice, + }) + if err != nil { + log.Error("open order fail", zap.Error(err)) + } + return + } + slices.SortFunc(orders, func(a, b *ormo.InOutOrder) int { + return int(a.RealEnterMS() - b.RealEnterMS()) + }) + prevOd := orders[len(orders)-1] + // 检查价格是否背离订单5% + chgRate := c/prevOd.InitPrice - 1 + if signal*chgRate >= badMaxRate { + // 价格背离未超过5%,不加仓 + return + } + // 以上一个订单的两倍开单 + baseAmt := s.Strat.GetStakeAmount(s) + prevFac := prevOd.GetInfoFloat64(ormo.OdInfoLegalCost) / baseAmt + costRate := calcFactor(prevFac) + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + Short: signal < 0, + CostRate: odWeight * costRate, + StopLoss: stopPrice, + }) + if err != nil { + log.Error("open order fail", zap.Error(err)) + } +} + +func closeOrders(s *strat.StratJob, signal float64) { + allOrders := s.GetOrders(signal) + orders := make([]*ormo.InOutOrder, 0, len(allOrders)) + totalProfit := float64(0) + for _, od := range allOrders { + if od.EnterTag != core.EnterTagThird { + orders = append(orders, od) + totalProfit += od.Profit + } + } + if len(orders) < 3 { + return + } + if totalProfit > 0 { + err := s.CloseOrders(&strat.ExitReq{ + Tag: "exit", + Dirt: int(signal), + }) + if err != nil { + log.Error("close orders fail", zap.Error(err)) + } + } +} + +func MainV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + trendFilter = true + goodWeight = 1 + badWeight = 0 + stopLossRate = 0.02 + badMaxRate = -0.05 + goodMaxRate = 0.005 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 361, + MinTfScore: 0.75, + DrawDownExit: true, + StopEnterBars: 10, + //OnPairInfos: func(s *strategy.StratJob) []*strategy.PairSub { + // return []*strategy.PairSub{ + // { + // Key: "_cur_", + // TimeFrame: "1d", + // WarmupNum: 60, + // }, + // } + //}, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreType{} + }, + //OnInfoBar: func(s *strategy.StratJob, pair, tf string) { + // m, _ := s.More.(*MoreType) + // m.maBig = ta.SMA(s.Env.Close, 60).Get(0) + //}, + OnBar: func(s *strat.StratJob) { + e := s.Env + _, d, _ := ta.KDJBy(e.High, e.Low, e.Close, 13, 3, 3, "sma") + ma3_d := ta.SMA(d, 3) + + //fmt.Printf("%s: %.3f %.3f %.3f\n", btime.ToDateStr(e.TimeStart, ""), + // k.Get(0), d.Get(0), ma3_d.Get(0)) + + c := e.Close.Get(0) + m, _ := s.More.(*MoreType) + m.maBig = ta.EMA(e.Close, 360).Get(0) + if math.IsNaN(m.maBig) { + m.bigDirt = 0 + } else { + m.bigDirt = -1 + if c >= m.maBig { + // K线高于大周期均线,牛市 + m.bigDirt = 1 + } + } + + zhicheng := ma3_d.Cross(20) == -1 + yali := ma3_d.Cross(80.5) == 1 + m.signal = float64(0) + if yali { + m.signal = -1 + } else if zhicheng { + m.signal = 1 + } else { + return + } + if m.bigDirt != 0 { + openOrder(s, m, m.signal) + } + closeOrders(s, m.signal) + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + odDirt := float64(1) + if od.Short { + odDirt = -1 + } + m, _ := s.More.(*MoreType) + c := s.Env.Close.Get(0) + basePrice := max(od.InitPrice, m.maBig) + if odDirt > 0 { + basePrice = min(od.InitPrice, m.maBig) + } + stopPrice := basePrice * (1 - odDirt*stopLossRate) + od.SetStopLoss(&ormo.ExitTrigger{Price: stopPrice}) + priceBk := c - stopPrice + if odDirt*priceBk < 0 { + // 触发止损,立刻卖出 + return &strat.ExitReq{ + Tag: core.ExitTagStopLoss, + OrderID: od.ID, + } + } + if m.signal == 0 || m.signal == odDirt { + // 没有信号,或顺趋势信号,退出 + return nil + } + // 出现逆趋势信号 + entPrice := od.InitPrice + if od.Enter.Price != 0 { + entPrice = od.Enter.Price + } + chgRate := c/entPrice - 1 + if odDirt*chgRate >= goodMaxRate { + // 价格盈利超过阈值,卖出止盈 + return &strat.ExitReq{ + Tag: "stop_win", + OrderID: od.ID, + } + } + return nil + }, + } +} diff --git a/org/digo/v2.go b/org/digo/v2.go new file mode 100644 index 0000000..c29a801 --- /dev/null +++ b/org/digo/v2.go @@ -0,0 +1,181 @@ +package digo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type MoreV2 struct { + bigEma199 float64 +} + +/* +# 趋势判断 +趋势多头 = EMA360 > 日线EMA199 且 当前价格 > EMA360 +趋势空头 = EMA360 < 日线EMA199 且 当前价格 < EMA360 + +# 震荡判断 +震荡偏多 = min(EMA360, 当前价格) > 日线EMA199 且 当前价格 < EMA360 +震荡偏空 = max(EMA360, 当前价格) < 日线EMA199 且 当前价格 > EMA360 + +如果是震荡偏多市场: + 在支撑位:开多单(1倍杠杆),平掉空单 + 在压力位:开小仓位空单(0.5倍杠杆),平掉多单 + +如果是震荡偏空市场: + 在支撑位:开小仓位多单(0.5倍杠杆),平掉空单 + 在压力位:开空单(1倍杠杆),平掉多单 + +止损逐bar更新: + 震荡偏空/震荡偏多: + 多单止损:min(ema360, bigEma199)下方2个点 + 空单止损:max(ema360, bigEma199)上方2个点 +*/ + +func v2(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义策略参数 + smoothK := int(pol.Def("smoothK", 13, core.PUniform(8, 20))) + smoothD := int(pol.Def("smoothD", 3, core.PUniform(2, 5))) + //bSmaLen := int(pol.Def("bSmaLen", 9, core.PUniform(5, 15))) + //bSmaDev := 2.0 + slRate := 0.02 + + return &strat.TradeStrat{ + WarmupNum: 600, // 需要足够的预热周期来计算EMA360 + EachMaxLong: 1, // 最多同时持有1笔多单 + EachMaxShort: 1, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreV2{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 360}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*MoreV2) + c := e.Close.Get(0) + + ema360 := ta.EMA(e.Close, 360).Get(0) + + var trend = 0 // 2 多头, 1 震荡偏多, 0 不确定, -1 震荡偏空 -2 空头 + if c > ema360 && ema360 > m.bigEma199 { + // 多头排列 + trend = 2 + } else if c < ema360 && ema360 < m.bigEma199 { + // 空头排列 + trend = -2 + } else if min(c, ema360) > m.bigEma199 && c < ema360 { + // 震荡偏多, + trend = 1 + } else if max(c, ema360) < m.bigEma199 && c > ema360 { + // 震荡偏空 + trend = -1 + } + + _, d, _ := ta.KDJBy(e.High, e.Low, e.Close, smoothK, 3, smoothD, "sma") + dSMA := ta.SMA(d, 3) + + // 判断支撑压力 + isUpp := dSMA.Cross(20) == -1 // crossunder + isDww := dSMA.Cross(80.5) == 1 // crossover + + // 计算布林带 + //bbCols := ta.BBANDS(e.Close, bSmaLen, bSmaDev, bSmaDev).Cols + //bbUpper, bbLower := bbCols[0].Get(0), bbCols[2].Get(0) + //h := e.High.Get(0) + + isYali := isDww // && h > bbUpper*1.08 + isZhicheng := isUpp // && c < bbLower*1.08 + + // 计算止损 + slLong, slShort := 0.0, 0.0 + if trend == 1 || trend == -1 { + slLong = min(ema360, m.bigEma199) * (1 - slRate) + slShort = max(ema360, m.bigEma199) * (1 + slRate) + for _, od := range s.LongOrders { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: slLong, + }) + } + for _, od := range s.ShortOrders { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: slShort, + }) + } + } + + // 开仓和平仓逻辑 + longRate, shortRate := 1.0, 1.0 + if trend == 1 { + // 震荡偏多 + longRate, shortRate = 1, 0.5 + } else if trend == -1 { + // 震荡偏空 + longRate, shortRate = 0.5, 1 + } + if trend == 1 || trend == -1 { + if isZhicheng { + // 支撑位开多 + s.OpenOrder(&strat.EnterReq{ + Tag: "longZC1", + CostRate: longRate, + StopLoss: slLong, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "zc1", + Dirt: core.OdDirtShort, + }) + } else if isYali { + //_ = shortRate + // 压力位半仓开空 + s.OpenOrder(&strat.EnterReq{ + Tag: "shortYL1", + CostRate: shortRate, + Short: true, + StopLoss: slShort, + }) + s.CloseOrders(&strat.ExitReq{ + Tag: "yl1", + Dirt: core.OdDirtLong, + }) + } + } + //if isZhicheng && trend == 2 && s.OrderNum == 0 { + // // 支撑位开多 + // s.OpenOrder(&strat.EnterReq{ + // Tag: "support_long", + // Short: false, + // }) + //} else if isYali && trend == 2 && s.OrderNum > 0 { + // // 压力位平多 + // s.CloseOrders(&strat.ExitReq{ + // Tag: "resistance_close", + // EnterTag: "support_long", + // Dirt: core.OdDirtLong, + // }) + //} + //if trend == -2 && isYali && s.OrderNum == 0 { + // // 顺势空,压力位开空 + // s.OpenOrder(&strat.EnterReq{ + // Tag: "resi_short", + // Short: true, + // }) + //} else if trend == -2 && isZhicheng && s.OrderNum > 0 { + // s.CloseOrders(&strat.ExitReq{ + // Tag: "supp_close", + // EnterTag: "resi_short", + // Dirt: core.OdDirtShort, + // }) + //} + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*MoreV2) + m.bigEma199 = ta.EMA(e.Close, 199).Get(0) + }, + } +} diff --git a/org/digo/v4.go b/org/digo/v4.go new file mode 100644 index 0000000..681f176 --- /dev/null +++ b/org/digo/v4.go @@ -0,0 +1,112 @@ +package digo + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +# 趋势判断 +趋势多头 = EMA360 > 日线EMA199 且 当前价格 > EMA360 +趋势空头 = EMA360 < 日线EMA199 且 当前价格 < EMA360 + +# 震荡判断 +震荡偏多 = min(EMA360, 当前价格) > 日线EMA199 且 当前价格 < EMA360 +震荡偏空 = max(EMA360, 当前价格) < 日线EMA199 且 当前价格 > EMA360 + +如果是震荡偏多市场: + 在支撑位:开多单(1倍杠杆),平掉空单 + 在压力位:开小仓位空单(0.5倍杠杆),平掉多单 + +如果是震荡偏空市场: + 在支撑位:开小仓位多单(0.5倍杠杆),平掉空单 + 在压力位:开空单(1倍杠杆),平掉多单 + +止损逐bar更新: + 震荡偏空/震荡偏多: + 多单止损:min(ema360, bigEma199)下方2个点 + 空单止损:max(ema360, bigEma199)上方2个点 +*/ + +func v4(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义策略参数 + smoothK := int(pol.Def("smoothK", 13, core.PUniform(8, 20))) + smoothD := int(pol.Def("smoothD", 3, core.PUniform(2, 5))) + //bSmaLen := int(pol.Def("bSmaLen", 9, core.PUniform(5, 15))) + //bSmaDev := 2.0 + slRate := 0.05 + tpRate := 0.05 + + return &strat.TradeStrat{ + WarmupNum: 600, // 需要足够的预热周期来计算EMA360 + EachMaxLong: 1, // 最多同时持有1笔多单 + EachMaxShort: 1, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreV2{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 360}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*MoreV2) + c := e.Close.Get(0) + + ema360 := ta.EMA(e.Close, 360).Get(0) + ema169 := ta.EMA(e.Close, 169).Get(0) + + var isLong = false + if ema169 > m.bigEma199 && c > ema360 { + //顺势偏多 + isLong = true + } + // 收盘价偏离ema169的百分比 + devRangePct := math.Abs(c-ema169) * 100 / ema169 + + _, d, _ := ta.KDJBy(e.High, e.Low, e.Close, smoothK, 3, smoothD, "sma") + dSMA := ta.SMA(d, 3) + + // 判断支撑压力 + isUpp := dSMA.Cross(20) == -1 // crossunder + isDww := dSMA.Cross(80.5) == 1 // crossover + + isYali := isDww // && h > bbUpper*1.08 + isZhicheng := isUpp // && c < bbLower*1.08 + + // 计算止损 + slLong := ema169 * (1 - slRate) + slShort := ema169 * (1 + slRate) + for _, od := range s.LongOrders { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: slLong, + }) + } + for _, od := range s.ShortOrders { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: slShort, + }) + } + + _ = isYali + + if isLong && isZhicheng && devRangePct < 5 { + s.OpenOrder(&strat.EnterReq{ + Tag: "longZC1", + StopLoss: slLong, + TakeProfit: c * (1 + tpRate), + }) + } + + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*MoreV2) + m.bigEma199 = ta.EMA(e.Close, 199).Get(0) + }, + } +} diff --git a/org/fmz/BearMACD.go b/org/fmz/BearMACD.go new file mode 100644 index 0000000..7c72fcc --- /dev/null +++ b/org/fmz/BearMACD.go @@ -0,0 +1,64 @@ +package fmz + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +https://www.fmz.com/strategy/426836 +该策略专注于熊市下跌行情中的做空交易,同时确保资产处于大周期下行通道中,然后在进一步下跌后止盈退出。 +测试在48个品种,210101-250101, 1h,收益296%,回撤17%,夏普7.42 + +空头优化620轮,取25%位置,maS: 7 maSLen: 191 rate: 2.35 +多头优化182轮,取22%位置,maL: 1 maLLen: 219 rate: 2.33 + +后续优化方向: +止损止盈的倍率,开单倍率 +*/ + +func BearMACD(p *config.RunPolicyConfig) *strat.TradeStrat { + // short: 7 191 2.35 + // long: 1 219 2.33 + maSType := com.DefMaType(p, "maS", com.MaALMA) + maSLen := int(p.Def("maSLen", 191, core.PNormF(30, 800, 230, 0))) + maLType := com.DefMaType(p, "maL", com.MaEMA) + maLLen := int(p.Def("maLLen", 219, core.PNormF(30, 800, 230, 0))) + rate := p.Def("rate", 2.35, core.PNormF(0.5, 3, 1.6, 0)) + mc1 := int(math.Round(12 * rate)) + mc2 := int(math.Round(26 * rate)) + mc3 := int(math.Round(9 * rate)) + return &strat.TradeStrat{ + WarmupNum: max(maSLen, maLLen), + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + maS := com.GetMA(e.Close, e.Volume, maSType, maSLen).Get(0) + maL := com.GetMA(e.Close, e.Volume, maLType, maLLen).Get(0) + macd, signal := ta.MACD(e.Close, mc1, mc2, mc3) + macdX := macd.Cross(signal) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + if s.OrderNum > 0 { + return + } + if macdX == -1 && c < maS { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + StopLossVal: atr * 1.3, + TakeProfitVal: atr * 4, + }) + } else if macdX == 1 && c > maL { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: atr * 1.3, + TakeProfitVal: atr * 6, + }) + } + }, + } +} diff --git a/org/fmz/ComboMaBear.go b/org/fmz/ComboMaBear.go new file mode 100644 index 0000000..9beb2c6 --- /dev/null +++ b/org/fmz/ComboMaBear.go @@ -0,0 +1,91 @@ +package fmz + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "strconv" +) + +/* +https://www.fmz.com/strategy/429466 +本策略结合了均线先行指标和熊强力指标,形成短期看跌方向信号的组合策略。均线先行指标判断趋势,熊强力指标确定做空时机。策略适合短线操作,追踪市场调整行情。 + +在48个品种,210101-241101上,表现很差 +4h,1h,15m均震荡严重 +*/ + +type CMA struct { + maPos float64 + bpPos float64 +} + +func ComboMaBear(p *config.RunPolicyConfig) *strat.TradeStrat { + bpShort := 4.0 + bpLong := 0.3 + return &strat.TradeStrat{ + WarmupNum: 50, + OnStartUp: func(s *strat.StratJob) { + s.More = &CMA{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + o, h, l, c, c1 := e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0), e.Close.Get(1) + m, _ := s.More.(*CMA) + ma := ta.EMA(e.Close, 14).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + hh := max(e.High.Get(0), e.High.Get(1)) + ll := min(e.Low.Get(0), e.Low.Get(1)) + nXS := hh + if ll > ma || hh < ma { + nXS = ll + } + xsDiff := nXS - c1 + if xsDiff < 0 { + m.maPos = 1 + } else if xsDiff > 0 { + m.maPos = -1 + } + + var bp float64 + solRate := math.Abs(c-o) / (h - l) + var isBear bool + if solRate < 0.1 { + // 实体不足10%,看上下影线 + isBear = h-c > c-l + } else { + // 实体较大,直接判断 + isBear = c < o + } + if isBear { + // 阴线取全部 + bp = h - l + } else { + // 阳线取较大影线 + bp = max(h-c, c-l) + } + bpRate := bp / atr + if bpRate > bpShort { + m.bpPos = -1 + } else if bpRate < bpLong { + m.bpPos = 1 + } + + openDirt := 0 + if m.maPos == 1 && m.bpPos == 1 { + openDirt = 1 + } else if m.maPos == -1 && m.bpPos == -1 { + openDirt = -1 + } else if s.OrderNum > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + if openDirt != 0 && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(openDirt), + Short: openDirt < 0, + }) + } + }, + } +} diff --git a/org/fmz/common.go b/org/fmz/common.go new file mode 100644 index 0000000..47382ab --- /dev/null +++ b/org/fmz/common.go @@ -0,0 +1,10 @@ +package fmz + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("fmz", map[string]strat.FuncMakeStrat{ + "ComboMaBear": ComboMaBear, + "BearMACD": BearMACD, + }) +} diff --git a/org/ft/BBMod2.go b/org/ft/BBMod2.go new file mode 100644 index 0000000..a742367 --- /dev/null +++ b/org/ft/BBMod2.go @@ -0,0 +1,177 @@ +package ft + +import ( + "github.com/banbox/banbot/orm/ormo" + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "gonum.org/v1/gonum/floats" +) + +/* +https://strat.ninja/overview.php?strategy=BBMod_2 +BBMod1 modified from BB_RPB_TSL ( https://github.com/jilv220/BB_RPB_TSL ) +@author jilv220 + +210101-241101 48品种 5m 收益570%,回撤9.3% +*/ + +type BBModSta struct { + bigEmaUp bool + atrBase float64 + tmpHolds map[int64]bool +} + +func BBMod2(pol *config.RunPolicyConfig) *strat.TradeStrat { + //emaDfRate := pol.Def("emaDf", 4, core.PNorm(1.5, 6)) + lenAtr := int(pol.Def("atr", 29, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + DrawDownExit: true, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 100}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &BBModSta{ + tmpHolds: make(map[int64]bool), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*BBModSta) + c, c1 := e.Close.Get(0), e.Close.Get(1) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + m.atrBase = atrBase + closeDelta := math.Abs(c - e.Close.Get(1)) + hlc3 := ta.AvgPrice(e) + _, bbMidC, bbLowC := ta.BBANDS(hlc3, 20, 2, 2) + bbMid, bbLow := bbMidC.Get(0), bbLowC.Get(0) + sma9 := ta.SMA(e.Close, 9).Get(0) + //sma15 := ta.SMA(e.Close, 15).Get(0) + // + //cti := ta.CTI(e.Close, 20).Get(0) + + ema8 := ta.EMA(e.Close, 8).Get(0) + ema12S := ta.EMA(e.Close, 12) + ema12 := ema12S.Get(0) + ema16 := ta.EMA(e.Close, 16).Get(0) + //ema20 := ta.EMA(e.Close, 20).Get(0) + ema24 := ta.EMA(e.Close, 24).Get(0) + ema26S := ta.EMA(e.Close, 26) + ema26 := ema26S.Get(0) + ema50 := ta.EMA(e.Close, 50).Get(0) + ema200 := ta.EMA(e.Close, 200).Get(0) + ewo := (ema50 - ema200) / atrBase + + rsi := ta.RSI(e.Close, 14).Get(0) + rsiFast := ta.RSI(e.Close, 4).Get(0) + rsiSlowS := ta.RSI(e.Close, 20) + rsiSlow := rsiSlowS.Get(0) + //rsi84 := ta.RSI(e.Close, 84).Get(0) + //rsi112 := ta.RSI(e.Close, 112).Get(0) + //crsi := ta.CRSIBy(e.Close, 3, 2, 100, 1).Get(0) + + //heiKA := ta.HeikinAshi(e).Cols + //heiHlc3 := ta.HLC3(heiKA[1], heiKA[2], heiKA[3]) + //bb40Cols := ta.BBANDS(heiHlc3, 40, 2, 2).Cols + //bb40Mid, bb40Low := bb40Cols[1].Get(0), bb40Cols[2].Get(0) + //bb40Low1 := bb40Cols[2].Get(1) + //bb40Delta := bb40Mid - bb40Low + //tail := math.Abs(heiKA[3].Get(0) - heiKA[2].Get(0)) + + //_, vwapLow := vwapB(hlc3, e.Volume, 20, 1) + //tcpPct4 := ta.Highest(e.Open, 4).Get(0)/c - 1 + + tryTmpHold := c > max(sma9, ema24-atrBase*0.2, bbMid) && rsi > 50 && rsiFast > rsiSlow + holdTags := make(map[string]bool) + for _, od := range s.LongOrders { + holdTags[od.EnterTag] = true + if od.ProfitRate < atrBase*4 { + if tryTmpHold { + m.tmpHolds[od.ID] = true + } + } else if _, ok := m.tmpHolds[od.ID]; ok { + delete(m.tmpHolds, od.ID) + } + } + if len(m.tmpHolds) > 0 && c < floats.Min(e.Low.Range(1, 4)) { + for oid := range m.tmpHolds { + s.CloseOrders(&strat.ExitReq{Tag: "sellDrop", OrderID: oid}) + } + m.tmpHolds = make(map[int64]bool) + } + + _ = c1 + + tag := "" + ema26_12rate := (ema26 - ema12) / atrBase + ema26_12rateP := (ema26S.Get(1) - ema12S.Get(1)) / atrBase + closeDeltaRate := closeDelta / atrBase + if ema26_12rateP > 2 && c < bbLow-atrBase*0.2 { + if ema26_12rate > 5 && closeDeltaRate > 3.5 { + //use origin bb_rpb_tsl value + tag = "up2" + } + // 略不稳定 + //else if ema26_12rate > emaDfRate && closeDeltaRate > 2.4 { + // // from NFI next gen, credit goes to @iterativ + // tag = "up1" + //} + } + if rsiFast < 44 && ewo > -10 && c < min(ema8-atrBase*13, ema16-atrBase*6.4) && rsi < 23 { + // from sma offset + //tag = "ewo" + tag = "smo" + } + // 下面的要么不稳定,要么几乎没有订单 + //} else if ema26_12rate > 4.8 && ema26_12rateP > 2 && c < ema20+atrBase*2.6 && rsi < 21 && crsi > 10 && closeDeltaRate > 2.4 { + // tag = "localDip" + //} else if rsiSlow < rsiSlowS.Get(1) && rsiFast < 46 && rsi > 19 && c < sma15-atrBase*11 && cti < -0.86 { + // tag = "nfi32" + //} else if m.bigEmaUp && bb40Delta > atrBase*11 && closeDeltaRate > 2 && tail < bb40Delta*0.5 && + // c < bb40Low1 && c <= c1 && c > ema12-atrBase*17 { + // tag = "nfix39" + //} + //else if c < vwapLow.Get(0) && tcpPct4 > atrBase*8 && cti < -0.8 && rsi < 35 && rsi84 < 60 && rsi112 < 60 { + // tag = "vwap" + //} + + if _, hasOld := holdTags[tag]; tag != "" && !hasOld { + s.OpenOrder(&strat.EnterReq{Tag: tag}) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*BBModSta) + ema200 := ta.EMA(e.Close, 200) + cur, p12, p24 := ema200.Get(0), ema200.Get(12), ema200.Get(24) + m.bigEmaUp = cur > p12 && p12 > p24 + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + m, _ := s.More.(*BBModSta) + if maxChg > m.atrBase*10 { + return 0.15 + } else if maxChg > m.atrBase*4 { + return 0.2 + } + return 0 + }, + } +} + +/* +vwapB 应传入hlc3和qtpylib.rolling_vwap中保持一致 + +return [vwap, low] +*/ +func vwapB(p, v *ta.Series, period int, rate float64) (*ta.Series, *ta.Series) { + vwap := ta.VWMA(p, v, period) + dev := ta.StdDev(vwap, period) + return vwap, vwap.Add(dev.Mul(-rate)) +} diff --git a/org/ft/BBMod7.go b/org/ft/BBMod7.go new file mode 100644 index 0000000..fe01ee4 --- /dev/null +++ b/org/ft/BBMod7.go @@ -0,0 +1,224 @@ +package ft + +import ( + "math" + + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://strat.ninja/overview.php?strategy=BBMod_7 +48品种,210101-241101,盈利327%,回撤8.83% +*/ + +type BBMod7Sta struct { + bigEma200 *ta.Series + bigRoc float64 + atrBase float64 + bigBBWid2 float64 + bigBBMid float64 + bigT3 float64 +} + +func BBMod7(p *config.RunPolicyConfig) *strat.TradeStrat { + //lowRate := p.Def("lowRate", 6, core.PNorm(1, 3.5)) + //highRate := p.Def("highRate", 15, core.PNorm(0.5, 2.5)) + lenAtr := int(p.Def("atr", 13.35, core.PNorm(7, 25))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 300}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &BBMod7Sta{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*BBMod7Sta) + o, c := e.Open.Get(0), e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + m.atrBase = atrBase + + //sma15 := ta.SMA(e.Close, 15).Get(0) + //sma28 := ta.SMA(e.Close, 28).Get(0) + + //tr := ta.TR(e.High, e.Low, e.Close) + //rangeMa28 := ta.SMA(tr, 28).Get(0) + //kcUpp28 := sma28 + rangeMa28 + //kcLow28 := sma28 - rangeMa28 + + closeDelta := math.Abs(e.Close.Get(0) - e.Close.Get(1)) + + //rmi := ta.RMI(e.Close, 8, 4).Get(0) + //cci := ta.CCI(e.Close, 25).Get(0) + //srsiK := ta.StochRSI(e.Close, 15, 20, 2, 2).Cols[0].Get(0) + + hlc3 := ta.HLC3(e.High, e.Low, e.Close) + bb2Up, _, bb2Dn := ta.BBANDS(hlc3, 20, 2, 2) + bb2Upp, bb2Low := bb2Up.Get(0), bb2Dn.Get(0) + //bb2Wid := (bb2Upp - bb2Low) / atrBase + //bb2Mid := bb2Cols[1].Get(0) + + //bb3Cols := ta.BBANDS(hlc3, 20, 3, 3).Cols + //bb3Low := bb3Cols[2].Get(0) + //bbDelta := (bb2Cols[2].Get(0) - bb3Low) / atrBase + + //ema13 := ta.EMA(e.Close, 13).Get(0) + ema8 := ta.EMA(e.Close, 8).Get(0) + //ema16 := ta.EMA(e.Close, 16).Get(0) + //ema20 := ta.EMA(e.Close, 20).Get(0) + ema50 := ta.EMA(e.Close, 50).Get(0) + //ema100 := ta.EMA(e.Close, 100).Get(0) + ema200 := ta.EMA(e.Close, 200).Get(0) + ewo := (ema50 - ema200) / atrBase + ema12S := ta.EMA(e.Close, 12) + ema26S := ta.EMA(e.Close, 26) + ema12, ema26 := ema12S.Get(0), ema26S.Get(0) + ema12P, ema26P := ema12S.Get(1), ema26S.Get(1) + + //hh20 := ta.Highest(e.High, 20).Get(0) + //ll20 := ta.Lowest(e.Low, 20).Get(0) + //sma20 := ta.SMA(e.Close, 20).Get(0) + //linRegSrc := e.Close.To("_linRegSrc", 20) + //if !linRegSrc.Cached() { + // linRegSrc.Append(c - ((hh20+ll20)/2+sma20)/2) + //} + //linReg20 := ta.LinReg(linRegSrc, 20) + + //rsi := ta.RSI(e.Close, 14).Get(0) + //rsiFast := ta.RSI(e.Close, 4).Get(0) + //rsiSlowS := ta.RSI(e.Close, 20) + //rsiSlow := rsiSlowS.Get(0) + //crsi := ta.CRSI(e.Close, 3, 2, 100).Get(0) + + //lowWid := atrBase * lowRate + //highWid := atrBase * highRate + //emaEnt := c < ema8-lowWid && c < ema16+highWid + // + //volMa12 := ta.SMA(e.Volume, 12).Get(1) + //volMa24 := ta.SMA(e.Volume, 24).Get(1) + + cti := ta.CTI(e.Close, 20).Get(0) + t3 := T3(e.Close, 5).Get(0) + fastD, _, fastK := ta.KDJBy(e.High, e.Low, e.Close, 5, 3, 3, "sma") + + willR := ta.WillR(e, 14).Get(0) + + //kdjRawX := fastK.Cross(fastD) + // + //adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + // + //vwapLow, _ := vwapB(hlc3, e.Volume, 20, 1) + // + //tcpPct := topPctChgDca(e.Open, e.Close, 4) + + _, _, _ = o, bb2Upp, fastD + + if fastK.Get(0) > 75 && s.OrderNum > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "fastk"}) + return + } + + for _, od := range s.LongOrders { + profit := (c - od.InitPrice) / atrBase + sl := od.GetStopLoss() + if profit > 15 { + sl.Price = od.InitPrice + atrBase*15 + } else if profit > 6 { + sl.Price = od.InitPrice + atrBase*6 + } + } + + tag := "" + if ewo < -11 && m.bigBBMid >= m.bigT3 && t3 <= ema8-atrBase*6 && cti < -0.5 && willR < -60 { + //tag = "gumbo" + tag = "gb" + } else if ema26-ema12 > atrBase*5 && ema26P-ema12P > atrBase*2 && + c < bb2Low-atrBase*0.6 && closeDelta > atrBase*3 { + //tag = "uptrend" + tag = "up" + } + //else if rmi < 35 && cci <= -133 && srsiK < 25 && bbDelta > 4.5 && bb2Wid > 18 && closeDelta > atrBase*3 && + // c < bb3Low-atrBase*0.2 { + // // 略不稳定,可选 + // tag = "BinH" + //} + // 下面的要么不稳定,要么几乎没有订单 + // else if bb2Low < kcLow28 && bb2Upp > kcUpp28 && linReg20.Get(2) > linReg20.Get(1) && + // linReg20.Get(1) < linReg20.Get(0) && linReg20.Get(0) < 0 && c < ema13-atrBase*6 && + // ewo < 0 && willR < -50 { + // tag = "sqzmom" + //} else if m.bigEma200 != nil && m.bigEma200.Get(0) > m.bigEma200.Get(12) && + // m.bigEma200.Get(12) > m.bigEma200.Get(24) && + // rsiFast < 45 && emaEnt && ewo > 4.18 && rsi < 35 { + // tag = "ewo2" + //} else if ema100 < ema200+atrBase*16 && bb2Wid > 10 && c < bb2Mid && volMa12 < volMa24 && cti < -0.5 && + // willR < -60 { + // // 亏损,效果差 + // //tag = "deadfish" + //} else if m.bigRoc < 10 && m.bigBBWid2 < 60 && o < ema8-atrBase*6 && kdjRawX == 1 && + // fastK.Get(0) < 20 && fastD.Get(0) < 20 && adx > 30 && ewo > 7 && cti < -0.5 && willR < -60 { + // tag = "cofi" + //} else if ema26-ema12 > atrBase*5 && ema26P-ema12P > atrBase*2 && c < ema20-atrBase*11 && rsi < 28 && + // crsi > 10 && closeDelta > atrBase*3 { + // tag = "localDip" + //} else if rsiFast < 45 && c < ema8-atrBase*11 && ewo > -5.585 && c < ema16+atrBase*16 && rsi < 35 { + // tag = "ewo" + //} else if rsiSlow < rsiSlowS.Get(1) && rsiFast < 46 && rsi > 19 && c < sma15-atrBase*11 && cti < -0.86 { + // tag = "nfi32" + //} else if c < vwapLow.Get(0) && tcpPct > 4 && cti < -0.86 && rsi < 35 { + // tag = "vwap" + //} + if tag != "" && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + StopLossVal: atrBase * 20, + }) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*BBMod7Sta) + m.bigEma200 = ta.EMA(e.Close, 200) + m.bigRoc = ta.ROC(e.Close, 9).Get(0) + + hlc3 := ta.HLC3(e.High, e.Low, e.Close) + bb2Up, bb2Mid, bb2Dn := ta.BBANDS(hlc3, 20, 2, 2) + bb2Upp, bb2Low := bb2Up.Get(0), bb2Dn.Get(0) + m.bigBBWid2 = (bb2Upp - bb2Low) / m.atrBase + m.bigBBMid = bb2Mid.Get(0) + + m.bigT3 = T3(e.Close, 5).Get(0) + }, + } +} + +/* +T3 Average by HPotter on Tradingview +https://www.tradingview.com/script/qzoC9H1I-T3-Average/ +*/ +func T3(p *ta.Series, period int) *ta.Series { + res := p.To("_t3", period) + if res.Cached() { + return res + } + xe1 := ta.EMA(p, period) + xe2 := ta.EMA(xe1, period) + xe3 := ta.EMA(xe2, period) + xe4 := ta.EMA(xe3, period) + xe5 := ta.EMA(xe4, period) + xe6 := ta.EMA(xe5, period) + b := 0.7 + c1 := -b * b * b + c2 := 3*b*b + 3*b*b*b + c3 := -6*b*b - 3*b - 3*b*b*b + c4 := 1 + 3*b + b*b*b + 3*b*b + val := c1*xe6.Get(0) + c2*xe5.Get(0) + c3*xe4.Get(0) + c4*xe3.Get(0) + return res.Append(val) +} diff --git a/org/ft/BinHModWhiteHOV0.go b/org/ft/BinHModWhiteHOV0.go new file mode 100644 index 0000000..9f5f902 --- /dev/null +++ b/org/ft/BinHModWhiteHOV0.go @@ -0,0 +1,80 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +https://strat.ninja/overview.php?strategy=BinHModWhiteHOV0 + +收益太低,210101-241101,48品种,15%收益,回撤3.4% +*/ + +func BinHModWhiteHOV0(pol *config.RunPolicyConfig) *strat.TradeStrat { + bbWidthVal := pol.Def("bb_width", 13.5, core.PNorm(1, 15)) + bbDeltaDiff := pol.Def("bb_delta_diff", 4.4, core.PNorm(0.2, 12)) + bbTailDiff := pol.Def("bb_tail_diff", 0.132, core.PNorm(0.05, 0.450)) + bbBottomDeltaDiff := pol.Def("bb_bottom_delta_diff", 1.1, core.PNorm(0.8, 8)) + bbTopDeltaDiff := pol.Def("bb_top_delta_diff", 2.5, core.PNorm(1, 18)) + emaRise := int(pol.Def("ema_rise", 54, core.PNorm(6, 60))) + emaSlowPeriod := int(pol.Def("ema_slow", 160, core.PNorm(140, 300))) + emaTrendPeriod := int(pol.Def("ema_trend", 90, core.PNorm(50, 110))) + ewoBaseLen := int(pol.Def("ewo_base", 70, core.PNorm(50, 150))) + ewoStepLen := int(pol.Def("ewo_step", 90, core.PNorm(10, 150))) + ewoHigh := pol.Def("ewo_high", 6, core.PNorm(1.0, 12.0)) + highOffset := pol.Def("high_offset", 0.0, core.PNorm(0.0, 4)) + lenAtr := 29 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + bottom := e.Open.Min(e.Close) + up := e.Open.Max(e.Close) + bbUp, bbMd, bbDn := ta.BBANDS(e.Close, 40, 2.0, 2.0) + bbUpp, bbMid, bbLow := bbUp.Get(0), bbMd.Get(0), bbDn.Get(0) + bbWidthRaw := e.Close.To("_bbWid", 40) + if !bbWidthRaw.Cached() { + bbWidthRaw.Append((bbUpp - bbLow) / atrBase) + } + space := ta.SMA(bbWidthRaw, 60).Get(0) + bbDelta := bbMid - bbLow + bottomDelta := math.Abs(bottom.Get(0) - bottom.Get(1)) + topDelta := math.Abs(up.Get(0) - up.Get(1)) + tail := bottom.Get(0) - e.Low.Get(0) + + emaTrend := ta.EMA(e.Close, emaTrendPeriod) + emaSlower := ta.EMA(e.Close, emaSlowPeriod) + ema1 := ta.EMA(e.Close, ewoBaseLen).Get(0) + ema2 := ta.EMA(e.Close, ewoBaseLen+ewoStepLen).Get(0) + ewo := (ema1 - ema2) / atrBase + + buyProtection1 := emaTrend.Get(0) > emaSlower.Get(0) + buyProtection2 := emaSlower.Get(0) > emaSlower.Get(emaRise) && + emaSlower.Get(emaRise) > emaSlower.Get(emaRise*2) + buyProtection3 := ewo > ewoHigh + + if buyProtection1 && buyProtection2 && buyProtection3 && + space > bbWidthVal && s.OrderNum == 0 && + bottom.Get(0) < math.Min(bottom.Get(1), bbDn.Get(1)) && + bbDelta > atrBase*bbDeltaDiff && + tail < bbDelta*bbTailDiff && + bottomDelta > atrBase*bbBottomDeltaDiff && + topDelta < atrBase*bbTopDeltaDiff && + bbDn.Get(1) > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "buy"}) + } + + if e.Close.Get(0) > bbMid+atrBase*highOffset { + s.CloseOrders(&strat.ExitReq{Tag: "sell"}) + } + }, + } +} diff --git a/org/ft/BinHV27.go b/org/ft/BinHV27.go new file mode 100644 index 0000000..fa3a5d6 --- /dev/null +++ b/org/ft/BinHV27.go @@ -0,0 +1,101 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://strat.ninja/overview.php?strategy=BinHV27_long +亏损,无效策略 +*/ + +func BinHV27(p *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := int(p.Def("atr", 29, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + ema60 := ta.EMA(e.Close, 60).Get(0) + ema120 := ta.EMA(e.Close, 120).Get(0) + sma120 := ta.SMA(e.Close, 120) + sma240 := ta.SMA(e.Close, 240) + + plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14) + minDiMa := ta.EMA(minusDi, 25).Get(0) + + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + + rsi := ta.RSI(e.Close, 5) + rsiMa := ta.EMA(rsi, 5).Get(0) + + minDiOk := minusDi.Get(0) > minDiMa + rsiOk := rsi.Get(0) >= rsi.Get(1) + bigTrendChg := sma120.Get(0)-sma240.Get(0) > sma120.Get(1)-sma240.Get(1) + bigContiUp := sma240.Get(0) > sma240.Get(1) && sma240.Get(1) > sma240.Get(2) + bigUp := (sma120.Get(0) - sma240.Get(0)) > atrBase*0.7 + bigDown := !bigUp + bigTrendChgCfm := bigTrendChg && sma120.Get(1)-sma240.Get(1) > sma120.Get(2)-sma240.Get(2) + slowDown := sma120.Get(0)-sma120.Get(1) < sma120.Get(1)-sma120.Get(2) + + if s.OrderNum > 0 { + exitTag := "" + if !bigTrendChgCfm && !bigContiUp && c > min(ema60, ema120) && bigDown { + exitTag = "exit1" + } else if !bigTrendChgCfm && !bigContiUp && c > ema120 && (rsiMa > 75 || c > sma240.Get(0)) && bigDown { + exitTag = "exit2" + } else if !bigTrendChgCfm && c > ema120 && adx > 30 && rsiMa >= 80 && bigUp { + exitTag = "exit3" + } else if bigTrendChgCfm && !bigContiUp && slowDown && rsiMa >= 75 { + exitTag = "exit4" + } else if bigTrendChgCfm && minusDi.Get(0) < plusDi.Get(0) && c > ema60 { + exitTag = "exit5" + } + for _, od := range s.LongOrders { + profit := (c - od.InitPrice) / atrBase + if profit <= 3 && exitTag != "" { + s.CloseOrders(&strat.ExitReq{Tag: exitTag, OrderID: od.ID}) + continue + } + sl := od.GetStopLoss() + var newSl float64 + if profit > 16 { + newSl = c - atrBase*8 + } else if profit > 3 { + newSl = od.InitPrice + atrBase*profit*0.5 + } + if newSl > sl.Price { + sl.Price = newSl + } + } + } + + tag := "" + if c < min(ema120, ema60) && minDiOk && rsiOk { + if !bigTrendChg && !bigContiUp && adx > 25 && bigDown && rsiMa <= 20 { + tag = "buy1" + } else if !bigTrendChg && bigContiUp && adx > 30 && bigDown && rsiMa <= 20 { + tag = "buy2" + } else if !bigContiUp && adx > 35 && bigUp && rsiMa <= 20 { + tag = "buy3" + } else if bigContiUp && adx > 30 && bigUp && rsiMa <= 25 { + tag = "buy4" + } + } + + if tag != "" && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + StopLossVal: atrBase * 16, + }) + } + }, + } +} diff --git a/org/ft/Cenderawasih.go b/org/ft/Cenderawasih.go new file mode 100644 index 0000000..1a62f5d --- /dev/null +++ b/org/ft/Cenderawasih.go @@ -0,0 +1,81 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func CenderaWasih3(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 超参数解析 + hmaLen := int(pol.Def("hmaLen", 42, core.PNorm(5, 100))) + hmaOffRate := pol.Def("hmaOffRate", 20, core.PNorm(2, 80)) + + zemaLen := int(pol.Def("zemaLen", 93, core.PNorm(5, 100))) + zemaHighRate := pol.Def("zemaHighRate", 18, core.PNorm(1, 30)) + zemaLowRate := pol.Def("zemaLowRate", 24, core.PNorm(2, 50)) + + emaShortLen1 := int(pol.Def("emaShort1", 31, core.PNorm(5, 100))) + emaHighRate1 := pol.Def("emaHigh1", 0.0, core.PNorm(0, 30)) + emaShortLen2 := int(pol.Def("emaShort2", 91, core.PNorm(5, 100))) + emaHighRate2 := pol.Def("emaHigh2", 22.5, core.PNorm(0, 40)) + emaShortLenL1 := int(pol.Def("emaShortL1", 87, core.PNorm(5, 100))) + emaLowRate1 := pol.Def("emaLow1", 32, core.PNorm(2, 60)) + emaShortLenL2 := int(pol.Def("emaShortL2", 98, core.PNorm(5, 100))) + emaLowRate2 := pol.Def("emaLow2", 6, core.PNorm(2, 60)) + + lenAtr := 29 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 200, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + hmaLow := ta.HMA(e.Close, hmaLen).Get(0) - atrBase*hmaOffRate + + // 计算ZEMA指标 + ema1 := ta.EMA(e.Close, zemaLen) + ema2 := ta.EMA(ema1, zemaLen) + zema := ema1.Add(ema1.Sub(ema2)).Get(0) + + // 计算ZEMA偏移量 + zemaHigh, zemaLow := zema+atrBase*zemaHighRate, zema-atrBase*zemaLowRate + + emaHigh1 := ta.EMA(e.Close, emaShortLen1).Get(0) + atrBase*emaHighRate1 + emaHigh2 := ta.EMA(e.Close, emaShortLen2).Get(0) + atrBase*emaHighRate2 + emaLow1 := ta.EMA(e.Close, emaShortLenL1).Get(0) - atrBase*emaLowRate1 + emaLow2 := ta.EMA(e.Close, emaShortLenL2).Get(0) - atrBase*emaLowRate2 + + // 买入条件 + if c < hmaLow && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "buy", + }) + } + + exitTag := "" + if c > zemaHigh { + exitTag = "zemaHigh" + } else if c < zemaLow { + exitTag = "zemaLow" + } else if c > emaHigh1 { + exitTag = "emaH1" + } else if c > emaHigh2 { + exitTag = "emaH2" + } else if c < emaLow1 { + exitTag = "emaL1" + } else if c < emaLow2 { + exitTag = "emaL2" + } + if exitTag != "" { + s.CloseOrders(&strat.ExitReq{ + Tag: exitTag, + }) + } + }, + } +} diff --git a/org/ft/Clucha.go b/org/ft/Clucha.go new file mode 100644 index 0000000..3dbeaeb --- /dev/null +++ b/org/ft/Clucha.go @@ -0,0 +1,108 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +https://strat.ninja/overview.php?strategy=ch +48个品种 210101-241101 289% 回撤14% +整体盈利,中间部分不稳定回撤 +*/ + +type CSSta struct { + ROC1h float64 +} + +func Clucha(pol *config.RunPolicyConfig) *strat.TradeStrat { + BbdeltaClose := pol.Def("BbdeltaClose", 3.5, core.PNorm(2, 10)) + BbdeltaTail := pol.Def("BbdeltaTail", 0.72235, core.PNorm(0.7, 1.2)) + CloseBblower := pol.Def("CloseBblower", 2.3, core.PNorm(0.2, 10)) + ClosedeltaClose := pol.Def("ClosedeltaClose", 1.8, core.PNorm(0.2, 10)) + Rocr1h := pol.Def("Rocr1h", 0.79492, core.PNorm(0.1, 1.0)) + sellFastx := pol.Def("sellFastx", 75, core.PUniform(50, 100)) + sellDeadfishBbWidth := pol.Def("sellDeadfishBbWidth", 10, core.PUniform(6, 100)) + lenAtr := 29 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 50, + EachMaxLong: 1, // 每个品种最多开1单 + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 200}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &CSSta{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*CSSta) + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + // 计算指标 + _, haHigh, haLow, haClose := ta.HeikinAshi(e) + hahlc3 := ta.HLC3(haHigh, haLow, haClose) + + _, mid, lower := ta.BBANDS(hahlc3, 40, 2, 2) + bbdelta := mid.Get(0) - lower.Get(0) + closedelta := math.Abs(haClose.Get(0) - haClose.Get(1)) + tail := math.Abs(haClose.Get(0) - haLow.Get(0)) + emaSlow := ta.EMA(haClose, 50).Get(0) + + hlc3 := ta.HLC3(e.High, e.Low, e.Close) + bb20Up, bb20Md, bb20Dn := ta.BBANDS(hlc3, 20, 2, 2) + bb20Wid := (bb20Up.Get(0) - bb20Dn.Get(0)) / atrBase + bb20Mid := bb20Md.Get(0) + + fastK := ta.Stoch(e.High, e.Low, e.Close, 5).Get(0) + + volMa12 := ta.SMA(e.Volume, 12).Get(1) + volMa24 := ta.SMA(e.Volume, 24).Get(1) + + // 买入条件 + isEwo := m.ROC1h > Rocr1h && + ((lower.Get(1) > 0 && + bbdelta > atrBase*BbdeltaClose && + closedelta > atrBase*ClosedeltaClose && + tail < bbdelta*BbdeltaTail && + haClose.Get(0) < lower.Get(1) && + haClose.Get(0) <= haClose.Get(1)) || + (haClose.Get(0) < emaSlow && haClose.Get(0) < lower.Get(0)-CloseBblower*atrBase)) + + if isEwo && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{Tag: "buy_ewo"}) + } + + // 自定义卖出逻辑 + for _, od := range s.LongOrders { + holdNum := e.BarCount(od.EnterAt) + profit := (c - od.InitPrice) / atrBase + exitTag := "" + if profit > 0 && fastK > sellFastx { + exitTag = "fastx" + } else if holdNum > 11.5 && profit > -2 && fastK > sellFastx { + exitTag = "kalah" + } else if holdNum > 288 && profit > -10 && fastK > sellFastx { + exitTag = "lama" + } else if profit < -10 && bb20Wid < sellDeadfishBbWidth && c > bb20Mid && volMa12 < volMa24 { + exitTag = "deadfish" + } + if exitTag != "" { + s.CloseOrders(&strat.ExitReq{OrderID: od.ID, Tag: exitTag}) + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*CSSta) + _, _, _, haClose := ta.HeikinAshi(e) + m.ROC1h = ta.ROC(haClose, 168).Get(0) + }, + } +} diff --git a/org/ft/HPSV7.go b/org/ft/HPSV7.go new file mode 100644 index 0000000..ca92580 --- /dev/null +++ b/org/ft/HPSV7.go @@ -0,0 +1,79 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +https://github.com/mkajnar/DailyBuyStrategy +历史提交中的策略 +*/ + +type hpsState struct { + downBar int // 连续下跌的bar数量 + chg3MA float64 // 间隔3个bar的价格变化率均值 +} + +func HPSV7Ultra(pol *config.RunPolicyConfig) *strat.TradeStrat { + // donchLen := int(pol.Def("donchLen", 23, core.PNorm(5, 50))) + rsiThs := pol.Def("rsiThs", 35, core.PNorm(10, 50)) + cciThs := pol.Def("cciThs", -100, core.PNorm(-200, -10)) + kickOffThs := -pol.Def("kickOffThs", 10, core.PNorm(1, 30)) + minRedNum := int(math.Round(pol.Def("minRedNum", 1, core.PNorm(0.7, 5.3)))) + chgRate1 := pol.Def("chgRate1", 0.3, core.PNorm(0.05, 2)) + chgRate2 := pol.Def("chgRate2", 2, core.PNorm(0.5, 5)) + + lenAtr := int(pol.Def("atr", 29, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + + chgAlpha := 0.35 + return &strat.TradeStrat{ + WarmupNum: 100, + OnStartUp: func(s *strat.StratJob) { + s.More = &hpsState{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*hpsState) + cc := e.Close + o, c := e.Open.Get(0), e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + if c < o { + m.downBar += 1 + } else { + m.downBar = 0 + } + chg3 := c/cc.Get(3) - 1 + if !math.IsNaN(chg3) { + m.chg3MA = m.chg3MA*(1-chgAlpha) + chg3*chgAlpha + } + + rsi := ta.RSI(e.Close, 14).Get(0) + cci := ta.CCI(e.Close, 20).Get(0) + // swingLow := cc.Get(1) < min(c, cc.Get(2)) + swingHigh := cc.Get(1) > max(c, cc.Get(2)) + + //donUpp := ta.Highest(e.High, donchLen).Get(0) + //donLow := ta.Lowest(e.Low, donchLen).Get(0) + //donMid := (donUpp + donLow) / 2 + // 原始策略入场要求close>donMid并记录到字段,但没有close= minRedNum && rsi < rsiThs && cci < cciThs && + chg3RateAbs > chgRate1 && chg3RateAbs < chgRate2 && s.OrderNum == 0 { + //(swingLow || c < donLow || c > donMid) && + s.OpenOrder(&strat.EnterReq{Tag: "long"}) + } + }, + } +} diff --git a/org/ft/MadV9HO.go b/org/ft/MadV9HO.go new file mode 100644 index 0000000..6ab4e21 --- /dev/null +++ b/org/ft/MadV9HO.go @@ -0,0 +1,144 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +https://strat.ninja/overview.php?strategy=MadV9HO +48个品种,210101-241101 收益360% 回撤5.9% 夏普11 +*/ + +type MadV9HOSta struct { + bigEma50 float64 + bigEma200 float64 + bigRsi float64 + bigSSLUp float64 + bigSSLDn float64 +} + +func MadV9HO(p *config.RunPolicyConfig) *strat.TradeStrat { + //bbLowRate1 := p.Def("bbLowRate1", 2, core.PNorm(0.5, 10)) + //bbLowRate2 := p.Def("bbLowRate2", 3.6, core.PNorm(0.0, 10)) + lenAtr := 29 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 200}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &MadV9HOSta{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*MadV9HOSta) + o, c := e.Open.Get(0), e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + hlc3 := ta.HLC3(e.High, e.Low, e.Close) + bbUpp, bbMid, bbLow := ta.BBANDS(hlc3, 20, 2, 2) + + //volMa := ta.SMA(e.Volume, 30) + + ema12S := ta.EMA(e.Close, 12) + ema26S := ta.EMA(e.Close, 26) + ema12, ema26 := ema12S.Get(0), ema26S.Get(0) + ema12P, ema26P := ema12S.Get(1), ema26S.Get(1) + //ema200 := ta.EMA(e.Close, 200).Get(0) + // + //rsi := ta.RSI(e.Close, 14).Get(0) + //sma5 := ta.SMA(e.Close, 5).Get(0) + + tag := "" + volOk := e.Volume.Get(0) < e.Volume.Get(1)*8 + //vol30ok := volMa.Get(0) > volMa.Get(30)*0.5 + + _, _, _ = m, o, bbUpp + + if ema26-ema12 > atrBase*6 && ema26P-ema12P > atrBase*2 && c < bbLow.Get(0) && volOk { + tag = "buy6" + } + // 下面的要么不稳定,要么几乎没有订单 + //if c > max(ema200, m.bigEma200) && c < bbLow.Get(0)-atrBase*bbLowRate1 && vol30ok && + // volOk && o-c < bbUpp.Get(2)-bbLow.Get(2) { + // tag = "buy1" + //} else if c > ema200 && c < bbLow.Get(0)-atrBase*bbLowRate2 && vol30ok && + // volOk && o-c < bbUpp.Get(2)-bbLow.Get(2) { + // // 订单很少,盈利很少 + // //tag = "buy2" + //} else if c > m.bigEma200 && c < bbLow.Get(0) && rsi < 8.5 && volOk { + // // 订单很少,盈利很少 + // //tag = "buy3" + //} else if m.bigRsi < 10.3 && c < bbLow.Get(0) && volOk { + // // 小额亏损 + // //tag = "buy4" + //} else if c > max(ema200, m.bigEma200) && ema26-ema12 > atrBase*18 && ema26P-ema12P > atrBase*2 && + // c < bbLow.Get(0) && volOk && vol30ok { + // tag = "buy5" + //} else if m.bigRsi < 13 && ema26-ema12 > atrBase*18 && ema26P-ema12P > atrBase*2 && volOk && vol30ok { + // // 订单很少,盈利很少 + // //tag = "buy7" + //} else if m.bigRsi < 12.2 && rsi < 39.6 && volOk && vol30ok { + // tag = "buy8" + //} else if m.bigRsi < 18 && rsi < 24.2 && volOk && vol30ok { + // tag = "buy9" + //} else if c < sma5 && m.bigSSLUp > m.bigSSLDn && m.bigEma50 > m.bigEma200 && rsi < m.bigRsi-43.69 { + // tag = "buy10" + //} + + if tag != "" && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{Tag: tag}) + } + + for _, od := range s.LongOrders { + holdNum := int(math.Round(e.BarCount(od.EnterAt))) + if holdNum >= 10 && od.ProfitRate < 0 && c+atrBase*3 < e.Open.Get(holdNum) { + od.SetStopLoss(&ormo.ExitTrigger{Price: c - atrBase*2}) + } + } + + if s.OrderNum > 0 && c > bbMid.Get(0)+atrBase*2 { + s.CloseOrders(&strat.ExitReq{Tag: "sell"}) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*MadV9HOSta) + m.bigEma50 = ta.EMA(e.Close, 50).Get(0) + m.bigEma200 = ta.EMA(e.Close, 200).Get(0) + m.bigRsi = ta.RSI(e.Close, 14).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + smaHigh := ta.SMA(e.High, 20).Get(0) + atr + smaLow := ta.SMA(e.Low, 20).Get(0) - atr + hlv := e.Close.To("_hlv", 20) + sslUp := e.Close.To("_sslUp", 20) + sslDn := e.Close.To("_sslDn", 20) + c := e.Close.Get(0) + if !hlv.Cached() { + if c > smaHigh { + hlv.Append(1) + } else if c < smaLow { + hlv.Append(-1) + } else { + hlv.Append(hlv.Get(0)) + } + if hlv.Get(0) < 0 { + sslUp.Append(smaLow) + sslDn.Append(smaHigh) + } else { + sslUp.Append(smaHigh) + sslDn.Append(smaLow) + } + } + m.bigSSLUp = sslUp.Get(0) + m.bigSSLDn = sslDn.Get(0) + }, + } +} diff --git a/org/ft/SMAOG.go b/org/ft/SMAOG.go new file mode 100644 index 0000000..26424e7 --- /dev/null +++ b/org/ft/SMAOG.go @@ -0,0 +1,69 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type maogState struct { + atrBase float64 +} + +func SMAOG(pol *config.RunPolicyConfig) *strat.TradeStrat { + longRate := pol.Def("longRate", 3.03, core.PNorm(1.5, 6)) + shortRate := pol.Def("shortRate", 1.03, core.PNorm(0.5, 4)) + + lls2 := pol.Def("lls2", 4, core.PNorm(0, 10)) + lls12 := pol.Def("lls12", 6, core.PNorm(2, 15)) + lls144 := pol.Def("lls144", 16, core.PNorm(4, 30)) + + lenAtr := int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 200, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &maogState{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*maogState) + c := e.Close.Get(0) + oo := e.Open + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + m.atrBase = atrBase + + maBuy := ta.SMA(e.Close, 26).Get(0) + maSell := ta.EMA(e.Close, 28).Get(0) + badLL2 := ta.Lowest(e.Open, 2).Get(0)-c > atrBase*lls2 + badLL12 := ta.Lowest(e.Open, 12).Get(0)-c > atrBase*lls12 + badLL144 := ta.Lowest(e.Open, 144).Get(0)-c > atrBase*lls144 + pairBad := badLL2 || badLL12 || badLL144 + + ema50 := ta.EMA(e.Close, 50).Get(0) + ema200 := ta.EMA(e.Close, 200).Get(0) + rsiExit := ta.RSI(e.Close, 2) + + if maSell-c < atrBase*shortRate { + if oo.Get(0) < oo.Get(1) || rsiExit.Get(0) < 50 || rsiExit.Get(0) < rsiExit.Get(1) { + s.CloseOrders(&strat.ExitReq{Tag: "short"}) + return + } + } + if ema50 > ema200 && c > ema200 && !pairBad && maBuy-c > atrBase*longRate && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLossVal: atrBase * 15}) + } + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + m, _ := s.More.(*maogState) + if maxChg/(m.atrBase/od.InitPrice) > 2 { + return 0.35 + } + return 0 + }, + } +} diff --git a/org/ft/Synthesis.go b/org/ft/Synthesis.go new file mode 100644 index 0000000..f88f902 --- /dev/null +++ b/org/ft/Synthesis.go @@ -0,0 +1,47 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://strat.ninja/overview.php?strategy=Money +过于简单,无法盈利,针对1h周期 +*/ + +func Synthesis(pol *config.RunPolicyConfig) *strat.TradeStrat { + buyRSI := pol.Def("buy_rsi", 30, core.PNorm(20, 40)) + sellRSI := pol.Def("sell_rsi", 70, core.PNorm(60, 80)) + buyEmaShort := int(pol.Def("buy_ema_short", 10, core.PNorm(5, 15))) + buyEmaLong := int(pol.Def("buy_ema_long", 50, core.PNorm(20, 60))) + + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + EachMaxShort: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + // 计算指标 + rsi := ta.RSI(e.Close, 14) + emaShort := ta.EMA(e.Close, buyEmaShort) + emaLong := ta.EMA(e.Close, buyEmaLong) + + // 买入条件:RSI低于买入阈值且短期EMA高于长期EMA + if rsi.Get(0) < buyRSI && emaShort.Get(0) > emaLong.Get(0) && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + + // 卖出条件:RSI高于卖出阈值且短期EMA低于长期EMA + if rsi.Get(0) > sellRSI && emaShort.Get(0) < emaLong.Get(0) { + s.CloseOrders(&strat.ExitReq{ + Tag: "short", + }) + } + }, + } +} diff --git a/org/ft/bbrsitv.go b/org/ft/bbrsitv.go new file mode 100644 index 0000000..375173e --- /dev/null +++ b/org/ft/bbrsitv.go @@ -0,0 +1,64 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://github.com/jilv220/freqtrade-stuff/BBRSITV.py + +滚动优化:bt_opt -review-period 18M -run-period 2M -opt-rounds 20 +bt_opt_2d11bb5607 最近两年半40%收益,回撤37%;不稳定。 + +5m,80品种,3年531%,中间不够稳定,待优化 +15m, 80品种,3年553%,中间略不稳定,两次半年回撤,待优化 +1h,80品种,3年278%,回撤49%,熊市时回撤较大 +*/ + +func BBRSITV(pol *config.RunPolicyConfig) *strat.TradeStrat { + ewoRate := pol.Def("ewoRate", 1.0, core.PNorm(0.1, 3)) + ewoHigh := 5 * ewoRate + + maLen := int(pol.Def("maLen", 22, core.PNorm(5, 80))) + maLenS := int(pol.Def("maLenS", 65, core.PNorm(5, 80))) + rsiSigma := pol.Def("rsiSigma", 1.74, core.PNorm(0, 6)) + rsiSigmaS := pol.Def("rsiSigmaS", 1.9, core.PNorm(0, 6)) + rsiHigh := pol.Def("rsiHigh", 72, core.PNorm(60, 100)) + + lenAtr := int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + //c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + rsi := ta.RSI(e.Close, 14) + rsiV := rsi.Get(0) + //rsi4 := ta.RSI(e.Close, 4).Get(0) + basisBuy := ta.EMA(rsi, maLen).Get(0) + basisSell := ta.EMA(rsi, maLenS).Get(0) + + devBuy := ta.StdDev(rsi, maLen) + devSell := ta.StdDev(rsi, maLenS) + + ema1 := ta.EMA(e.Close, 50).Get(0) + ema2 := ta.EMA(e.Close, 200).Get(0) + ewo := (ema1 - ema2) / atrBase + + if rsiV > rsiHigh || rsiV > basisSell+devSell.Get(0)*rsiSigmaS { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + return + } + if rsiV < basisBuy-devBuy.Get(0)*rsiSigma && ewo > ewoHigh && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLossVal: atrBase * 20}) + } + }, + } +} diff --git a/org/ft/bin_ha_cluc_v8.go b/org/ft/bin_ha_cluc_v8.go new file mode 100644 index 0000000..bb41b77 --- /dev/null +++ b/org/ft/bin_ha_cluc_v8.go @@ -0,0 +1,245 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* + +https://github.com/zmomz/freq_strategies/blob/f25f8493ebab589e327cd011b829d1c64cafae84/strategies/CombinedBinHAndClucV8Hyper.py#L53 + +滚动回测 bt_opt -review-period 18M -run-period 2M -opt-rounds 20 +bt_opt_d0c194932b 2年半收益61%,回撤14%;最近三四个月亏损 +*/ + +type haClucState struct { + sma200Dec bool + rsi float64 + atrBase float64 + + bigMa50 float64 + bigMa100 float64 + bigEMA200 float64 + bigSMA200 float64 + bigSma200Dec bool //bigSMA200是否低于20小时前的值 + bigRsi float64 + + bigHlv float64 // 大周期市场趋势 + sslDown float64 + sslUp float64 +} + +func CombinedBinHAndClucV8Hyper(pol *config.RunPolicyConfig) *strat.TradeStrat { + BuyThs1 := pol.Def("BuyThs1", 0.15, core.PNorm(0.05, 0.3)) + BuyThs2 := pol.Def("BuyThs2", 0.49, core.PNorm(0.1, 0.6)) + BuyThs3 := pol.Def("BuyThs3", 0.24, core.PNorm(0.15, 0.7)) + bb40BBDeltaClose := pol.Def("bb40BBDeltaClose", 7.4, core.PNorm(5, 30)) + bb40CloseDeltaClose := pol.Def("bb40CloseDeltaClose", 9, core.PNorm(3, 20)) + bb40TailBBDelta := pol.Def("bb40TailBBDelta", 0.27, core.PNorm(0.1, 0.5)) + + bb20CloseBBLower := pol.Def("bb20CloseBBLower", 0.94, core.PNorm(0.5, 3)) + bb20Vol := pol.Def("bb20Vol", 20.2, core.PNorm(5, 50)) + + rsiDiff := pol.Def("rsiDiff", 53.23, core.PNorm(20, 80)) + minInc := pol.Def("minInc", 1.2, core.PNorm(0.5, 5)) + rsi1H := 67.0 + rsiSml := 38.5 + mfiThs := 36.0 + + vol1 := 2.0 + emaOpenMult := pol.Def("emaOpenMult", 2.3, core.PNorm(1.5, 5)) + + earlyExitRate := pol.Def("earlyExitRate", 1, core.PNorm(-1, 2)) + earlyExitNum := int(pol.Def("earlyExitNum", 65, core.PNorm(10, 70))) + shortRsiMain := pol.Def("shortRsiMain", 80.75, core.PNorm(70, 95)) + sellRoiProfit1 := pol.Def("sellRoiProfit1", 2, core.PNorm(0.3, 5)) + sellRoiRsi1 := pol.Def("sellRoiRsi1", 63.4, core.PNorm(40, 64)) + sellRoiProfit2 := pol.Def("sellRoiProfit2", 3, core.PNorm(1, 7)) + sellRoiRsi2 := pol.Def("sellRoiRsi2", 63.4, core.PNorm(40, 64)) + sellRoiProfit3 := pol.Def("sellRoiProfit3", 8, core.PNorm(4, 12)) + sellRoiRsi3 := pol.Def("sellRoiRsi3", 50.1, core.PNorm(45, 67)) + sellRoiProfit4 := pol.Def("sellRoiProfit4", 12.4, core.PNorm(7, 18)) + sellRoiRsi4 := pol.Def("sellRoiRsi4", 55.4, core.PNorm(50, 70)) + sellRoiProfit5 := pol.Def("sellRoiProfit5", 1, core.PNorm(1, 7)) + + trailProfitMin1 := pol.Def("trailProfitMin1", 0.06, core.PNorm(0.03, 0.3)) + trailProfitMin2 := pol.Def("trailProfitMin2", 0.04, core.PNorm(0.01, 0.1)) + customSL1 := -pol.Def("customSL1", 2.17, core.PNorm(0.5, 3)) + + lenAtr := int(pol.Def("atr", 40, core.PNorm(10, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + + pol.KeepHyperOnly("earlyExitRate", "earlyExitNum", "sellRoiProfit1", "sellRoiProfit2", "sellRoiProfit3", + "sellRoiProfit4", "sellRoiProfit5", "customSL1") + + return &strat.TradeStrat{ + WarmupNum: 300, + DrawDownExit: true, + EachMaxLong: 1, + OnStartUp: func(s *strat.StratJob) { + s.More = &haClucState{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 300}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + more, _ := s.More.(*haClucState) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + more.atrBase = atrBase + + o, l, c, v := e.Open.Get(0), e.Low.Get(0), e.Close.Get(0), e.Volume.Get(0) + c1, c2 := e.Close.Get(1), e.Close.Get(2) + _, bbMidC, bbLowCol := ta.BBANDS(e.Close, 40, 2, 2) + bbLow, bbMid := bbLowCol.Get(0), bbMidC.Get(0) + bbDelta := math.Abs(bbMid - bbLow) + closeDelta := math.Abs(c - e.Close.Get(1)) + tail := math.Abs(c - l) + + avgPrice := ta.AvgPrice(e) + bb20UppC, _, bb20Dn := ta.BBANDS(avgPrice, 20, 2, 2) + bb20Upp, bb20Low := bb20UppC.Get(0), bb20Dn.Get(0) + + volSlow1 := ta.SMA(e.Volume, 30).Get(1) + volMa4 := ta.SMA(e.Volume, 4).Get(0) + + ema12C, ema26C := ta.EMA(e.Close, 12), ta.EMA(e.Close, 26) + ema12 := ema12C.Get(0) + ema26 := ema26C.Get(0) + ema50 := ta.EMA(e.Close, 50).Get(0) + ema200 := ta.EMA(e.Close, 200).Get(0) + + sma5 := ta.SMA(e.Close, 5).Get(0) + sma200 := ta.SMA(e.Close, 200) + sma200V := sma200.Get(0) + + sma200Dec := sma200V < sma200.Get(20) + more.sma200Dec = sma200Dec + + mfi := ta.MFI(e, 14).Get(0) + rsi := ta.RSI(e.Close, 14).Get(0) + more.rsi = rsi + + openHH2 := ta.Highest(e.Open, 2).Get(0) + openHH12 := ta.Highest(e.Open, 12).Get(0) + openHH144 := ta.Highest(e.Open, 144).Get(0) + openLL24 := ta.Lowest(e.Open, 24).Get(0) + + if c > bb20Upp && c1 > bb20UppC.Get(1) && c2 > bb20UppC.Get(2) { + s.CloseOrders(&strat.ExitReq{Tag: "exit1", Dirt: core.OdDirtLong}) + return + } + if rsi > shortRsiMain { + s.CloseOrders(&strat.ExitReq{Tag: "exit2", Dirt: core.OdDirtLong}) + return + } + + if (openHH2-c)/c < BuyThs1 && (openHH12-c)/c < BuyThs2 { + if c > more.bigEMA200 && ema50 > ema200 && more.bigMa50 > more.bigEMA200 { + if bbDelta > atrBase*bb40BBDeltaClose && closeDelta > atrBase*bb40CloseDeltaClose && + tail < bbDelta*bb40TailBBDelta && c < bbLowCol.Get(1) && c <= e.Close.Get(1) { + s.OpenOrder(&strat.EnterReq{Tag: "long1"}) + return + } + } + if c > more.bigEMA200 && c > ema200 && more.bigMa50 > max(more.bigMa100, more.bigEMA200) { + if c < ema50 && (bb20Low-c) > atrBase*bb20CloseBBLower && v < volSlow1*bb20Vol { + s.OpenOrder(&strat.EnterReq{Tag: "long2"}) + return + } + } + if c < sma5 && more.sslUp > more.sslDown && ema50 > ema200 && more.bigMa50 > more.bigEMA200 { + if (openHH144-c)/c < BuyThs3 && rsi < more.bigRsi-rsiDiff { + s.OpenOrder(&strat.EnterReq{Tag: "long3"}) + return + } + } + if !sma200Dec && !more.bigSma200Dec && (openHH144-c)/c < BuyThs3 && (openLL24-c)/c > atrBase*minInc { + if more.bigRsi > rsi1H && rsi < rsiSml && mfi < mfiThs { + s.OpenOrder(&strat.EnterReq{Tag: "long4"}) + return + } + } + if c > more.bigMa100 && more.bigMa50 > more.bigMa100 && (openHH144-c)/c < BuyThs3 && volMa4*vol1 > v { + if ema26 > ema12 && ema26-ema12 > atrBase*emaOpenMult { + if ema26C.Get(1)-ema12C.Get(1) > o/100 && c < bb20Low { + s.OpenOrder(&strat.EnterReq{Tag: "long5"}) + return + } + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + more, _ := s.More.(*haClucState) + more.bigMa50 = ta.EMA(e.Close, 50).Get(0) + more.bigMa100 = ta.EMA(e.Close, 100).Get(0) + more.bigEMA200 = ta.EMA(e.Close, 200).Get(0) + sma200 := ta.SMA(e.Close, 200) + more.bigSMA200 = sma200.Get(0) + more.bigSma200Dec = more.bigSMA200 < sma200.Get(20) + more.bigRsi = ta.RSI(e.Close, 14).Get(0) + + length := 20 + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + maHigh := ta.SMA(e.High, length).Get(0) + atr + maLow := ta.SMA(e.Low, length).Get(0) - atr + c := e.Close.Get(0) + if c > maHigh { + more.bigHlv = 1 + } else if c < maLow { + more.bigHlv = -1 + } + if more.bigHlv < 0 { + more.sslDown = maHigh + more.sslUp = maLow + } else { + more.sslDown = maLow + more.sslUp = maHigh + } + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + more, _ := s.More.(*haClucState) + holdNum := int((s.Env.TimeStop - od.RealEnterMS()) / s.Env.TFMSecs) + // 计算相对利润率,1%利润对应均值大概为1 + profitRate := od.ProfitRate / od.Leverage / (more.atrBase / od.InitPrice) + if holdNum >= earlyExitNum && profitRate < earlyExitRate { + od.SetStopLoss(&ormo.ExitTrigger{Price: s.Env.Close.Get(0) - more.atrBase*0.65}) + } else if profitRate < customSL1 && more.sma200Dec && more.bigSma200Dec { + return &strat.ExitReq{Tag: "sl2"} + } + if profitRate > sellRoiProfit4 && more.rsi < sellRoiRsi4 { + return &strat.ExitReq{Tag: "roi4"} + } + if profitRate > sellRoiProfit3 && more.rsi < sellRoiRsi3 { + return &strat.ExitReq{Tag: "roi3"} + } + if profitRate > sellRoiProfit2 && more.rsi < sellRoiRsi2 { + return &strat.ExitReq{Tag: "roi2"} + } + if profitRate > sellRoiProfit1 && more.rsi < sellRoiRsi1 { + return &strat.ExitReq{Tag: "roi1"} + } + if profitRate > 0 && profitRate < sellRoiProfit5 && more.sma200Dec { + return &strat.ExitReq{Tag: "roi5"} + } + return nil + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + if maxChg > trailProfitMin1 { + return 0.3 + } else if maxChg > trailProfitMin2 { + return 0.7 + } + return 0 + }, + } +} diff --git a/org/ft/binance.go b/org/ft/binance.go new file mode 100644 index 0000000..e8fd266 --- /dev/null +++ b/org/ft/binance.go @@ -0,0 +1,97 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://strat.ninja/overview.php?strategy=binance +210101-241101 48品种,收益159% 回撤5% 夏普10.25 + +*/ + +func binance(pol *config.RunPolicyConfig) *strat.TradeStrat { + //ewoRate := pol.Def("ewoRate", 1.0, core.PNorm(0.1, 3)) + //ewoHigh := -2.7 * ewoRate + //lowRate := pol.Def("lowRate", 8.6, core.PNorm(1, 3.5)) + //highRate := pol.Def("highRate", 2.7, core.PNorm(0.5, 2.5)) + //rsiLong := pol.Def("rsiLong", 30, core.PNorm(20, 65)) + //maLongF := pol.Def("maLong", 8, core.PNorm(8, 16)) + //maLong := int(math.Round(maLongF)) + //maRate := pol.Def("maRate", 2, core.PNorm(1.5, 3)) + //maShort := int(math.Round(maLongF * maRate)) + lenAtr := 29 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + //emaLong := ta.EMA(e.Close, maLong).Get(0) + //emaShort := ta.EMA(e.Close, maShort).Get(0) + + sma15 := ta.SMA(e.Close, 15).Get(0) + cti := ta.CTI(e.Close, 20).Get(0) + + //ema1 := ta.EMA(e.Close, 50).Get(0) + //ema2 := ta.EMA(e.Close, 200).Get(0) + //ewo := (ema1 - ema2) / atrBase + + rsi := ta.RSI(e.Close, 14).Get(0) + rsiFast := ta.RSI(e.Close, 4).Get(0) + rsiSlowS := ta.RSI(e.Close, 20) + rsiSlow, rsiSlowP := rsiSlowS.Get(0), rsiSlowS.Get(1) + + fastK := ta.Stoch(e.High, e.Low, e.Close, 5).Get(0) + + bb20Up, bb20Md, bb20Dn := ta.BBANDS(ta.HLC3(e.High, e.Low, e.Close), 20, 2, 2) + bb20Upp, bb20Mid, bb20Low := bb20Up.Get(0), bb20Md.Get(0), bb20Dn.Get(0) + bb20Wid := (bb20Upp - bb20Low) / atrBase + + volMa12 := ta.SMA(e.Volume, 12).Get(1) + volMa24 := ta.SMA(e.Volume, 24).Get(1) + + //lowWid := atrBase * lowRate + //highWid := atrBase * highRate + + for _, od := range s.LongOrders { + holdNum := e.BarCount(od.EnterAt) + profit := (c - od.Enter.Average) / atrBase + if od.Short { + profit *= -1 + } + if profit < -10 && bb20Wid < 10 && c > bb20Mid && volMa12 < volMa24 { + s.CloseOrders(&strat.ExitReq{Tag: "deadfish"}) + continue + } + if fastK > 75 && (holdNum > 11.5 && profit > -2 || holdNum > 287 && profit > -10) { + od.SetStopLoss(&ormo.ExitTrigger{Price: c - atrBase*0.2}) + } else if od.EnterTag == "ewo" && profit > 10 { + od.SetStopLoss(&ormo.ExitTrigger{Price: c - atrBase}) + } else if profit > 0 && fastK > 75 { + od.SetStopLoss(&ormo.ExitTrigger{Price: c - atrBase*0.2}) + } + } + + if rsiSlow < rsiSlowP && rsiFast < 63 && rsi > 16 && c < sma15-atrBase*13 && cti < -0.8 { + s.OpenOrder(&strat.EnterReq{ + Tag: "buy1", + }) + } + // 略不稳定 + //emaEnt := c < emaLong-lowWid && c < emaShort-highWid + //if rsiFast < 50 && emaEnt && ewo > ewoHigh && rsi < rsiLong { + // s.OpenOrder(&strat.EnterReq{ + // Tag: "ewo", + // }) + //} + }, + } +} diff --git a/org/ft/common.go b/org/ft/common.go new file mode 100644 index 0000000..8702821 --- /dev/null +++ b/org/ft/common.go @@ -0,0 +1,35 @@ +package ft + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("ft", map[string]strat.FuncMakeStrat{ + "smaoff": SMAOffset, + "smaoff2": SMAOffsetV2, + "bin_ha": CombinedBinHAndClucV8Hyper, + "hpsv7": HPSV7Ultra, + "namaoff": NotAnotherSMAOffset, + "e0v1e": E0V1E, + "smaog": SMAOG, + "BBRSITV": BBRSITV, + "ichiv3": ichiv3, + "ichiV1": ichiV1, + "BBMod2": BBMod2, + "Synthesis": Synthesis, + "CenderaWasih3": CenderaWasih3, + "x_zvwap06": x_zvwap06, + "binance": binance, + "BinHModWhiteHOV0": BinHModWhiteHOV0, + "Clucha": Clucha, + "MadV9HO": MadV9HO, + "BBMod7": BBMod7, + "BinHV27": BinHV27, + + "01": SMAOffset, + "02": BBMod2, + "03": BBMod7, + "04": MadV9HO, + "05": binance, + "06": CenderaWasih3, + }) +} diff --git a/org/ft/e0v1e.go b/org/ft/e0v1e.go new file mode 100644 index 0000000..66da9e8 --- /dev/null +++ b/org/ft/e0v1e.go @@ -0,0 +1,165 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://github.com/28dec/E0V1E-HO/blob/main/user_data/strategies/E0V1E_20231014_0847.py + + +https://github.com/ssssi/freqtrade_strs/blob/main/binance/E0V1E.py + + +https://strat.ninja/overview.php?strategy=E0V1E +params: {ctiThs: -0.86, maOffRate: 11.5, buyRsi: 19, buyRsiFast: 46} +比上面多了个入场逻辑,反而更差了 + +1h3年只有40订单,收益为正 +15m和5m收益均不稳定 +*/ + +type E0State struct { + c float64 + h float64 + atrBase float64 + kdjK float64 + cci float64 + ma120 float64 + ma240 float64 + hold map[int64]bool + hold1 map[int64]bool +} + +func E0V1E(pol *config.RunPolicyConfig) *strat.TradeStrat { + buyRsiFast := pol.Def("buyRsiFast", 41, core.PNorm(20, 70)) + buyRsi := pol.Def("buyRsi", 46.7, core.PNorm(15, 50)) + maOffRate := pol.Def("maOffRate", 2.05, core.PNorm(0.2, 5)) + ctiThs := pol.Def("ctiThs", -0.73, core.PNorm(-1, 0)) + sellKdX := pol.Def("sellKdX", 85.2, core.PNorm(50, 100)) + sellCCI := pol.Def("sellCCI", 80, core.PNorm(0, 400)) + + lenAtr := int(pol.Def("atr", 13.35, core.PNorm(7, 25))) + baseAtrLen := int(float64(lenAtr) * 4.3) + + return &strat.TradeStrat{ + WarmupNum: 300, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &E0State{ + hold: make(map[int64]bool), + hold1: make(map[int64]bool), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + m, _ := s.More.(*E0State) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + m.atrBase = atrBase + m.h = e.High.Get(0) + + sma15 := ta.SMA(e.Close, 15).Get(0) + cti := ta.CTI(e.Close, 20).Get(0) + rsi := ta.RSI(e.Close, 14).Get(0) + rsi4 := ta.RSI(e.Close, 4).Get(0) + rsi20S := ta.RSI(e.Close, 20) + rsi20 := rsi20S.Get(0) + + kdjK, _, _ := ta.KDJBy(e.High, e.Low, e.Close, 5, 3, 3, "sma") + cci := ta.CCI(e.Close, 20).Get(0) + ma120 := ta.SMA(e.Close, 120).Get(0) + ma240 := ta.SMA(e.Close, 240).Get(0) + m.kdjK = kdjK.Get(0) + m.cci = cci + m.ma120 = ma120 + m.ma240 = ma240 + + fastK := ta.Stoch(e.High, e.Low, e.Close, 5).Get(0) + + ema100 := ta.EMA(e.Close, 100).Get(0) + ema200 := ta.EMA(e.Close, 200).Get(0) + + hlc3 := ta.HLC3(e.High, e.Low, e.Close) + bb20Up, bb20Md, bb20Dn := ta.BBANDS(hlc3, 20, 2, 2) + bb20Wid := (bb20Up.Get(0) - bb20Dn.Get(0)) / atrBase + bb20Mid := bb20Md.Get(0) + + volMa12 := ta.SMA(e.Volume, 12).Get(1) + volMa24 := ta.SMA(e.Volume, 24).Get(1) + + willR := ta.WillR(e, 14).Get(0) + + if s.OrderNum > 0 && fastK > 75 { + // 这是strat.ninja的出场逻辑,差 + // s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + // return + } + if s.OrderNum > 0 && e.Open.Get(0) < ma120 { + s.CloseOrders(&strat.ExitReq{Tag: "ma120"}) + return + } + tag := "" + if rsi20 < rsi20S.Get(1) && rsi4 < buyRsiFast && rsi > buyRsi && c < sma15-atrBase*maOffRate && cti < ctiThs { + tag = "buy1" + } else if ema100 < ema200+atrBase*17 && bb20Wid > 10 && c < bb20Mid && volMa12 > volMa24 && cti < -0.5 && + willR < -60 { + // 这是strat.ninja的入场逻辑,差 + //tag = "buy2" + } + if s.OrderNum == 0 && tag != "" { + s.OpenOrder(&strat.EnterReq{Tag: tag, StopLossVal: atrBase * 20}) + } + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + m, _ := s.More.(*E0State) + bestRate := maxChg / (m.atrBase / od.InitPrice) + if bestRate > 2.3 { + return 0.3 + } + return 0 + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + m, _ := s.More.(*E0State) + if m.c > min(m.ma120, m.ma240) { + m.hold[od.ID] = true + } else { + m.hold1[od.ID] = true + } + profitRate := od.ProfitRate / od.Leverage / (m.atrBase / od.InitPrice) + if profitRate > 0 && m.kdjK > sellKdX { + return &strat.ExitReq{Tag: "fastK"} + } + if m.cci > sellCCI { + if profitRate > -0.5 { + return &strat.ExitReq{Tag: "cci_.5"} + } + if profitRate > -2 { + return &strat.ExitReq{Tag: "cci_2"} + } + if profitRate > -8 { + return &strat.ExitReq{Tag: "cci_8"} + } + } + holdNum := int((s.Env.TimeStop - od.RealEnterMS()) / s.Env.TFMSecs) + if holdNum > 3 { + _, inHold := m.hold[od.ID] + if inHold && m.c < min(m.ma120, m.ma240) { + delete(m.hold, od.ID) + return &strat.ExitReq{Tag: "ma120"} + } + _, inHold1 := m.hold1[od.ID] + if inHold1 && m.h > min(m.ma120, m.ma240) { + delete(m.hold1, od.ID) + return &strat.ExitReq{Tag: "x120_240"} + } + } + return nil + }, + } +} diff --git a/org/ft/ichiV1.go b/org/ft/ichiV1.go new file mode 100644 index 0000000..f152dad --- /dev/null +++ b/org/ft/ichiV1.go @@ -0,0 +1,80 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "gonum.org/v1/gonum/floats" +) + +func ichiV1(p *config.RunPolicyConfig) *strat.TradeStrat { + senkouLevel := 1 + bullLevel := 6 + h1Rate := 2.0 + fanMagLen := 3 + lenAtr := 14 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + hkasOpen, _, _, _ := ta.HeikinAshi(e) + c := e.Close.Get(0) + close15m := ta.EMA(e.Close, 3).Get(0) + close30m := ta.EMA(e.Close, 6).Get(0) + close1hS := ta.EMA(e.Close, 12) + close1h := close1hS.Get(0) + close2h := ta.EMA(e.Close, 24).Get(0) + close4h := ta.EMA(e.Close, 48).Get(0) + close6h := ta.EMA(e.Close, 72).Get(0) + close8hS := ta.EMA(e.Close, 96) + close8h := close8hS.Get(0) + + open15m := ta.EMA(hkasOpen, 3).Get(0) + open30m := ta.EMA(hkasOpen, 6).Get(0) + open1h := ta.EMA(hkasOpen, 12).Get(0) + open2h := ta.EMA(hkasOpen, 24).Get(0) + open4h := ta.EMA(hkasOpen, 48).Get(0) + open6h := ta.EMA(hkasOpen, 72).Get(0) + open8h := ta.EMA(hkasOpen, 96).Get(0) + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + fanMagnitude := close1h / close8h + fanMagnitudeGain := (close1h - close1hS.Get(1)) / atrBase + + ihci := com.IchimokuCloud(e, 20, 60, 120, 30).Cols + senkouA, senkouB := ihci[2].Get(0), ihci[3].Get(0) + + closeArr := []float64{c, close15m, close30m, close1h, close2h, close4h, close6h, close8h} + openArr := []float64{c, open15m, open30m, open1h, open2h, open4h, open6h, open8h} + + senkouOK := floats.Min(closeArr[senkouLevel-1:]) > max(senkouA, senkouB) + bullOk := true + for i := bullLevel - 1; i < 8; i++ { + if closeArr[i] <= openArr[i] { + bullOk = false + break + } + } + fanMag := true + for i := fanMagLen; i > 0; i-- { + if close1hS.Get(i)/close8hS.Get(i) >= fanMagnitude { + fanMag = false + break + } + } + if s.OrderNum > 0 { + if c < close2h { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + } else if senkouOK && bullOk && fanMagnitude > 1 && fanMagnitudeGain >= h1Rate && fanMag { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + }, + } +} diff --git a/org/ft/ichiv3.go b/org/ft/ichiv3.go new file mode 100644 index 0000000..89dbb07 --- /dev/null +++ b/org/ft/ichiv3.go @@ -0,0 +1,78 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +SMAOffset 的升级版本,也称为 ElliotV8 NASOS +https://github.com/jilv220/freqtrade-stuff/blob/main/ElliotV8_original_ichiv3.py + +滚动回测远不如默认参数稳定 +滚动回测:bt_opt_0529d353d7 +bt_opt -review-period 18M -run-period 2M -opt-rounds 20 +*/ + +func ichiv3(pol *config.RunPolicyConfig) *strat.TradeStrat { + ewoRate := pol.Def("ewoRate", 1.0, core.PNorm(0.1, 3)) + ewoHigh := 3.15 * ewoRate + ewoLow := -17.15 * ewoRate + lowRate := pol.Def("lowRate", 1.7, core.PNorm(1, 3.5)) + highRate := pol.Def("highRate", 0.85, core.PNorm(0.5, 2.5)) + highRate2 := highRate * 2 + rsiLong := pol.Def("rsiLong", 57, core.PNorm(50, 65)) + maLongF := pol.Def("maLong", 12, core.PNorm(8, 16)) + maLong := int(math.Round(maLongF)) + maRate := pol.Def("maRate", 1.833, core.PNorm(1.5, 3)) + maShort := int(math.Round(maLongF * maRate)) + lenAtr := 29 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + emaLong := ta.EMA(e.Close, maLong).Get(0) + emaShort := ta.EMA(e.Close, maShort).Get(0) + + hma50 := ta.HMA(e.Close, 50).Get(0) + + ema1 := ta.EMA(e.Close, 50).Get(0) + ema2 := ta.EMA(e.Close, 200).Get(0) + ewo := (ema1 - ema2) / atrBase + + rsi := ta.RSI(e.Close, 14).Get(0) + rsiFast := ta.RSI(e.Close, 4).Get(0) + rsiSlow := ta.RSI(e.Close, 20).Get(0) + + lowWid := atrBase * lowRate + highWid := atrBase * highRate + highWid2 := atrBase * highRate2 + + emaEnt := c < emaLong-lowWid && c < emaShort+highWid + if rsiFast < 35 && emaEnt && (ewo > ewoHigh && rsi < rsiLong || ewo < ewoLow) { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } else if rsiFast > rsiSlow && c < hma50 && c > emaShort+highWid { + s.CloseOrders(&strat.ExitReq{ + Dirt: core.OdDirtLong, + Tag: "exit", + }) + } else if rsiFast > rsiSlow && c > hma50 && rsi > 50 && c > emaShort+highWid2 { + s.CloseOrders(&strat.ExitReq{ + Dirt: core.OdDirtLong, + Tag: "exit2", + }) + } + }, + } +} diff --git a/org/ft/noAnSmaOff.go b/org/ft/noAnSmaOff.go new file mode 100644 index 0000000..5c1b091 --- /dev/null +++ b/org/ft/noAnSmaOff.go @@ -0,0 +1,74 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +滚动优化:bt_opt -review-period 18M -run-period 2M -opt-rounds 20 +bt_opt_9954bcb1d0 最近两年半1.6%收益,回撤7.6%;不稳定。 +1h > 15m > 5m +*/ + +func NotAnotherSMAOffset(pol *config.RunPolicyConfig) *strat.TradeStrat { + maBuyLen := int(pol.Def("maBuyLen", 14, core.PNorm(5, 80))) + maSellLen := int(pol.Def("maSellLen", 24, core.PNorm(5, 80))) + lowOff := pol.Def("lowOff", 2, core.PNorm(0.5, 4)) + lowOff2 := pol.Def("lowOff2", 4, core.PNorm(1.5, 7)) + highOff := pol.Def("highOff", 1, core.PNorm(0.3, 2.5)) + highOff2 := pol.Def("highOff2", 0.5, core.PNorm(-0.5, 1.5)) + ewoLow := pol.Def("ewoLow", -21, core.PNorm(-25, -8)) + ewoHigh := pol.Def("ewoHigh", 2.33, core.PNorm(1, 12)) + ewoHigh2 := pol.Def("ewoHigh2", -2.33, core.PNorm(-6, 12)) + rsiBuy := pol.Def("rsiBuy", 69, core.PNorm(30, 85)) + hmaExRate := pol.Def("hmaExRate", 12, core.PNorm(3, 20)) + emaExRate := pol.Def("emaExRate", 4, core.PNorm(1.5, 7)) + + lenAtr := int(pol.Def("atr", 29, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + maBuy := ta.EMA(e.Close, maBuyLen).Get(0) + maSell := ta.EMA(e.Close, maSellLen).Get(0) + hma50 := ta.HMA(e.Close, 50).Get(0) + ema100 := ta.EMA(e.Close, 100).Get(0) + sma9 := ta.SMA(e.Close, 9).Get(0) + + ema50 := ta.EMA(e.Close, 50).Get(0) + ema200 := ta.EMA(e.Close, 200).Get(0) + ewo := (ema50 - ema200) * 100 / e.Low.Get(0) + + rsi14 := ta.RSI(e.Close, 14).Get(0) + rsi4 := ta.RSI(e.Close, 4).Get(0) + rsi20 := ta.RSI(e.Close, 20).Get(0) + + if c > sma9 && c-maSell > atrBase*highOff2 && rsi14 > 50 && rsi4 > rsi20 { + s.CloseOrders(&strat.ExitReq{Tag: "exit1"}) + } else if c < hma50 && c-maSell > atrBase*highOff && rsi4 > rsi20 { + s.CloseOrders(&strat.ExitReq{Tag: "exit2"}) + } else if ema100-hma50 >= atrBase*hmaExRate { + s.CloseOrders(&strat.ExitReq{Tag: "exit3"}) + } else if c-ema100 <= atrBase*emaExRate { + s.CloseOrders(&strat.ExitReq{Tag: "exit4"}) + } else if rsi4 < 35 && maSell-c > atrBase*highOff && s.OrderNum == 0 { + if maBuy-c > atrBase*lowOff && ewo > ewoHigh && rsi14 < rsiBuy { + s.OpenOrder(&strat.EnterReq{Tag: "ewo1"}) + } else if maBuy-c > atrBase*lowOff2 && ewo > ewoHigh2 && rsi14 < min(25, rsiBuy) { + s.OpenOrder(&strat.EnterReq{Tag: "ewo2"}) + } else if maBuy-c > atrBase*lowOff && ewo < ewoLow { + s.OpenOrder(&strat.EnterReq{Tag: "ewoLow"}) + } + } + }, + } +} diff --git a/org/ft/sma_offset.go b/org/ft/sma_offset.go new file mode 100644 index 0000000..646fcd1 --- /dev/null +++ b/org/ft/sma_offset.go @@ -0,0 +1,73 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +来自 +https://github.com/hippocritical/strategies/blob/main/SMAOffset_Hippocritical_dca/SMAOffset_Hippocritical_dca.py + +也称ElliotV3 ElliotV5_SMA +https://github.com/minulislam/freqtrade-strategies/user_data/strategies/ElliotV3.py + +滚动测试: +bt_opt_745b13cd06 bt_opt -review-period 18M -run-period 2M -opt-rounds 20 + +添加止损或长期未盈利退出,都会导致整体收益下降 +*/ + +func SMAOffset(pol *config.RunPolicyConfig) *strat.TradeStrat { + ewoRate := pol.Def("ewoRate", 1.0, core.PNorm(0.1, 3)) + ewoHigh := 4.0 * ewoRate + ewoLow := -12 * ewoRate + + lowRate := pol.Def("lowRate", 3.03, core.PNorm(1.5, 5)) + highRate := pol.Def("highRate", 1.03, core.PNorm(0.5, 5)) + + lenAtr := int(pol.Def("atr", 29, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 300, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + emaLong := ta.EMA(e.Close, 16).Get(0) + emaShort := ta.EMA(e.Close, 20).Get(0) + ema1 := ta.EMA(e.Close, 50).Get(0) + ema2 := ta.EMA(e.Close, 200).Get(0) + ewo := (ema1 - ema2) / atrBase + rsi := ta.RSI(e.Close, 14).Get(0) + + if s.OrderNum > 0 { + if c-emaShort > atrBase*highRate { + s.CloseOrders(&strat.ExitReq{ + Dirt: core.OdDirtLong, + Tag: "exit", + }) + } + return + } + if emaLong-c > atrBase*lowRate { + tag := "" + if ewo > ewoHigh && rsi < 59 { + tag = "buy1" + } else if ewo < ewoLow { + //tag = "buy2" + } + if tag != "" { + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + }) + } + } + }, + } +} diff --git a/org/ft/sma_offv2.go b/org/ft/sma_offv2.go new file mode 100644 index 0000000..b09fd0e --- /dev/null +++ b/org/ft/sma_offv2.go @@ -0,0 +1,74 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://github.com/GeorgeMurAlkh/freqtrade-stuff/tree/main/user_data/strategies + +考虑加入大趋势过滤 + +滚动回测:bt_opt_59cb7a0075 +bt_opt -review-period 18M -run-period 2M -opt-rounds 20 +波动性较大,2年累计收益300%但回撤48% +*/ + +type SmaOf2 struct { + goLong bool + atrBase float64 +} + +func SMAOffsetV2(pol *config.RunPolicyConfig) *strat.TradeStrat { + longRate := pol.Def("longRate", 3.03, core.PNorm(1.5, 6)) + shortRate := pol.Def("shortRate", 1.03, core.PNorm(0.5, 4)) + + lenAtr := int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + OnStartUp: func(s *strat.StratJob) { + s.More = &SmaOf2{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 50}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*SmaOf2) + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + m.atrBase = atrBase + + sma := ta.SMA(e.Close, 20).Get(0) + if m.goLong && sma-c > atrBase*longRate && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{Tag: "long"}) + } else if !m.goLong && c-sma > atrBase*shortRate { + s.CloseOrders(&strat.ExitReq{Tag: "short"}) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*SmaOf2) + emaFast := ta.EMA(e.Close, 20).Get(0) + emaSlow := ta.EMA(e.Close, 25).Get(0) + m.goLong = emaFast > emaSlow + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + m, _ := s.More.(*SmaOf2) + holdNum := int((s.Env.TimeStop - od.RealEnterMS()) / s.Env.TFMSecs) + profitRate := od.ProfitRate / od.Leverage / (m.atrBase / od.InitPrice) + if holdNum > 8 && profitRate < -8 { + return &strat.ExitReq{Tag: "sl"} + } + return nil + }, + } +} diff --git a/org/ft/x_zvwap06.go b/org/ft/x_zvwap06.go new file mode 100644 index 0000000..7bbe205 --- /dev/null +++ b/org/ft/x_zvwap06.go @@ -0,0 +1,197 @@ +package ft + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "gonum.org/v1/gonum/floats" + "math" +) + +/* +https://strat.ninja/overview.php?strategy=x_zvwap06 +整体盈利,48品种,盈利190%,回撤30%,中间不够稳定。 +*/ + +type zvwapSta struct { + atrBase float64 + fastK float64 + rsi float64 + cmf15m float64 + cmf1h float64 + tpctChange1h float64 +} + +func x_zvwap06(p *config.RunPolicyConfig) *strat.TradeStrat { + tpctChange4Val := p.Def("tpctChange4", 8, core.PNorm(1, 16)) + ctiVal := p.Def("cti", -0.087, core.PNorm(-0.9, 0)) + rsiLongVal := p.Def("rsiLong", 35, core.PNorm(15, 50)) + bbWidVol := p.Def("bbWid", 6, core.PNorm(3, 15)) + bbMidRateVol := p.Def("bbMidRate", 0.4, core.PNorm(-20, 30)) + emaHighRate := p.Def("emaHighRate", 2, core.PNorm(1, 10)) + emaHighRate2 := p.Def("emaHighRate2", 3, core.PNorm(1, 10)) + + lenAtr := 29 + baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + OnStartUp: func(s *strat.StratJob) { + s.More = &zvwapSta{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "15m", 100}, + {"_cur_", "1h", 100}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*zvwapSta) + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + hlc3 := ta.HLC3(e.High, e.Low, e.Close) + + zvwpCol := zvwap(e.Close, e.Volume, 14) + zvwp := zvwpCol.Get(0) + + rsi := ta.RSI(e.Close, 14).Get(0) + rsiFast := ta.RSI(e.Close, 4).Get(0) + rsiSlow := ta.RSI(e.Close, 20).Get(0) + rsi84 := ta.RSI(e.Close, 84).Get(0) + rsi112 := ta.RSI(e.Close, 112).Get(0) + m.rsi = rsi + + //tpctChange0 := topPctChgDca(e.Open, e.Close, 0) / atrBase + //tpctChange1 := topPctChgDca(e.Open, e.Close, 1) / atrBase + //tpctChange2 := topPctChgDca(e.Open, e.Close, 2) / atrBase + tpctChange4 := topPctChgDca(e.Open, e.Close, 4) / atrBase + //tpctChange12 := topPctChgDca(e.Open, e.Close, 12) / atrBase + //tpctChange72 := topPctChgDca(e.Open, e.Close, 72) / atrBase + //tpctChange144 := topPctChgDca(e.Open, e.Close, 144) / atrBase + + cti := ta.CTI(e.Close, 20).Get(0) + cmf := ta.CMF(e, 20).Get(0) + haOpen, _, _, haClose := ta.HeikinAshi(e) + + ema22 := ta.EMA(e.Close, 22).Get(0) + ema200 := ta.EMA(e.Close, 200).Get(0) + + hma50 := ta.HMA(e.Close, 50).Get(0) + //hma30 := ta.HMA(e.Close, 30).Get(0) + //hma15 := ta.HMA(e.Close, 15).Get(0) + + bb20Up, bb20Md, bb20Dn := ta.BBANDS(hlc3, 20, 2, 2) + bb20Upp, bb20Mid, bb20Low := bb20Up.Get(0), bb20Md.Get(0), bb20Dn.Get(0) + bb20Wid := (bb20Upp - bb20Low) / atrBase + + deadfish := e.Close.To("_deadfish", 0) + if !deadfish.Cached() { + dfVal := 0.0 + if c < ema200 && bb20Wid < bbWidVol && c > bb20Mid+atrBase*bbMidRateVol && cmf < 0 && + haOpen.Get(0) > haClose.Get(0) { + dfVal = 1 + } + deadfish.Append(dfVal) + } + + fastK := ta.Stoch(e.High, e.Low, e.Close, 5).Get(0) + m.fastK = fastK + + for _, od := range s.LongOrders { + profitRate := (c - od.InitPrice) / atrBase + exitTag := "" + if profitRate > 10 && c > hma50+atrBase && c > ema22+atrBase*emaHighRate && rsi > 50 && rsiFast > rsiSlow { + exitTag = "sell1" + } else if profitRate > 10 && c > hma50+atrBase && c > ema22+atrBase*emaHighRate2 { + exitTag = "sell2" + } else if profitRate > 10 && rsi < 42 && cmf < -0.4 { + exitTag = "pft1" + } else if profitRate > 10 && rsi < 43 && cmf < 0 && m.cmf15m < 0 && m.cmf1h < 0 { + exitTag = "pft2" + } else if profitRate > 10 && rsi < 41 && cmf < -0.2 && m.cmf1h < 0 { + exitTag = "pft3" + } else if profitRate > 10 && rsi < 44 && cmf < -0.1 && m.cmf15m < -0.1 && m.cmf1h < -0.1 { + exitTag = "pft4" + } else if profitRate > 10 && rsi < 40 && cmf < -0.2 && m.cmf15m < -0.2 { + exitTag = "pft5" + } else if profitRate > 10 && rsi < 43 && cmf < -0.4 && m.cmf15m < 0 && m.cmf1h < 0 { + exitTag = "pft6" + } else if profitRate > 4 && rsi < 30 && cmf < -0.4 { + exitTag = "bear1" + } else if profitRate > 6 && rsi < 35 && cmf < -0.4 { + exitTag = "bear2" + } else if profitRate > 2 && rsi < 25 && cmf < -0.6 { + exitTag = "bear3" + } else if profitRate < -10 && c < ema200 && bb20Wid < bbWidVol && c > bb20Mid+atrBase*bbMidRateVol && cmf < 0 { + exitTag = "deadfish" + } + if exitTag != "" { + s.CloseOrders(&strat.ExitReq{Tag: exitTag, OrderID: od.ID}) + } + } + + tag := "" + if c < zvwp && tpctChange4 > tpctChange4Val && cti < ctiVal && rsi < rsiLongVal && max(rsi84, rsi112) < 60 && + floats.Max(deadfish.Range(0, 4)) == 0 { + tag = "zvwap" + } else if c < zvwp && tpctChange4 > 13 && cti < -0.8 && rsi < 35 && max(rsi84, rsi112) < 60 && m.tpctChange1h < 0.45 { + tag = "pcdown" + } + if tag != "" && s.OrderNum == 0 { + s.OpenOrder(&strat.EnterReq{Tag: tag}) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*zvwapSta) + if tf == "15m" { + m.cmf15m = ta.CMF(e, 20).Get(0) + } else { + m.cmf1h = ta.CMF(e, 20).Get(0) + m.tpctChange1h = topPctChgDca(e.Open, e.Close, 72) / m.atrBase + } + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + m, _ := s.More.(*zvwapSta) + c := s.Env.Close.Get(0) + profitRate := (c - od.InitPrice) / m.atrBase + if od.Short { + profitRate *= -1 + } + if profitRate > 20 && profitRate < 80 && od.MaxDrawDown > 0.3 { + return &strat.ExitReq{Tag: "trail1"} + } else if profitRate > 8 && profitRate < 22 && od.MaxDrawDown > 0.35 { + return &strat.ExitReq{Tag: "trail2"} + } else if profitRate > 200 && m.rsi > 85 { + return &strat.ExitReq{Tag: "rsi85"} + } + return nil + }, + } +} + +func zvwap(p, v *ta.Series, period int) *ta.Series { + res := p.To("_zvwap", period) + if res.Cached() { + return res + } + maP := ta.SMA(p, period) + maV := ta.SMA(v, period) + mean := ta.VWMA(p, maV, period) + pow2 := mean.To("_pow2", period) + pow2.Append(math.Pow(p.Get(0)-mean.Get(0), 2)) + pow2Ma := ta.SMA(pow2, period) + vwapsd := math.Sqrt(pow2Ma.Get(0)) + return res.Append((maP.Get(0) - mean.Get(0)) / vwapsd) +} + +func topPctChgDca(o, c *ta.Series, period int) float64 { + if period == 0 { + return o.Get(0)/c.Get(0) - 1 + } else { + return ta.Highest(o, period).Get(0)/c.Get(0) - 1 + } +} diff --git a/org/jesse/AlligatorAi.go b/org/jesse/AlligatorAi.go new file mode 100644 index 0000000..ce0625f --- /dev/null +++ b/org/jesse/AlligatorAi.go @@ -0,0 +1,109 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "gonum.org/v1/gonum/floats" + "math" + "strconv" +) + +/* +https://jesse.trade/strategies/alligatorai-by-o1-preview-model +原始版本: +BTC/USDT 30m +仅对BTC有较好的表现,略小回撤。对48个品种收益很不稳定 +*/ + +func smma(obj *ta.Series, period int) *ta.Series { + res := obj.To("_smma", period) + if res.Cached() { + return res + } + val := obj.Get(0) + alpha := 1 / float64(period) + old := res.Get(0) + if !math.IsNaN(val) && !math.IsNaN(old) { + return res.Append(alpha*val + old*(1-alpha)) + } + old = floats.Sum(obj.Range(0, period)) * alpha + if math.IsNaN(old) { + return res.Append(math.NaN()) + } + for i := period - 1; i >= 0; i-- { + val = obj.Get(i) + old = alpha*val + old*(1-alpha) + } + return res.Append(old) +} + +func alligatorTrend(e *ta.BarEnv, c float64) int { + obj := ta.HL2(e.High, e.Low) + jaw := smma(obj, 13).Back(8) + teeth := smma(obj, 8).Back(5) + lips := smma(obj, 5).Back(3) + jawV, teethV, lipsV := jaw.Get(0), teeth.Get(0), lips.Get(0) + trend := 0 + if c > lipsV && lipsV > teethV && teethV > jawV { + trend = 1 + } else if c < lipsV && lipsV < teethV && teethV < jawV { + trend = -1 + } + return trend +} + +type alliga struct { + bigMALong bool + bigTrend int +} + +func AlligatorAI(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 200, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + bigTf := "4h" + if s.TimeFrame == "4h" { + bigTf = "8h" + } + return []*strat.PairSub{ + {"_cur_", bigTf, 200}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &alliga{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*alliga) + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + trend := alligatorTrend(e, c) + adxVal := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + adx := adxVal > 30 + cmo := ta.CMO(e.Close, 14).Get(0) + srsiC, _ := ta.StochRSI(e.Close, 14, 14, 3, 3) + srsi := srsiC.Get(0) + dirt := 0 + if trend == 1 && adx && m.bigTrend == 1 && m.bigMALong && cmo > 20 && srsi < 20 { + dirt = 1 + } else if trend == -1 && adx && m.bigTrend == -1 && !m.bigMALong && cmo < -20 && srsi > 80 { + dirt = -1 + } + if dirt != 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(dirt), + Short: dirt < 0, + StopLossVal: atr * 2, + TakeProfitVal: atr * 2, + }) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*alliga) + c := e.Close.Get(0) + m.bigTrend = alligatorTrend(e, c) + m.bigMALong = e.Close.Get(0) > ta.EMA(e.Close, 100).Get(0) + }, + } +} diff --git a/org/jesse/IFR2.go b/org/jesse/IFR2.go new file mode 100644 index 0000000..14aeef4 --- /dev/null +++ b/org/jesse/IFR2.go @@ -0,0 +1,20 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" +) + +/* +使用了talib.HT_TRENDMODE指标,太复杂,暂不实现 +*/ + +func IFR2(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + //e := s.Env + + }, + } +} diff --git a/org/jesse/IchimokuCloud.go b/org/jesse/IchimokuCloud.go new file mode 100644 index 0000000..e5ff8b7 --- /dev/null +++ b/org/jesse/IchimokuCloud.go @@ -0,0 +1,53 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +/* +https://jesse.trade/strategies/ichimokucloud +在48个品种上表现很不稳定 +*/ + +func IchimokuCloud(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + ichi := com.IchimokuCloud(e, 9, 26, 52, 26).Cols + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) > 50 + chop := ta.CHOP(e, 14).Get(0) < 50 + ichiSub1 := ichi[0].Get(0) - ichi[1].Get(0) + ichiSub2 := ichi[2].Get(0) - ichi[3].Get(0) + doClose := false + if len(s.LongOrders) > 0 { + if ichiSub1 < 0 { + doClose = true + } + } else if len(s.ShortOrders) > 0 { + if ichiSub1 > 0 { + doClose = true + } + } else if ichiSub1*ichiSub2 > 0 && adx && chop { + dirt := float64(1) + tag := "long" + if ichiSub1 < 0 { + dirt = -1 + tag = "short" + } + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + Short: dirt < 0, + StopLossVal: atr * 2.5 * dirt, + }) + } + if doClose { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + }, + } +} diff --git a/org/jesse/KAMATrendFollow.go b/org/jesse/KAMATrendFollow.go new file mode 100644 index 0000000..cc85dae --- /dev/null +++ b/org/jesse/KAMATrendFollow.go @@ -0,0 +1,72 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "strconv" +) + +/* +https://jesse.trade/strategies/kama-trendfollowing +只适合btc,在15m上回测表现良好。 +其他品种表现很不稳定。 +*/ + +type KamaTrdSta struct { + bigKama float64 + lastExit int +} + +func KamaTrendFollow(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", 100}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = &KamaTrdSta{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*KamaTrdSta) + c := e.Close.Get(0) + kama := ta.KAMA(e.Close, 30).Get(0) + bbUp, bbMid, bbDn := ta.BBANDS(e.Close, 20, 2, 2) + bbWidth := (bbUp.Get(0) - bbDn.Get(0)) * 100 / bbMid.Get(0) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + chop := ta.CHOP(e, 14).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + + if adx > 50 && chop < 50 && bbWidth < 7 && e.BarNum-m.lastExit >= 10 && s.OrderNum == 0 { + dirt := 0 + if c > max(kama, m.bigKama) { + dirt = 1 + } else if c < min(kama, m.bigKama) { + dirt = -1 + } + if dirt != 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(dirt), + Short: dirt < 0, + StopLossVal: atr * 2.5, + TakeProfitVal: atr * 2.5, + }) + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*KamaTrdSta) + m.bigKama = ta.KAMA(e.Close, 30).Get(0) + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + m, _ := s.More.(*KamaTrdSta) + if chgType == strat.OdChgExitFill && od.Enter.Filled > 0 { + m.lastExit = s.Env.BarNum + } + }, + } +} diff --git a/org/jesse/MACDEMA.go b/org/jesse/MACDEMA.go new file mode 100644 index 0000000..cd27529 --- /dev/null +++ b/org/jesse/MACDEMA.go @@ -0,0 +1,37 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://jesse.trade/strategies/macd-ema +适合4h,在1h级别震荡严重 +21年到241101,累计收益1555%,回撤28.4% +TODO: 考虑加入做空,提高夏普比率 +*/ + +func MACDEMA(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + ema := ta.EMA(e.Close, 100).Get(0) + macdC, signalC := ta.MACD(e.Close, 12, 26, 9) + macd, signal := macdC.Get(0), signalC.Get(0) + if s.OrderNum > 0 { + if macd < signal && c < ema { + s.CloseOrders(&strat.ExitReq{Tag: "close"}) + } + } else if c > ema && macd > signal { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + }, + } +} diff --git a/org/jesse/RSITrend.go b/org/jesse/RSITrend.go new file mode 100644 index 0000000..6d980e9 --- /dev/null +++ b/org/jesse/RSITrend.go @@ -0,0 +1,42 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://jesse.trade/strategies/rsi-trend +过于简单,无法盈利 +*/ + +func RSITrend(p *config.RunPolicyConfig) *strat.TradeStrat { + //lenAtr := int(p.Def("atr", 14, core.PNorm(7, 40))) + //baseAtrLen := int(float64(lenAtr) * 4.3) + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + rsi := ta.RSI(e.Close, 5) + rsiX35 := rsi.Cross(35) + rsiX75 := rsi.Cross(75) + rsiX10 := rsi.Cross(10) + //atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + //atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + if s.OrderNum > 0 { + if rsiX75 == -1 || rsiX10 == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + } else if rsiX35 == 1 { + c := e.Close.Get(0) + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: c * 0.95, + TakeProfitVal: c * 1.1, + }) + } + }, + } +} diff --git a/org/jesse/SimpleBollinger.go b/org/jesse/SimpleBollinger.go new file mode 100644 index 0000000..bcb2730 --- /dev/null +++ b/org/jesse/SimpleBollinger.go @@ -0,0 +1,36 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +/* +https://jesse.trade/strategies/simple-bollinger-bands-strategy +适合4h,在1h级别收益很不稳定 +21年到241101,收益804%,回撤13% +22年到241101,收益364%,回撤36% +*/ + +func SimpleBollinger(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + hl2 := ta.HL2(e.High, e.Low) + bbUp, bbMid, _ := ta.BBANDS(hl2, 20, 2, 2) + ichiCols := com.IchimokuCloud(e, 9, 26, 52, 26).Cols + if c < bbMid.Get(0) { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } else if c > max(bbUp.Get(0), ichiCols[2].Get(0), ichiCols[3].Get(0)) { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + }, + } +} diff --git a/org/jesse/TTMSqueezeWithAlma.go b/org/jesse/TTMSqueezeWithAlma.go new file mode 100644 index 0000000..6be4fa3 --- /dev/null +++ b/org/jesse/TTMSqueezeWithAlma.go @@ -0,0 +1,52 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "strconv" +) + +/* +https://jesse.trade/strategies/ttmsqueezewithalma +alma实现和jesse中不完全一致,在48个品种,4h级别上震荡严重。 +*/ + +func ttmSqueeze(e *ta.BarEnv, period int, bbMul, kcMul float64) bool { + bbUp, _, bbDn := ta.BBANDS(e.Close, period, bbMul, bbMul) + ma := ta.SMA(e.Close, period).Get(0) + atr := ta.SMA(ta.TR(e.High, e.Low, e.Close), period).Get(0) + thres := atr * kcMul + return bbDn.Get(0) < ma-thres || bbUp.Get(0) > ma+thres +} + +func TTMSqueezeWithAlma(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + alma := ta.ALMA(e.Close, 50, 6, 0.85).Get(0) + stiff := ta.Stiffness(e.Close, 100, 60, 3).Get(0) > 25 + ttms := ttmSqueeze(e, 20, 2, 2) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + + if s.OrderNum > 0 { + return + } + if stiff && ttms { + dirt := 1 + if c < alma { + dirt = -1 + } + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(dirt), + Short: dirt < 0, + StopLossVal: atr * 2, + TakeProfitVal: atr * 2.5, + }) + } + }, + } +} diff --git a/org/jesse/TrendSwingV1.go b/org/jesse/TrendSwingV1.go new file mode 100644 index 0000000..b697e9b --- /dev/null +++ b/org/jesse/TrendSwingV1.go @@ -0,0 +1,73 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "strconv" +) + +/* +https://jesse.trade/strategies/trendswingtrader-v1 +多均线结合ADX。 +原始版本基于4h市价单入场 +改为0.5倍atr限价入场后1h略好于4h +在48个交易对20210101-20241101 +4h, 收益442%回撤20% +1h, 收益404%回撤19% +*/ + +func TrendSwingV1(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 300, + EachMaxShort: 1, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + maSml := ta.EMA(e.Close, 21).Get(0) + maMid := ta.EMA(e.Close, 50).Get(0) + maBig := ta.EMA(e.Close, 100).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + trend := 0 + if maBig < maMid && maMid < maSml && maSml < c { + trend = 1 + } else if maBig > maMid && maMid > maSml && maSml > c { + trend = -1 + } + if len(s.LongOrders) > 0 { + for _, od := range s.LongOrders { + tp := od.GetTakeProfit() + if tp == nil { + // 止盈为nil表示已部分止盈退出 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: max(c-atr*2, od.InitPrice), + }) + } + } + } else if len(s.ShortOrders) > 0 { + for _, od := range s.ShortOrders { + tp := od.GetTakeProfit() + if tp == nil { + // 止盈为nil表示已部分止盈退出 + od.SetStopLoss(&ormo.ExitTrigger{ + Price: min(c+atr*2, od.InitPrice), + }) + } + } + } else if adx > 25 && trend != 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(trend), + Limit: c - atr*float64(trend)*0.5, + StopLossVal: atr * 2, + Short: trend < 0, + TakeProfitVal: atr * 3, + TakeProfitRate: 0.5, + StopBars: 4, + }) + } + }, + } +} diff --git a/org/jesse/TurtleAI.go b/org/jesse/TurtleAI.go new file mode 100644 index 0000000..0744046 --- /dev/null +++ b/org/jesse/TurtleAI.go @@ -0,0 +1,82 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "strconv" +) + +/* +https://jesse.trade/strategies/turtleai-by-o1-preview-model +初始版本基于btcusdt 1h +测试在48个品种上表现很不稳定 +*/ + +func TurtleAI(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxLong: 1, + EachMaxShort: 1, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "12h", 200}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = float64(0) + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + bigMa, _ := s.More.(float64) + hh := ta.Highest(e.High, 20).Get(1) + ll := ta.Lowest(e.Low, 20).Get(1) + //ma := ta.SMA(e.Close, 200).Get(0) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) > 30 + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + chopVal := ta.CHOP(e, 14).Get(0) + chop := chopVal < 40 + + if len(s.LongOrders) > 0 { + newSL := c - atr*2.5 + for _, od := range s.LongOrders { + sl := od.GetStopLoss() + if newSL > sl.Price { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: newSL, + }) + } + } + } else if len(s.ShortOrders) > 0 { + newSL := c + atr*2.5 + for _, od := range s.LongOrders { + sl := od.GetStopLoss() + if newSL < sl.Price { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: newSL, + }) + } + } + } else if adx && chop { + dirt := 0 + if c > max(bigMa, hh) { + dirt = 1 + } else if c < min(bigMa, ll) { + dirt = -1 + } + if dirt != 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(dirt), + Short: dirt < 0, + StopLossVal: atr * 2.5, + }) + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + s.More = ta.SMA(e.Close, 200).Get(0) + }, + } +} diff --git a/org/jesse/common.go b/org/jesse/common.go new file mode 100644 index 0000000..f296bcd --- /dev/null +++ b/org/jesse/common.go @@ -0,0 +1,18 @@ +package jesse + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("jesse", map[string]strat.FuncMakeStrat{ + "TrendSwingV1": TrendSwingV1, + "donchian": donchian, + "AlligatorAI": AlligatorAI, + "TurtleAI": TurtleAI, + "SimpleBollinger": SimpleBollinger, + "MACDEMA": MACDEMA, + "RSITrend": RSITrend, + "IchimokuCloud": IchimokuCloud, + "TTMSqueezeWithAlma": TTMSqueezeWithAlma, + "KamaTrendFollow": KamaTrendFollow, + }) +} diff --git a/org/jesse/donchian.go b/org/jesse/donchian.go new file mode 100644 index 0000000..41cb60e --- /dev/null +++ b/org/jesse/donchian.go @@ -0,0 +1,39 @@ +package jesse + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +https://jesse.trade/strategies/simple-donchian-strategy +唐安奇通道,默认周期20 +适用周期:2h,4h +*/ +func donchian(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + hh := ta.Highest(e.High, 25).Get(1) + ll := ta.Lowest(e.Low, 25).Get(1) + ma := ta.SMA(e.Close, 200).Get(0) + //atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + + if len(s.LongOrders) > 0 { + if c < ll { + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } + } else if c > max(ma, hh) { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + //TakeProfitVal: atr * 20, + //TakeProfitRate: 0.5, + }) + } + }, + } +} diff --git a/org/jesse/jesse_test.go b/org/jesse/jesse_test.go new file mode 100644 index 0000000..8c27c9b --- /dev/null +++ b/org/jesse/jesse_test.go @@ -0,0 +1,40 @@ +package jesse + +import ( + "fmt" + "github.com/banbox/banta" + "testing" +) + +var env = &banta.BarEnv{ + TimeFrame: "1d", + TFMSecs: 86400000, + Exchange: "binance", + MarketType: "future", +} + +var DataKline = []banta.Kline{ + {1688169600000, 1, 1, 1, 1, 1, 0}, + {1688256000000, 2, 2, 2, 2, 2, 0}, + {1688342400000, 3, 3, 3, 3, 3, 0}, + {1688428800000, 4, 4, 4, 4, 4, 0}, + {1688515200000, 5, 5, 5, 5, 5, 0}, + {1688601600000, 6, 6, 6, 6, 6, 0}, + {1688688000000, 7, 7, 7, 7, 7, 0}, + {1688774400000, 8, 8, 8, 8, 8, 0}, + {1688860800000, 9, 9, 9, 9, 9, 0}, + {1688947200000, 10, 10, 10, 10, 10, 0}, + {1689033600000, 11, 11, 11, 11, 11, 0}, + {1689120000000, 12, 12, 12, 12, 12, 0}, +} + +func TestSMMA(t *testing.T) { + env.Reset() + var res []float64 + for _, bar := range DataKline { + env.OnBar(bar.Time, bar.Open, bar.High, bar.Low, bar.Close, bar.Volume, bar.Info) + ma := smma(env.Close, 4) + res = append(res, ma.Get(0)) + } + fmt.Printf("%v", res) +} diff --git a/org/llm/common.go b/org/llm/common.go new file mode 100644 index 0000000..3aae945 --- /dev/null +++ b/org/llm/common.go @@ -0,0 +1,7 @@ +package llm + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("llm", map[string]strat.FuncMakeStrat{}) +} diff --git a/org/sf/01.go b/org/sf/01.go new file mode 100644 index 0000000..331fc8b --- /dev/null +++ b/org/sf/01.go @@ -0,0 +1,74 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +/* +此策略核心是同时突破均线和n周期最高/低价时入场。然后跟踪止损出场。 +适合品种:['AG888', 'RB888', 'I888', 'J888', 'A888', 'Y888', 'JM888', 'PP888', 'C888', 'V888', 'JD888', 'SS888', + + 'EB888', 'BC888', 'SC888', 'PG888', 'EG888', 'LH888', 'SI888', 'BR888', 'EC888', 'LC888'] +*/ + +func SF01V2(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa2 = int(pol.Def("ma2", 130, core.PNorm(50, 200))) + lenExtrm = int(pol.Def("extrm", 240, core.PNorm(100, 400))) + lenAtr = int(pol.Def("atr", 14, core.PNorm(7, 40))) + initSl := pol.Def("initSl", 100, core.PNorm(50, 150)) + baseAtrLen := int(float64(lenAtr) * 4.3) + slMinRate, slPow, slRate := 0.4, 0.3, 0.6 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(lenMa2, lenExtrm, baseAtrLen), + OnStartUp: func(s *strat.StratJob) { + s.More = &MTRSDirt{ + TRS: com.NewTRS(initSl, slMinRate, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + //date := btime.ToDateStrLoc(e.TimeStop, "") + maVal := ta.SMA(e.Close, lenMa2).Get(1) + + openUp := e.High.Back(1).Sub(e.Close.Back(1)).Add(e.Open) + openDn := e.Low.Back(1).Sub(e.Close.Back(1)).Add(e.Open) + hh1 := ta.Highest(openUp, lenExtrm).Get(1) + ll1 := ta.Lowest(openDn, lenExtrm).Get(1) + c := e.Close.Get(1) + midPrice := (e.High.Get(1) + e.Low.Get(1)) * 0.5 + + width := e.High.Sub(e.Low) + m, _ := s.More.(*MTRSDirt) + + var openDirt = 0.0 + if width.Get(1) > width.Get(2) { + // 价差是否增大 + if len(s.LongOrders) == 0 && c > max(maVal, hh1) && midPrice > e.High.Get(2) { + // 价格突破均线、周期内最高价;中间价>前bar最高 + openDirt = 1 + m.TrialLong(e.Close.Get(0)) + } else if len(s.ShortOrders) == 0 && c < min(maVal, ll1) && midPrice < e.Low.Get(2) { + // 价格跌破均线、周期内最低价;中间价<前bar最低 + openDirt = -1 + m.TrialShort(e.Close.Get(0)) + } + } + + stopLong, stopShort := m.CalcSL2(s, lenAtr, baseAtrLen, slPow, slRate) + if s.IsWarmUp { + return + } + if openDirt != 0 { + //log.Info(fmt.Sprintf("%s, open, slVal: %.1f%% slUnit: %.2f%%", + // s.Symbols.Symbols, slUnit*m.InitSLTR*100/c, slUnit*100/c)) + com.FastOpen(s, openDirt, stopLong, stopShort, 0) + } + setAllStopLoss(s, stopLong, stopShort) + }, + } +} diff --git a/org/sf/02.go b/org/sf/02.go new file mode 100644 index 0000000..07162b6 --- /dev/null +++ b/org/sf/02.go @@ -0,0 +1,87 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "go.uber.org/zap" +) + +/* +AROON_UP和AROON_DN,反映了一段时间内出现最高价和最低价距离当前时间的远近。 +数值越大,说明发生的时间越近;反之,数值越小,说明发生的时间越远。 +AROON为二者的差值,大于0,说明最高价出现在最低价之后,离现在更近; +反之,则说明最高价出现在最低价之前,离现在更远。 + +使用方法 +(1) 当 AROON_UP 上穿 70,并且 AROON>0,说明上涨趋势形成,产生买入信号; +(2) 当 AROON_DOWN 上穿 70,并且 AROON<0,说明下跌趋势形成,产生卖出信号; +(3) 当 AROON_UP 下穿 30,并且 AROON<0,说明上涨趋势减弱,可能反转下跌,产生卖出信号; +(4) 当 AROON_DOWN 下穿 30,并且 AROON>0,说明下跌趋势减弱,可能反转上涨,产生买入信号。 + +在币圈市场15m/1h/4h上均不稳定,且交易次数太少 +*/ + +func SF02V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 250 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: lenMa1 + 1, + MinTfScore: 0.75, + OnBar: func(s *strat.StratJob) { + e := s.Env + cXMa := e.Close.Get(0) - ta.EMA(e.Close, lenMa1).Get(0) + arnUp, arnOsc, arnDn := ta.Aroon(e.High, e.Low, lenMa1) + upX70 := arnUp.Cross(70) + upX30 := arnUp.Cross(30) + upX50 := arnUp.Cross(50) + dnX70 := arnDn.Cross(70) + dnX30 := arnDn.Cross(30) + dnX50 := arnDn.Cross(50) + oscVal := arnOsc.Get(0) + + openDirt := 0.0 + if len(s.LongOrders) == 0 && cXMa > 0 && oscVal > 0 { + if upX70 == 1 || dnX30 == -1 { + // (1) 当 AROON_UP 上穿 70,并且 AROON>0,说明上涨趋势形成,产生买入信号; + // (4) 当 AROON_DOWN 下穿 30,并且 AROON>0,说明下跌趋势减弱,可能反转上涨,产生买入信号。 + openDirt = 1 + } + } else if len(s.ShortOrders) == 0 && cXMa < 0 && oscVal < 0 { + if dnX70 == 1 || upX30 == -1 { + //if upX30 == -1 { + //(2) 当 AROON_DOWN 上穿 70,并且 AROON<0,说明下跌趋势形成,产生卖出信号; + //(3) 当 AROON_UP 下穿 30,并且 AROON<0,说明上涨趋势减弱,可能反转下跌,产生卖出信号; + openDirt = -1 + } + } + if openDirt != 0 { + com.FastOpen(s, openDirt, 0, 0, 0) + } + closeDirt := 0 + if len(s.LongOrders) > 0 && oscVal > 0 && upX50 == -1 { + // 当AROON大于0且AROON_UP死叉50,平多; + closeDirt = 1 + } + if len(s.LongOrders) > 0 && oscVal < 0 && dnX50 == -1 { + // 当AROON小于0且AROON_DN死叉50,平空; + closeDirt = -1 + } + if closeDirt != 0 { + tag := "xlong" + if closeDirt < 0 { + tag = "xshort" + } + err := s.CloseOrders(&strat.ExitReq{ + Tag: tag, + Dirt: closeDirt, + }) + if err != nil { + log.Error("close fail", zap.Error(err)) + } + } + }, + } +} diff --git a/org/sf/03.go b/org/sf/03.go new file mode 100644 index 0000000..0059d68 --- /dev/null +++ b/org/sf/03.go @@ -0,0 +1,85 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +使用一定周期的变化率均线+价格均线突破作为信号。 +原版只有盈利卖出信号。没有止损。 +tb测试未复现文章中的净值曲线 +*/ + +func SF03V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 100 + lenMa2 = 800 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: lenMa1 + lenMa2, + MinTfScore: 0.75, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma1 := ta.SMA(e.Close, lenMa2) + ma2 := ta.SMA(ma1, lenMa1) + maSub := ma1.Get(0) - ma2.Get(0) + + chg := e.High.To("chg", 0) + if !chg.Cached() { + h, l := e.High.Get(0), e.Low.Get(0) + h1, l1 := e.High.Get(1), e.Low.Get(1) + tr := max(math.Abs(h-h1), math.Abs(l-l1)) + dbf, kbf := 0.0, 0.0 + if h+l > h1+l1 { + dbf = tr + } else { + kbf = tr + } + chgVal := (dbf - kbf) / (dbf + kbf + float64(lenMa1)*2) + chg.Set(chgVal) + } + maChg := ta.SMA(chg, lenMa1) + maChg2 := ta.EMA(chg, lenMa1) + chgVal := chg.Get(0) + maChgSub := maChg.Get(0) - maChg2.Get(0) + + c := e.Close.Get(0) + cSubMa := c - ma1.Get(0) + openDirt := 0.0 + stopLoss := 0.0 + if len(s.LongOrders) == 0 && cSubMa > 0 && maSub > 0 && chgVal > 0 && maChgSub > 0 { + openDirt = 1 + stopLoss = c * 0.95 + } else if len(s.ShortOrders) == 0 && cSubMa < 0 && maSub < 0 && chgVal < 0 && maChgSub < 0 { + openDirt = -1 + stopLoss = c * 1.05 + } + if openDirt != 0 { + com.FastOpen(s, openDirt, stopLoss, stopLoss, 0) + } + if len(s.LongOrders) > 0 && cSubMa < 0 { + for _, od := range s.LongOrders { + if c/od.InitPrice > 1.01 { + _ = s.CloseOrders(&strat.ExitReq{ + Tag: "exitL", + OrderID: od.ID, + }) + } + } + } + if len(s.ShortOrders) > 0 && cSubMa > 0 { + for _, od := range s.ShortOrders { + if c/od.InitPrice < 0.99 { + _ = s.CloseOrders(&strat.ExitReq{ + Tag: "exitS", + OrderID: od.ID, + }) + } + } + } + }, + } +} diff --git a/org/sf/04.go b/org/sf/04.go new file mode 100644 index 0000000..d3716b6 --- /dev/null +++ b/org/sf/04.go @@ -0,0 +1,61 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +最高价突破上轨,且快线高于慢线时,开多。 +使用前n个low作为止损。 +*/ + +func SF04V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 20 + lenMa2 = 230 + X := 3.0 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: lenMa1 + lenMa2, + MinTfScore: 0.75, + OnBar: func(s *strat.StratJob) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, lenMa1) + ma1 := ta.SMA(e.Close, lenMa1) + ma2 := ta.SMA(e.Close, lenMa2) + c := e.Close.Get(0) + upperBand := c + X*atr.Get(0) + lowerBand := c - X*atr.Get(0) + stopLong := e.Low.Get(lenMa2) + stopShort := e.High.Get(lenMa2) + + maSub := ma1.Get(0) - ma2.Get(0) + h, h1 := e.High.Get(0), e.High.Get(1) + l, l1 := e.Low.Get(0), e.Low.Get(1) + + openPrice := 0.0 + if len(s.LongOrders) == 0 && h >= upperBand && maSub > 0 && h >= h1 { + openPrice = max(c, upperBand, e.High.Get(lenMa1)) + } else if len(s.ShortOrders) == 0 && l < lowerBand && maSub < 0 && l <= l1 { + openPrice = -min(c, lowerBand, e.Low.Get(lenMa1)) + } + if openPrice != 0 { + com.FastOpen(s, openPrice, stopLong, stopShort, math.Abs(openPrice)) + } + for _, od := range s.LongOrders { + if l >= od.InitPrice && l <= stopLong { + od.SetStopLoss(&ormo.ExitTrigger{Price: stopLong}) + } + } + for _, od := range s.ShortOrders { + if h <= od.InitPrice && h >= stopShort { + od.SetStopLoss(&ormo.ExitTrigger{Price: stopShort}) + } + } + }, + } +} diff --git a/org/sf/05.go b/org/sf/05.go new file mode 100644 index 0000000..f647dc1 --- /dev/null +++ b/org/sf/05.go @@ -0,0 +1,83 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "math" +) + +/* +策略说明: + + 基于K线加权均值的支撑阻力线的突破系统 + + 1. 支撑线 = 周期范围内的K线加权均值 - ( 最高价 - K线加权均值) + 2. 阻力线 = 周期范围内的K线加权均值 + ( K线加权均值 - 最低价) + 入场 + 1. 当价格向上突破阻力线做多 + 2. 当价格向下突破支撑线做空 + 出场 + 1. 趋势反转即反向突破时出场 + 2. 基于ATR的一定倍数的止盈 +*/ +func SF05V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 250 + trs := 30.0 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: lenMa1, + MinTfScore: 0.75, + OnBar: func(s *strat.StratJob) { + e := s.Env + h, l, c := e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + avgP := (h + l + c*2) / 4 + rsv := e.Close.To("rsv2", 0) + rsv.Set(avgP*2 - l) + rs := ta.Highest(rsv, lenMa1) + stv := e.Close.To("stv", 0) + stv.Set(avgP*2 - h) + st := ta.Lowest(stv, lenMa1) + + atr := ta.ATR(e.High, e.Low, e.Close, lenMa1).Get(0) + + openPrice := 0.0 + if len(s.LongOrders) == 0 && h > rs.Get(0) { + openPrice = max(c, rs.Get(0)) + } else if len(s.ShortOrders) == 0 && l < st.Get(0) { + openPrice = -min(c, st.Get(0)) + } + slVal := atr * trs + takeLong, takeShort := c+slVal, c-slVal + if openPrice != 0 { + openTag := "long" + takeProfit := takeLong + if openPrice < 0 { + openTag = "short" + takeProfit = takeShort + } + err := s.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: openPrice < 0, + Limit: math.Abs(openPrice), + TakeProfit: takeProfit, + }) + if err != nil { + log.Error("open fail", zap.Error(err)) + } + exitDirt := 1 + exitTag := "exitL" + if openPrice > 0 { + exitDirt = -1 + exitTag = "exitS" + } + _ = s.CloseOrders(&strat.ExitReq{ + Tag: exitTag, + Dirt: exitDirt, + }) + } + }, + } +} diff --git a/org/sf/06.go b/org/sf/06.go new file mode 100644 index 0000000..98c4d34 --- /dev/null +++ b/org/sf/06.go @@ -0,0 +1,67 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +/* +同时突破:两个周期的最高价、均线 +跟踪止损退出 +*/ + +func SF06V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 30 + lenMaRate = 3 + lenMa2 = int(float64(lenMa1) * lenMaRate) + lenAtr = max(15, lenMa1/2) + baseAtrLen := lenAtr * 13 / 5 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(lenMa2, baseAtrLen), + MinTfScore: 0.75, + OnStartUp: func(s *strat.StratJob) { + s.More = &MTRSDirt{ + TRS: com.NewTRS(20, 0.5, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + hh1 := ta.Highest(e.High.Back(lenMa1), lenMa1) + hh2 := ta.Highest(e.High.Back(lenMa2), lenMa2) + ll1 := ta.Lowest(e.Low.Back(lenMa1), lenMa1) + ll2 := ta.Lowest(e.Low.Back(lenMa2), lenMa2) + hh1Val, hh2Val, ll1Val, ll2Val := hh1.Get(0), hh2.Get(0), ll1.Get(0), ll2.Get(0) + + c, c1 := e.Close.Get(0), e.Close.Get(1) + + entLong := c > max(hh1Val, hh2Val, e.Close.Get(lenMa1)) + entShort := c < min(ll1Val, ll2Val, e.Close.Get(lenMa1)) + + fltLong := c1 > min(hh1Val, hh2Val) + fltShort := c1 < max(ll1Val, ll2Val) + + m, _ := s.More.(*MTRSDirt) + + openDirt := 0.0 + if len(s.LongOrders) == 0 && hh1Val > hh2Val && entLong && fltLong { + openDirt = 1 + m.TrialLong(c) + } else if len(s.ShortOrders) == 0 && ll1Val < ll2Val && entShort && fltShort { + openDirt = -1 + m.TrialShort(c) + } + + stopLong, stopShort := m.CalcSL2(s, lenAtr, baseAtrLen, 0.5, 0.5) + if openDirt != 0 { + //log.Info(fmt.Sprintf("open, slVal: %.1f%% trRate: %.2f atr: %.1f%%", + // atrBase*atrRate*100/c, atrRate, atrBase*100/c)) + com.FastOpen(s, openDirt, stopLong, stopShort, 0) + } + setAllStopLoss(s, stopLong, stopShort) + }, + } +} diff --git a/org/sf/08.go b/org/sf/08.go new file mode 100644 index 0000000..78b23a5 --- /dev/null +++ b/org/sf/08.go @@ -0,0 +1,75 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +func SF08V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = int(pol.Def("ma1", 139, core.PNorm(10, 300))) + lenMaRate = pol.Def("maRate", 2.47, core.PNorm(1.5, 5)) + lenMa2 = int(float64(lenMa1) * lenMaRate) + lenExtrm = int(pol.Def("extrm", 30, core.PNorm(10, 300))) + + lenAtr = int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + + initSl := pol.Def("initSl", 90, core.PNorm(30, 150)) + slMinRate, slPow, slRate := 0.4, 0.3, 0.6 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(lenMa2+lenMa1, baseAtrLen), + OnStartUp: func(s *strat.StratJob) { + s.More = &MTRSDirt{ + TRS: com.NewTRS(initSl, slMinRate, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma := ta.SMA(e.Close, lenMa2).Get(0) + kk, _, krsv := ta.KDJBy(e.High, e.Low, e.Close, lenMa2, lenMa1, lenMa1, "sma") + kdX := krsv.Cross(kk) + + hh := ta.Highest(e.High, lenExtrm) + ll := ta.Lowest(e.Low, lenExtrm) + hd := e.Close.To("hd", 0) + ld := e.Close.To("ld", 0) + + m, _ := s.More.(*MTRSDirt) + + if kdX == 1 { + hd.Set(hh.Get(0)) + } else if kdX == -1 { + ld.Set(ll.Get(0)) + } + + h, l, c := e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + hdVal1, ldVal1 := hd.Get(1), ld.Get(1) + openPrice := 0.0 + if len(s.LongOrders) == 0 && kdX == 2 && h >= hdVal1 && hdVal1 > 0 && c > ma { + openPrice = hdVal1 + m.TrialLong(hdVal1) + } else if len(s.ShortOrders) == 0 && kdX == -2 && l < ldVal1 && ldVal1 > 0 && c < ma { + openPrice = -ldVal1 + m.TrialShort(ldVal1) + } + + stopLong, stopShort := m.CalcSL2(s, lenAtr, baseAtrLen, slPow, slRate) + if openPrice != 0 { + entPrice := math.Abs(openPrice) + //if (entPrice-c)*openPrice < 0 { + // // 限价入场 + // slVal := stopShort - entPrice + // stopLong, stopShort = entPrice-slVal, entPrice+slVal + //} + com.FastOpen(s, openPrice, stopLong, stopShort, entPrice) + } + setAllStopLoss(s, stopLong, stopShort) + }, + } +} diff --git a/org/sf/10.go b/org/sf/10.go new file mode 100644 index 0000000..1cee4e8 --- /dev/null +++ b/org/sf/10.go @@ -0,0 +1,72 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +type Band10 struct { + Band float64 + PBand float64 + BandUp float64 + BandDn float64 + DAtr float64 +} + +func SF10V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + var lenDayAtr = 30 + var k = 1.0 + //var slRate = 1.5 + //lenAtr = max(15, lenMa1/2) + //baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 30, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &Band10{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 30}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*Band10) + h, l, c := e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + openDirt := 0.0 + if m.Band > m.PBand { + l1, h1 := e.Low.Get(1), e.High.Get(1) + if len(s.LongOrders) == 0 && h >= m.BandUp && l1 < m.BandUp { + openDirt = 1 + } else if len(s.ShortOrders) == 0 && l <= m.BandDn && h1 > m.BandDn { + openDirt = -1 + } + if openDirt != 0 { + stopLong, stopShort := c-m.Band, c+m.Band + com.FastOpen(s, openDirt, stopLong, stopShort, 0) + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*Band10) + h, l, c := e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + m.PBand = m.Band + m.Band = max(h-c, c-l) * k + m.BandUp, m.BandDn = c+m.Band, c-m.Band + m.DAtr = ta.ATR(e.High, e.Low, e.Close, lenDayAtr).Get(0) * 100 / c + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + m, _ := s.More.(*Band10) + //rate := math.Sqrt(m.DAtr) + rate := m.DAtr * 5 + return strat.CalcDrawDownExitRate(maxChg / rate) + }, + } +} diff --git a/org/sf/14.go b/org/sf/14.go new file mode 100644 index 0000000..987cbe6 --- /dev/null +++ b/org/sf/14.go @@ -0,0 +1,80 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +type More14 struct { + *com.TRS + dirt int + btm float64 + top float64 + hh float64 + ll float64 +} + +func SF14V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr = 50 + atrAlpha := 20.0 + atrN := 3.0 + lenAtr2 := 15 + baseAtrLen := int(math.Round(float64(lenAtr2) * 4.3)) + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: baseAtrLen, + MinTfScore: 0.75, + OnStartUp: func(s *strat.StratJob) { + s.More = &More14{ + TRS: com.NewTRS(30, 0.5, 5), + dirt: 1, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*More14) + atrVal := ta.ATR(e.High, e.Low, e.Close, lenAtr).Get(0) + hl2 := (e.High.Get(0) + e.Low.Get(0)) / 2 + c, o := e.Close.Get(0), e.Open.Get(0) + h, l := e.High.Get(0), e.Low.Get(0) + curBtm := hl2 - atrVal*atrAlpha + curTop := hl2 + atrVal*atrAlpha + + openDirt := 0.0 + if m.btm == 0 || math.IsNaN(m.btm) { + m.btm = curBtm + m.top = curTop + } else { + if m.dirt == 1 && len(s.LongOrders) == 0 && h >= m.hh { + openDirt = 1 + m.TrialLong(c) + } else if m.dirt == -1 && len(s.ShortOrders) == 0 && l <= m.ll { + openDirt = -1 + m.TrialShort(c) + } + if m.dirt == -1 && c > m.top { + m.dirt = 1 + } else if m.dirt == 1 && c < m.btm { + m.dirt = -1 + } + if c > m.btm { + m.btm = max(m.btm, curBtm) + } else { + m.top = min(m.top, curTop) + } + } + m.hh = o + atrVal*atrN + m.ll = o - atrVal*atrN + + slLong, slShort := m.CalcSL2(s, lenAtr2, baseAtrLen, 0.5, 0.5) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + }, + } +} diff --git a/org/sf/17.go b/org/sf/17.go new file mode 100644 index 0000000..a860906 --- /dev/null +++ b/org/sf/17.go @@ -0,0 +1,67 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +func SF17V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = int(pol.Def("ma1", 62, core.PNorm(10, 300))) + lenMaRate = pol.Def("maRate", 4.34, core.PNorm(1.5, 5)) + lenMa2 = int(float64(lenMa1) * lenMaRate) + + lenAtr = int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + + initSl := pol.Def("initSl", 101, core.PNorm(30, 150)) + slMinRate, slPow, slRate := 0.4, 0.3, 0.6 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(lenMa2*2, baseAtrLen), + OnStartUp: func(s *strat.StratJob) { + s.More = &MTRSDirt{ + TRS: com.NewTRS(initSl, slMinRate, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + avgPrice := ta.AvgPrice(e) + ma1 := ta.SMA(avgPrice, lenMa1) + ma2 := ta.EMA(ma1, lenMa1) + maLong1 := ta.SMA(avgPrice, lenMa2) + maLong2 := ta.EMA(maLong1, lenMa2) + + maRange := ma1.Sub(maLong2) + maRangeE := ta.EMA(maRange, lenMa1) + hh := ta.Highest(e.High, lenMa2) + ll := ta.Lowest(e.Low, lenMa2) + + maRangeVal, maRangeEVal := maRange.Get(0), maRangeE.Get(0) + ma1Val, ma2Val, maLong1Val, maLong2Val := ma1.Get(0), ma2.Get(0), maLong1.Get(0), maLong2.Get(0) + maSub, longSub, otherSub := ma1Val-ma2Val, maLong1Val-maLong2Val, ma2Val-maLong2Val + rangeSub := maRangeVal - maRangeEVal + hhSub, llSub := e.High.Get(0)-hh.Get(0), e.Low.Get(0)-ll.Get(0) + + m, _ := s.More.(*MTRSDirt) + + openDirt := 0.0 + if len(s.LongOrders) == 0 && hhSub >= 0 && rangeSub > 0 && maRangeVal > 0 && maSub > 0 && longSub > 0 && otherSub > 0 { + openDirt = 1 + m.TrialLong(e.Close.Get(0)) + } else if len(s.ShortOrders) == 0 && llSub <= 0 && rangeSub < 0 && maRangeVal < 0 && maSub < 0 && longSub < 0 && otherSub < 0 { + openDirt = -1 + m.TrialShort(e.Close.Get(0)) + } + if openDirt != 0 { + com.FastOpen(s, openDirt, 0, 0, 0) + } + + stopLong, stopShort := m.CalcSL2(s, lenAtr, baseAtrLen, slPow, slRate) + setAllStopLoss(s, stopLong, stopShort) + }, + } +} diff --git a/org/sf/18.go b/org/sf/18.go new file mode 100644 index 0000000..a875609 --- /dev/null +++ b/org/sf/18.go @@ -0,0 +1,137 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "slices" +) + +/*策略思想: + +1.使用变量将MACD的快慢均线交叉点记录, +然后获取上次交叉到本次交叉之间的周期数。 +计算此周期内的高低点, +当价格突破交叉后的高低点时进场,当MACD金叉(死叉)时出场; +2.当MACD出现顶底背离时,开多开空; + +核心计算:1.MACD交叉状态记录 + 2.构建周期内的高低点区间 + 3.描述背离状态,同时保存结果; + +多头进场:1.最近一个MACD信号是金叉,突破周期内高点; + 2. 出现底背离时,开多; +空头进场:1.最近一个MACD信号是死叉,突破周期内低点; + 2.出现顶背离时,开空; + +2024/05/24 anyongjin +上面是原始逻辑,但测试发现顶底背离时按传统逻辑反而稳定亏损,改成顶背离开多,底背离开空反而盈利 + +*/ +//使用周期:1小时周期 + +type More18 struct { + *com.TRS + LHNum int // 上次金叉的序号 + LLNum int // 上次死叉的序号 + XHigh float64 // 上次交叉到本次交叉之间最高点,突破开多 + XLow float64 // 上次交叉到本次交叉之间最低点,突破开空 + ExmDirt int // 上次突破最高最低的方向 + + LHPrice float64 // 上次上穿时的价格 + LLPrice float64 // 上次下穿时的价格 + LHMacd float64 // 上次上穿时macd的值 + LLMacd float64 // 上次下穿时macd的值 + RevDirt int // 上次背离的方向 +} + +func SF18V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenN := int(pol.Def("n", 8, core.PNorm(2, 30))) + lenX := int(pol.Def("x", 10, core.PNorm(5, 50))) + lenAtr = int(pol.Def("atr", 15, core.PNorm(7, 40))) + lenAtrB := int(float64(lenAtr) * 4.3) + + initSl := pol.Def("initSl", 100, core.PNorm(20, 150)) + slMinRate, slPow, slRate := 0.4, 0.3, 0.6 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(lenAtrB, lenN*9), + OnStartUp: func(s *strat.StratJob) { + s.More = &More18{ + TRS: com.NewTRS(initSl, slMinRate, 8), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + macd, sig := ta.MACD(e.Close, 4*lenN, 9*lenN, 3*lenN) + macdX := macd.Cross(sig) + c := e.Close.Get(0) + + m, _ := s.More.(*More18) + openDirt := 0.0 + + if macdX == 1 { + x2Dist := (e.BarNum - m.LHNum) * 7 / 5 + m.XHigh = slices.Max(e.High.Range(1, max(lenX, x2Dist))) + cHMacd := sig.Get(0) + m.LHNum = e.BarNum + + if c < m.LHPrice && cHMacd > m.LHMacd && cHMacd < 0 { + // 底背离,但开空 + openDirt = -1 + } + m.LHPrice = c + m.LHMacd = cHMacd + } else if macdX == -1 { + x2Dist := (e.BarNum - m.LLNum) * 7 / 5 + m.XLow = slices.Min(e.Low.Range(1, max(lenX, x2Dist))) + cLMacd := sig.Get(0) + m.LLNum = e.BarNum + + if c > m.LLPrice && cLMacd < m.LLMacd && cLMacd > 0 { + // 顶背离,但开多 + openDirt = 1 + } + m.LLPrice = c + m.LLMacd = cLMacd + } + // 这里是控制是否过滤背离重复开单的。注释掉不过滤的话,开单数增加一倍,利润增加一倍 + //if openDirt != 0 { + // if int(openDirt) == m.RevDirt { + // openDirt = 0 + // } else { + // m.RevDirt = int(openDirt) + // } + //} + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrVal := atr.Get(0) + if macdX >= 2 && c >= m.XHigh+atrVal*2 && m.XHigh > 0 && m.ExmDirt != 1 { + // 金叉,突破两次交叉高点,开多 + openDirt = 1 + m.ExmDirt = 1 + } else if macdX <= -2 && c <= m.XLow-atrVal*2 && m.XLow > 0 && m.ExmDirt != -1 { + // 死叉,突破两次交叉低点,开空 + openDirt = -1 + m.ExmDirt = -1 + } + + if openDirt == 1 { //&& len(s.LongOrders) == 0 { + m.TrialLong(e.Close.Get(0)) + } else if openDirt == -1 { //} && len(s.ShortOrders) == 0 { + m.TrialShort(e.Close.Get(0)) + } else { + openDirt = 0 + } + + slLong, slShort := m.CalcSL(s, atr, lenAtr, lenAtrB, slPow, slRate) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + }, + } +} diff --git a/org/sf/19.go b/org/sf/19.go new file mode 100644 index 0000000..71a71b6 --- /dev/null +++ b/org/sf/19.go @@ -0,0 +1,73 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +type More19 struct { + *com.TRS + UpHigh float64 // 金叉时最高价 + DnLow float64 // 死叉时最低价 +} + +func SF19V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 30 + lenMa2 = lenMa1 * 3 + lenExtrm = lenMa1 + lenAtr = 15 + baseAtrLen := lenAtr * 13 / 5 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(baseAtrLen, lenMa2), + MinTfScore: 0.75, + OnStartUp: func(s *strat.StratJob) { + s.More = &More19{ + TRS: com.NewTRS(30, 0.5, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + mid := ta.AvgPrice(e) + ma1 := ta.VWMA(mid, e.Volume, lenMa1) + ma2 := ta.VWMA(mid, e.Volume, lenMa2) + crx := ma1.Cross(ma2) + + hh, ll := ta.Highest(e.High, lenExtrm), ta.Lowest(e.Low, lenExtrm) + h, l := e.High.Get(0), e.Low.Get(0) + coSub := e.Close.Get(0) - e.Open.Get(0) + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + m, _ := s.More.(*More19) + + if crx == 1 { + // 上穿,金叉; + m.UpHigh = hh.Get(0) + } else if crx == -1 { + // 下穿,死叉 + m.DnLow = ll.Get(0) + } + openDirt := 0.0 + if crx >= 1 && len(s.LongOrders) == 0 && coSub > 0 { + // 金叉,突破最高,当前是阳线 + if h >= max(m.UpHigh, hh.Get(0)) { + openDirt = 1 + } + } else if crx <= -1 && len(s.ShortOrders) == 0 && coSub < 0 { + // 死叉,突破最低,阴线 + if l <= min(m.DnLow, ll.Get(0)) { + openDirt = -1 + } + } + + slLong, slShort := m.CalcSL(s, atr, lenAtr, baseAtrLen, 0.5, 0.5) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + }, + } +} diff --git a/org/sf/21.go b/org/sf/21.go new file mode 100644 index 0000000..f3c96a2 --- /dev/null +++ b/org/sf/21.go @@ -0,0 +1,68 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +初始版本: +本质上是简化版RSI,超买做空,超卖做多;vwma跟踪止损出场 + +修改版本: +RSI在75~93开多;在7-25开空;同时结合ma过滤;跟踪止损出场。 +*/ + +func SF21V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = int(pol.Def("ma1", 10, core.PNorm(10, 300))) + lenMaRate = pol.Def("maRate", 3, core.PNorm(1.5, 5)) + lenMa2 = int(float64(lenMa1) * lenMaRate) + lenExtrm = int(pol.Def("extrm", 15, core.PNorm(10, 400))) + + lenAtr = int(pol.Def("atr", 15, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + + initSl := pol.Def("initSl", 60, core.PNorm(20, 200)) + slMinRate, slPow, slRate := 0.4, 0.3, 0.6 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(baseAtrLen, lenMa2), + OnStartUp: func(s *strat.StratJob) { + s.More = &MTRSDirt{ + TRS: com.NewTRS(initSl, slMinRate, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + rsi := ta.RSI50(e.Close, lenExtrm).Get(0) + rsiAbs := math.Abs(rsi) + ma1 := ta.SMA(e.Close, lenMa1) + ma2 := ta.SMA(e.Close, lenMa2) + + maSub := ma1.Get(0) - ma2.Get(0) + + c := e.Close.Get(0) + m, _ := s.More.(*MTRSDirt) + openDirt := 0.0 + if rsiAbs > 25 && rsiAbs < 43 && rsi*maSub > 0 && s.OrderNum == 0 { + if rsi < 0 { + m.TrialShort(c) + openDirt = -1 + } else { + m.TrialLong(c) + openDirt = 1 + } + } + slLong, slShort := m.CalcSL2(s, lenAtr, baseAtrLen, slPow, slRate) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + }, + } +} diff --git a/org/sf/30_v0.go b/org/sf/30_v0.go new file mode 100644 index 0000000..80feb1f --- /dev/null +++ b/org/sf/30_v0.go @@ -0,0 +1,75 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +原始版本:止损空间TRS使用固定百分比,测试在4h上收益较好,低于4h不够稳定。 +*/ + +func SF30V0(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 100 + lenMaRate = 3 + lenMa2 = int(float64(lenMa1) * lenMaRate) + var trs = 0.01 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: lenMa2, + MinTfScore: 0.75, + OnStartUp: func(s *strat.StratJob) { + s.More = &MTRSDirt{ + TRS: com.NewTRS(1, 0.301, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + p := ta.AvgPrice(e) + ma1 := ta.SMA(p, lenMa1) + slowMa1 := ta.EMA(ma1, lenMa1) + ma2 := ta.SMA(p, lenMa2) + slowMa2 := ta.EMA(ma2, lenMa2) + dlh := slowMa1.Sub(slowMa2) + maDlh := ta.EMA(dlh, lenMa1) + + hh := ta.Highest(e.High, lenMa2) + ll := ta.Lowest(e.Low, lenMa2) + h, l, c := e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + + m, _ := s.More.(*MTRSDirt) + + vMa1, vSlowMa1 := ma1.Get(0), slowMa1.Get(0) + vMa2, vSlowMa2 := ma2.Get(0), slowMa2.Get(0) + sub1, sub2, subSlow := vMa1-vSlowMa1, vMa2-vSlowMa2, vSlowMa1-vSlowMa2 + vDlh, vMaDlh := dlh.Get(0), maDlh.Get(0) + subDlh := vDlh - vMaDlh + curDirt := 0 + if sub1 > 0 && sub2 > 0 && subSlow > 0 && vDlh > 0 && subDlh > 0 { + curDirt = 1 + } else if sub1 < 0 && sub2 < 0 && subSlow < 0 && vDlh < 0 && subDlh < 0 { + curDirt = -1 + } + + openPrice := float64(0) + if m.dirt == 1 && h >= hh.Get(1) && len(s.LongOrders) == 0 { + openPrice = max(e.Open.Get(0), hh.Get(1)) + m.TrialLong(min(openPrice, c)) + } else if m.dirt == -1 && l <= ll.Get(1) && len(s.ShortOrders) == 0 { + openPrice = min(e.Open.Get(0), ll.Get(1)) + m.TrialShort(max(openPrice, c)) + openPrice = -openPrice + } + m.dirt = curDirt + + stopLong, stopShort := m.Calc(s, c*trs) + if openPrice != 0 { + com.FastOpen(s, openPrice, stopLong, stopShort, math.Abs(openPrice)) + } + setAllStopLoss(s, stopLong, stopShort) + }, + } +} diff --git a/org/sf/30_v1.go b/org/sf/30_v1.go new file mode 100644 index 0000000..d01624b --- /dev/null +++ b/org/sf/30_v1.go @@ -0,0 +1,78 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +func SF30V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = int(pol.Def("ma1", 54, core.PNorm(10, 300))) + lenMaRate = pol.Def("maRate", 1.82, core.PNorm(1.5, 5)) + lenMa2 = int(float64(lenMa1) * lenMaRate) + + lenAtr = int(pol.Def("atr", 20, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + + initSl := pol.Def("initSl", 120, core.PNorm(20, 160)) + slMinRate, slPow, slRate := 0.4, 0.3, 0.6 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(lenMa2, baseAtrLen), + OnStartUp: func(s *strat.StratJob) { + s.More = &MTRSDirt{ + TRS: com.NewTRS(initSl, slMinRate, 5), + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + p := ta.AvgPrice(e) + ma1 := ta.SMA(p, lenMa1) + slowMa1 := ta.EMA(ma1, lenMa1) + ma2 := ta.SMA(p, lenMa2) + slowMa2 := ta.EMA(ma2, lenMa2) + dlh := slowMa1.Sub(slowMa2) + maDlh := ta.EMA(dlh, lenMa1) + + hh := ta.Highest(e.High, lenMa2) + ll := ta.Lowest(e.Low, lenMa2) + h, l, c := e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + + m, _ := s.More.(*MTRSDirt) + + vMa1, vSlowMa1 := ma1.Get(0), slowMa1.Get(0) + vMa2, vSlowMa2 := ma2.Get(0), slowMa2.Get(0) + sub1, sub2, subSlow := vMa1-vSlowMa1, vMa2-vSlowMa2, vSlowMa1-vSlowMa2 + vDlh, vMaDlh := dlh.Get(0), maDlh.Get(0) + subDlh := vDlh - vMaDlh + curDirt := 0 + if sub1 > 0 && sub2 > 0 && subSlow > 0 && vDlh > 0 && subDlh > 0 { + curDirt = 1 + } else if sub1 < 0 && sub2 < 0 && subSlow < 0 && vDlh < 0 && subDlh < 0 { + curDirt = -1 + } + + openPrice := float64(0) + if m.dirt == 1 && h >= hh.Get(1) && len(s.LongOrders) == 0 { + openPrice = max(e.Open.Get(0), hh.Get(1)) + m.TrialLong(min(openPrice, c)) + } else if m.dirt == -1 && l <= ll.Get(1) && len(s.ShortOrders) == 0 { + openPrice = min(e.Open.Get(0), ll.Get(1)) + m.TrialShort(max(openPrice, c)) + openPrice = -openPrice + } + m.dirt = curDirt + + stopLong, stopShort := m.CalcSL2(s, lenAtr, baseAtrLen, slPow, slRate) + if openPrice != 0 { + //log.Info("open:", zap.Float64("slVal", slVal/c), zap.Float64("trRate", atrRate), + // zap.Float64("atr", atrBase/c)) + com.FastOpen(s, openPrice, stopLong, stopShort, math.Abs(openPrice)) + } + setAllStopLoss(s, stopLong, stopShort) + }, + } +} diff --git a/org/sf/33.go b/org/sf/33.go new file mode 100644 index 0000000..19358a2 --- /dev/null +++ b/org/sf/33.go @@ -0,0 +1,103 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +type More33 struct { + *com.TRS + openD float64 // 当日开盘价 + trend int +} + +func SF33V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenBid := int(pol.Def("ma1", 9, core.PNorm(10, 300))) + + lenAtr = int(pol.Def("atr", 14, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.0) + + lenTit := int(pol.Def("tit", 5, core.PNorm(3, 30))) + wid1 := pol.Def("wid", 1, core.PNorm(0.5, 5)) + wid2 := wid1 + + initSl := pol.Def("initSl", 60, core.PNorm(20, 150)) + slMinRate, slPow, slRate := 0.4, 0.3, 0.6 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(baseAtrLen, lenTit, lenBid), + OnStartUp: func(s *strat.StratJob) { + s.More = &More33{ + TRS: com.NewTRS(initSl, slMinRate, 5), + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 2}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + hh := ta.Highest(e.High, lenBid) + ll := ta.Lowest(e.Low, lenBid) + abid := ta.EMA(hh.Sub(ll), lenBid) + + mid := ta.EMA(e.High, lenTit).Add(ta.EMA(e.Low, lenTit)).Mul(0.5) + bidN := abid.Mul(wid1) + up := mid.Add(bidN) + dn := mid.Sub(bidN) + + m, _ := s.More.(*More33) + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + dhh := m.openD + atr.Get(0)*wid2 + dll := m.openD - atr.Get(0)*wid2 + + h, l := e.High.Get(0), e.Low.Get(0) + xLong, xShort := false, false + if m.trend != 1 && h >= max(up.Get(0), hh.Get(0)) { + m.trend = 1 + xLong = true + } + if m.trend != -1 && l <= min(dn.Get(0), ll.Get(0)) { + m.trend = -1 + xShort = true + } + + uppVal := up.Get(0) + dnnVal := dn.Get(0) + // 当trend=1的时候,也就是多头判定后,如果出现dn线比前一个还小情况,那么用前一个大的,不再减小dn线 + if !xLong && m.trend > 0 && dnnVal < dn.Get(1) { + dnnVal = dn.Get(1) + } + // 当trend=-1的时候,也就是空头判定后,如果出现up线比前一个还大情况,那么用前一个大的,不再增加up线 + if !xShort && m.trend < 0 && uppVal > up.Get(1) { + uppVal = up.Get(1) + } + + c := e.Close.Get(0) + openDirt := 0.0 + if m.trend == 1 && c > dnnVal && h >= dhh && len(s.LongOrders) == 0 { + openDirt = 1 + m.TrialLong(c) + } + if m.trend == -1 && c < uppVal && l < dll && len(s.ShortOrders) == 0 { + openDirt = -1 + m.TrialShort(c) + } + slLong, slShort := m.CalcSL(s, atr, lenAtr, baseAtrLen, slPow, slRate) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*More33) + m.openD = e.Open.Get(0) + }, + } +} diff --git a/org/sf/35.go b/org/sf/35.go new file mode 100644 index 0000000..f1a2d57 --- /dev/null +++ b/org/sf/35.go @@ -0,0 +1,112 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +type More35 struct { + yma float64 // 单边权重均线 + openD float64 + longMin float64 + shortMax float64 + dirt float64 // 1超买,-1超卖 + + *com.EFTRS +} + +func SF35V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenSmt := int(pol.Def("ma1", 12, core.PNorm(10, 300))) + + lenAtr = int(pol.Def("atr", 14, core.PNorm(7, 40))) + lenAtBig := int(float64(lenAtr) * 4) + + mulB := pol.Def("b", 3, core.PNorm(1.2, 10)) + mulD := pol.Def("d", 2, core.PNorm(1.2, 10)) + slMul := pol.Def("sl", 3, core.PNorm(1.2, 10)) + lenEff := int(pol.Def("eff", 10, core.PNorm(3, 100))) + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: max(lenAtBig, lenSmt), + OnStartUp: func(s *strat.StratJob) { + s.More = &More35{ + dirt: 1, + EFTRS: com.NewEFTRS(lenEff), + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 2}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*More35) + // 计算均线 + closes := e.Close.Range(0, lenSmt+1) + c := closes[0] + if len(closes) == 1 || math.IsNaN(c) { + m.yma = c + } else { + upSum, dnSum := float64(0), float64(0) + num := len(closes) - 1 + for i, v := range closes[:num] { + sub := v - closes[i+1] + if sub > 0 { + upSum += sub + } else { + dnSum -= sub + } + } + // cmo: 度量单边系数;值域0-1;1表示单边涨或跌,0表示震荡 + cmo := math.Abs(upSum-dnSum) / (upSum + dnSum) + weight := 2 * cmo / (1 + float64(lenSmt)) + m.yma = c*weight + m.yma*(1-weight) + } + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrMx := ta.Highest(atr, lenAtBig).Get(0) + + // 计算上下轨 + longMin, shortMax := m.yma-atrMx*mulB, m.yma+atrMx*mulB + if m.yma > m.longMin { + longMin = max(longMin, m.longMin) + } + if m.yma < m.shortMax { + shortMax = min(shortMax, m.shortMax) + } + if m.dirt == -1 && m.yma > m.shortMax { + m.dirt = 1 + } else if m.dirt == 1 && m.yma < m.longMin { + m.dirt = -1 + } + m.longMin = longMin + m.shortMax = shortMax + + hh, ll := m.openD+atrMx*mulD, m.openD-atrMx*mulD + + openDirt := 0.0 + if m.dirt == 1 && c >= max(hh, longMin) && len(s.LongOrders) == 0 { + openDirt = 1 + } else if m.dirt == -1 && c <= min(ll, shortMax) && len(s.ShortOrders) == 0 { + openDirt = -1 + } + + // 根据ER效率计算止损 + slLong, slShort := m.Calc(s, atrMx*slMul) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*More35) + m.openD = e.Open.Get(0) + }, + } +} diff --git a/org/sf/36.go b/org/sf/36.go new file mode 100644 index 0000000..55ceb53 --- /dev/null +++ b/org/sf/36.go @@ -0,0 +1,138 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +无输出信号 +*/ + +type More36 struct { + pdmS float64 + mdmS float64 + pdiS float64 + mdiS float64 + vma float64 + + support float64 + stress float64 + + dirt int + openD float64 + + *com.EFTRS +} + +func SF36V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr = 14 + lenAtBig := lenAtr * 17 / 4 + alpha := 6.0 + mulB, mulD, slMul := 2.0, 2.0, 2.0 + lenEff := 10 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: lenAtBig, + MinTfScore: 0.75, + OnStartUp: func(s *strat.StratJob) { + s.More = &More36{ + dirt: 1, + EFTRS: com.NewEFTRS(lenEff), + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 2}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*More36) + k := 1 / alpha + c, c1 := e.Close.Get(0), e.Close.Get(1) + pdm := max(c-c1, 0) + mdm := max(c1-c, 0) + m.pdmS = (1-k)*m.pdmS + k*pdm + m.mdmS = (1-k)*m.mdmS + k*mdm + pdi := m.pdmS / (m.pdmS + m.mdmS) + mdi := m.mdmS / (m.pdmS + m.mdmS) + + m.pdiS = (1-k)*m.pdiS + k*pdi + m.mdiS = (1-k)*m.mdiS + k*mdi + + iS := e.Close.To("iS", 0) + if !iS.Cached() { + if iS.Len() == 0 { + iS.Append(0) + } else { + val := (1-k)*iS.Get(0) + k*math.Abs(m.pdiS-m.mdiS)/(m.pdiS+m.mdiS) + iS.Append(val) + } + } + isVal := iS.Get(0) + + hhv := ta.Highest(iS, int(alpha)).Get(0) + llv := ta.Lowest(iS, int(alpha)).Get(0) + vI := (isVal - llv) / (hhv - llv) + + if m.vma == 0 { + m.vma = c + } else { + m.vma = (1-k*vI)*m.vma + k*vI*c + } + + atr := ta.Lowest(ta.ATR(e.High, e.Low, e.Close, lenAtBig), lenAtr) + atrV, atrV2 := atr.Get(0), atr.Get(1) + support := m.vma - atrV*mulB + stress := m.vma + atrV*mulB + if m.vma > m.support { + support = max(m.support, support) + } + if m.vma < m.stress { + stress = min(m.stress, stress) + } + + var dirt = m.dirt + if m.dirt == -1 && m.vma > m.stress { + dirt = 1 + } else if m.dirt == 1 && m.vma < m.support { + dirt = -1 + } + hh := e.Close.To("hh", 0) + ll := e.Close.To("ll", 0) + if !hh.Cached() { + hh.Append(m.openD + atrV2*mulD) + } + if !ll.Cached() { + ll.Append(m.openD - atrV2*mulD) + } + upHH := e.Close.Cross(hh) + dnLL := e.Close.Cross(ll) + + openDirt := 0.0 + if m.dirt == 1 && upHH == 2 && c > m.support && len(s.LongOrders) == 0 { + openDirt = 1 + } else if m.dirt == -1 && dnLL == -2 && c < m.stress && len(s.ShortOrders) == 0 { + openDirt = -1 + } + slLong, slShort := m.Calc(s, atrV2*slMul) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + + m.support = support + m.stress = stress + m.dirt = dirt + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*More36) + m.openD = e.Open.Get(0) + }, + } +} diff --git a/org/sf/37.go b/org/sf/37.go new file mode 100644 index 0000000..07e33e2 --- /dev/null +++ b/org/sf/37.go @@ -0,0 +1,97 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +/* +交易信号很少 +*/ + +type More37 struct { + tup float64 + tdown float64 + dirt int + + madd float64 + openD float64 + *com.EFTRS +} + +func SF37V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + length := 20 + lenMad := 5 + mulB, mulD, slMul := 2.0, 2.0, 2.0 + lenEff := 10 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: length, + MinTfScore: 0.75, + OnStartUp: func(s *strat.StratJob) { + s.More = &More37{ + dirt: 1, + EFTRS: com.NewEFTRS(lenEff), + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", lenMad}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*More37) + half := (length - 1) / 2 + c1 := e.Close.Get(1) + ayami := ta.EMA(e.Close.Add(e.Close.Sub(e.Close.Back(half))), length) + ayaV, ayaV1 := ayami.Get(0), ayami.Get(1) + + atr := ta.Highest(ta.ATR(e.High, e.Low, e.Close, length), length).Get(1) + solid := ta.Highest(ta.SMA(e.Close.Sub(e.Open).Abs(), length), length) + up := ayami.Get(0) + mulB*solid.Get(1) + dn := ayami.Get(0) - mulB*solid.Get(1) + + tup, tdown, dirt := float64(0), float64(0), m.dirt + if m.tup == 0 || ayaV1 <= m.tup || dn > m.tup { + tup = dn + } + if m.tdown == 0 || ayaV1 >= m.tdown || up < m.tdown { + tdown = up + } + if ayaV > m.tdown { + dirt = 1 + } else if ayaV < m.tup { + dirt = -1 + } + + hh := m.openD + solid.Get(0)*mulD + ll := m.openD - solid.Get(0)*mulD + + openDirt := 0.0 + if m.dirt == 1 && len(s.LongOrders) == 0 && c1 > max(m.madd, hh) { + openDirt = 1 + } else if m.dirt == -1 && len(s.ShortOrders) == 0 && c1 < min(m.madd, ll) { + openDirt = -1 + } + + slLong, slShort := m.Calc(s, atr*slMul) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + + m.tup = tup + m.tdown = tdown + m.dirt = dirt + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*More37) + m.madd = ta.SMA(e.Close, lenMad).Get(0) + m.openD = e.Open.Get(0) + }, + } +} diff --git a/org/sf/38.go b/org/sf/38.go new file mode 100644 index 0000000..cd9cfef --- /dev/null +++ b/org/sf/38.go @@ -0,0 +1,98 @@ +package sf + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +type More38 struct { + upper float64 + lower float64 + dirt int + + openD float64 + *com.EFTRS +} + +func SF38V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + lenMa1 = 4 + lenMa2 = 16 + lenC := 50 + lenMad := 5 + lenAtr = 20 + length := 20 + mulB, mulD, slMul := 2.0, 2.0, 2.0 + lenEff := 10 + + return &strat.TradeStrat{ + Version: 1, + WarmupNum: length, + MinTfScore: 0.75, + OnStartUp: func(s *strat.StratJob) { + s.More = &More38{ + dirt: 1, + EFTRS: com.NewEFTRS(lenEff), + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", lenMad}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*More38) + c1 := e.Close.Get(1) + ma1 := ta.SMA(e.Close, lenMa1) + ma2 := ta.SMA(e.Close, lenMa2) + avgMa := e.Close.To("avgMa", 0) + if !avgMa.Cached() { + avgMa.Append((ma1.Get(0) + ma2.Get(0)) / 2) + } + maVal := ta.SMA(avgMa, lenC).Get(0) + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + solid := ta.Highest(ta.SMA(e.Close.Sub(e.Open).Abs(), length), length) + + up := maVal + solid.Get(0)*mulB + dn := maVal - solid.Get(0)*mulB + + upper, lower := m.upper, m.lower + if c1 > upper && up > upper { + upper = up + } + if c1 < lower && dn < lower { + lower = dn + } + if c1 > lower { + m.dirt = 1 + } else if c1 < upper { + m.dirt = -1 + } + + hh := m.openD + solid.Get(0)*mulD + ll := m.openD - solid.Get(0)*mulD + + openDirt := 0.0 + if m.dirt == 1 && c1 >= max(hh, upper) && len(s.LongOrders) == 0 { + openDirt = 1 + } else if m.dirt == -1 && c1 <= min(ll, lower) && len(s.ShortOrders) == 0 { + openDirt = -1 + } + slLong, slShort := m.Calc(s, atr.Get(1)*slMul) + if openDirt != 0 { + com.FastOpen(s, openDirt, slLong, slShort, 0) + } + setAllStopLoss(s, slLong, slShort) + + m.upper = upper + m.lower = lower + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*More38) + m.openD = e.Open.Get(0) + }, + } +} diff --git a/org/sf/common.go b/org/sf/common.go new file mode 100644 index 0000000..475e590 --- /dev/null +++ b/org/sf/common.go @@ -0,0 +1,50 @@ +package sf + +import ( + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" +) + +var ( + lenMa1 = 10 // 快线MA的周期 + lenMaRate = 2.0 // 慢线MA相对快线的倍率 + lenMa2 = int(float64(lenMa1) * lenMaRate) + lenAtr = 14 + lenExtrm = 50 // 查找极值点的周期 +) + +type MTRSDirt struct { + dirt int // 1开多 -1开空 + *com.TRS +} + +func init() { + strat.AddStratGroup("sf", map[string]strat.FuncMakeStrat{ + "01": SF01V2, + "02": SF02V1, + "03": SF03V1, + "04": SF04V1, + "30": SF30V1, + "05": SF05V1, + "06": SF06V1, + "08": SF08V1, + "10": SF10V1, + "14": SF14V1, + "17": SF17V1, + "18": SF18V1, + "19": SF19V1, + "21": SF21V1, + "33": SF33V1, + "35": SF35V1, + "36": SF36V1, + "37": SF37V1, + "38": SF38V1, + }) +} + +func setAllStopLoss(s *strat.StratJob, slLong, slShort float64) { + s.SetAllStopLoss(core.OdDirtLong, &ormo.ExitTrigger{Price: slLong}) + s.SetAllStopLoss(core.OdDirtShort, &ormo.ExitTrigger{Price: slShort}) +} diff --git a/ptn/all/main.go b/ptn/all/main.go new file mode 100644 index 0000000..e92d149 --- /dev/null +++ b/ptn/all/main.go @@ -0,0 +1,18 @@ +package all + +import ( + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/ptn" + "github.com/banbox/banstagy/ptn/hammer" +) + +func init() { + strat.AddStratGroup("ptn", map[string]strat.FuncMakeStrat{ + "hm1": hammer.MainV1, + "hm2": hammer.MainV2, + "hm3": hammer.MainV3, + "hm4": hammer.MainV4, + "hm5": hammer.MainV5, + "tunMo": ptn.TunMo, + }) +} diff --git a/ptn/classic.go b/ptn/classic.go new file mode 100644 index 0000000..7e010fc --- /dev/null +++ b/ptn/classic.go @@ -0,0 +1,62 @@ +package ptn + +import ( + ta "github.com/banbox/banta" + "math" +) + +func getOHLC(e *ta.BarEnv, i int) (float64, float64, float64, float64) { + return e.Open.Get(i), e.High.Get(i), e.Low.Get(i), e.Close.Get(i) +} + +type EngulfingRes struct { + Bottom float64 + Top float64 +} + +func makeEngulfingRes(dirt int, minVal, maxVal float64) *PtnRes { + return &PtnRes{ + Type: PtnEngulfing, + Dirt: dirt, + Confidence: 1, + Data: &EngulfingRes{ + Bottom: minVal, + Top: maxVal, + }, + } +} + +/* +检查是否是吞没形态 +1 看涨吞没 -1看跌吞没 +*/ +func isEngulfing(e *ta.BarEnv) *PtnRes { + o, h, l, c := getOHLC(e, 0) + o1, h1, l1, c1 := getOHLC(e, 1) + solid := math.Abs(o - c) + if solid/(h-l) < 0.6 { + // 第二个蜡烛实体至少60% + return nil + } + solid1 := math.Abs(o1 - c1) + if solid1/(h1-l1) <= 0.13 && solid > solid1*3 { + // 第一个是十字星,直接符合 + if o > c { + return makeEngulfingRes(-1, min(l, l1), max(h, h1)) + } else { + return makeEngulfingRes(1, min(l, l1), max(h, h1)) + } + } + if o < c { + // 后一个是阳线,要求前一个是阴线 + if o1 > c1 && o1 <= c && c1 >= o { + // 前开<收盘 & 前收 > 开盘 + return makeEngulfingRes(1, min(l, l1), max(h, h1)) + } + } else if o1 < c1 && c1 <= o && o1 >= c { + // 后一个阴线,要求前一个阳线 + // 前收<开盘 & 前开 > 收盘 + return makeEngulfingRes(-1, min(l, l1), max(h, h1)) + } + return nil +} diff --git a/ptn/common.go b/ptn/common.go new file mode 100644 index 0000000..e28395b --- /dev/null +++ b/ptn/common.go @@ -0,0 +1,28 @@ +package ptn + +import ( + ta "github.com/banbox/banta" +) + +func GetMains(e *ta.BarEnv, i int) (float64, float64, float64, float64) { + o, h, l, c := e.Open.Get(i), e.High.Get(i), e.Low.Get(i), e.Close.Get(i) + return o, h, l, c +} + +type FuncPtnRecognize = func(e *ta.BarEnv) *PtnRes + +var ( + recognizers = []FuncPtnRecognize{ + isEngulfing, + } +) + +func Recognize(e *ta.BarEnv) *PtnRes { + for _, reg := range recognizers { + res := reg(e) + if res != nil { + return res + } + } + return nil +} diff --git a/ptn/data.go b/ptn/data.go new file mode 100644 index 0000000..a7025f0 --- /dev/null +++ b/ptn/data.go @@ -0,0 +1,6 @@ +package ptn + +const ( + PtnUnknown = iota + PtnEngulfing // 吞没形态 +) diff --git a/ptn/hammer/common.go b/ptn/hammer/common.go new file mode 100644 index 0000000..5c4ef54 --- /dev/null +++ b/ptn/hammer/common.go @@ -0,0 +1,179 @@ +package hammer + +import ( + "fmt" + "github.com/banbox/banbot/com" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg" + "github.com/banbox/banstagy/ptn" + ta "github.com/banbox/banta" + "math" +) + +type OdInfo struct { + btmRate float64 // 锤子下影线占价格的比率 + closeRate float64 // 锤子收盘价占价格比率,与上一个相同说明是阴线 +} + +type MoreHammer struct { + ents [][2]float64 // 前面信号的特征:atrBase比率,回撤比率 + backRate float64 // 历史平均回撤比率 + lastEnt int // 上次出现信号时BarNum + entBar *banexg.Kline // 上次信号的Bar + minPrice float64 // 信号后最低价格 + minAt int // 信号后最低价格索引 + atrTimes float64 // 信号是atrBase的倍数 + odInfos map[int64]*OdInfo // 订单ID对应的信息 +} + +func StartWatch(e *ta.BarEnv, more *MoreHammer, bar *banexg.Kline, atrTimes float64) { + more.lastEnt = e.BarNum + more.entBar = bar + more.atrTimes = atrTimes +} + +func UpdateWatch(s *strat.StratJob, more *MoreHammer, c, l float64, watchMin, watchMax, refNum int) { + distNum := s.Env.BarNum - more.lastEnt + bar := more.entBar + if distNum > watchMax || distNum > watchMin && (c-bar.Close)/(max(bar.Open, bar.Close)-bar.Low) > 0.2 { + // 超过最大观察个数;或者盈利超过锤子的20%,退出观察 + CloseWatch(s, more, refNum) + return + } + // 处于有效观察期内,更新 + if more.minPrice == 0 { + more.minPrice = l + } else if l < more.minPrice { + more.minPrice = l + more.minAt = distNum + if more.minPrice < bar.Low { + CloseWatch(s, more, refNum) + } + } +} + +func CloseWatch(s *strat.StratJob, more *MoreHammer, refNum int) { + if more.lastEnt == 0 { + return + } + defer func() { + more.lastEnt = 0 + more.atrTimes = 0 + more.minPrice = 0 + more.minAt = 0 + more.entBar = nil + }() + bar := more.entBar + if more.minPrice == 0 || more.minPrice < bar.Low { + // 价格突破锤子下影线,不再看涨,不作为信号参考 + return + } + backUp := bar.Close - bar.Low + backRate := (bar.Close - more.minPrice) / backUp + backRate = max(0, backRate) + more.ents = append(more.ents, [2]float64{more.atrTimes, backRate}) + if len(more.ents) > refNum { + more.ents = more.ents[:refNum] + } + sumRate := float64(0) + for _, it := range more.ents { + sumRate += it[1] + } + more.backRate = sumRate / float64(len(more.ents)) + backUpRate := backUp / bar.Close + line := fmt.Sprintf("%v,%.1f,%.1f,%.1f,%v", s.Symbol.Symbol, backUpRate*100, + more.atrTimes, backRate*100, more.minAt) + s.Strat.WriteOutput(line, true) +} + +func makeOnBar(atrLen, refNum, watchMin, watchMax int, baseRate float64) func(s *strat.StratJob) { + return func(s *strat.StratJob) { + e := s.Env + ma5 := ta.SMA(e.Close, 5).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, atrLen) + baseAtrLen := int(math.Round(float64(atrLen) * 4.3)) + atrBase := ta.Lowest(ta.Highest(atr, atrLen), baseAtrLen).Get(0) + maSml := ta.SMA(e.Close, 50).Get(0) + maBig := ta.SMA(e.Close, 300).Get(0) + + more, _ := s.More.(*MoreHammer) + + o, h, l, c := ptn.GetMains(e, 0) + if math.IsNaN(atrBase) || c < maSml && maSml < maBig { + return + } + price := com.GetPrice(s.Symbol.Symbol, "") + _ = c == price + v := e.Volume.Get(0) + solid := math.Abs(o - c) + btm := min(o, c) - l + up := h - max(o, c) + if btm > max(2.0*solid, up*5.0, atrBase*baseRate) && c < ma5 { + // 检查是否处于信号后观察模式 + CloseWatch(s, more, refNum) + // 开始新观察 + bar := &banexg.Kline{Time: e.TimeStart, Open: o, High: h, Low: l, Close: c, Volume: v} + StartWatch(e, more, bar, btm/atrBase) + // 创建入场订单 + limit := c - (c-l)*more.backRate*0.55 + stopLoss := c - atr.Get(0)*3.0 + if stopLoss >= min(limit, c) { + stopLoss = min(limit, c) - atr.Get(0) + } + err := s.OpenOrder(&strat.EnterReq{ + Tag: "long", + Limit: limit, + StopLoss: stopLoss, + }) + if err != nil { + fmt.Printf("open fail %v", err) + } + } else if more.lastEnt > 0 { + // 更新观察数据 + UpdateWatch(s, more, c, l, watchMin, watchMax, refNum) + } + } +} + +func onOrderChange(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + if chgType != strat.OdChgEnter { + // 只监听入场事件 + return + } + more, _ := s.More.(*MoreHammer) + if more.entBar == nil { + return + } + bar := more.entBar + closePct := (bar.Close - bar.Low) / bar.Close + btmPct := (min(bar.Open, bar.Close) - bar.Low) / bar.Close + more.odInfos[od.ID] = &OdInfo{ + btmRate: btmPct, + closeRate: closePct, + } +} + +/* +生成回撤止损函数 +第一个alpha参数调整反弹值 +thres是二维数组,每一维2个,第一个是反弹阈值,第二个是返回的止损阈值;应按第一个降序排列 +*/ +func makeDrawDownExit(alpha float64, thres [][2]float64) func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + return func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + more, _ := s.More.(*MoreHammer) + info, ok := more.odInfos[od.ID] + if !ok { + return -1 + } + // 盈利比率占锤子反弹的比率 + // 建议超参数测试顺序:case upRate -> upRate 尾部系数 -> switch rate -> btmRate/closeRate + upRate := maxChg / info.btmRate * alpha + for _, item := range thres { + if upRate > item[0] { + return item[1] + } + } + return 0 + } +} diff --git a/ptn/hammer/v1.go b/ptn/hammer/v1.go new file mode 100644 index 0000000..52374a4 --- /dev/null +++ b/ptn/hammer/v1.go @@ -0,0 +1,45 @@ +package hammer + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +const v1AtrLen = 14 + +func MainV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 30, + MinTfScore: 0.75, + DrawDownExit: true, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.SMA(e.Close, 5).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, v1AtrLen) + baseAtrLen := int(math.Round(float64(v1AtrLen) * 4.3)) + atrBase := ta.Lowest(ta.Highest(atr, v1AtrLen), baseAtrLen).Get(0) + + if math.IsNaN(atrBase) { + return + } + o, h, l, c := e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + solid := math.Abs(o - c) + btm := min(o, c) - l + up := h - max(o, c) + if btm > max(2.0*solid, up*5.0, atrBase*3) && c < ma5 { + stopLoss := c - atr.Get(0)*3.0 + err := s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLoss: stopLoss, + }) + if err != nil { + fmt.Printf("open fail %v", err) + } + } + }, + } +} diff --git a/ptn/hammer/v2.go b/ptn/hammer/v2.go new file mode 100644 index 0000000..78bc371 --- /dev/null +++ b/ptn/hammer/v2.go @@ -0,0 +1,27 @@ +package hammer + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" +) + +const ( + v2AtrLen = 14 + v2WatchMin = 5 // 入场后观察后续蜡烛的最少个数 + v2WatchMax = 100 // 入场后观察后续蜡烛的最多个数 + v2RefNum = 20 // 回顾历史回撤的数量 +) + +func MainV2(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 3000, + MinTfScore: 0.75, + DrawDownExit: true, + StopEnterBars: 10, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreHammer{} + }, + OnBar: makeOnBar(v2AtrLen, v2RefNum, v2WatchMin, v2WatchMax, 3), + } +} diff --git a/ptn/hammer/v3.go b/ptn/hammer/v3.go new file mode 100644 index 0000000..1f8f288 --- /dev/null +++ b/ptn/hammer/v3.go @@ -0,0 +1,35 @@ +package hammer + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" +) + +const ( + v3AtrLen = 14 + v3WatchMin = 5 // 入场后观察后续蜡烛的最少个数 + v3WatchMax = 100 // 入场后观察后续蜡烛的最多个数 + v3RefNum = 20 // 回顾历史回撤的数量 +) + +func MainV3(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 3000, + MinTfScore: 0.75, + DrawDownExit: true, + StopEnterBars: 10, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreHammer{ + odInfos: map[int64]*OdInfo{}, + } + }, + OnBar: makeOnBar(v3AtrLen, v3RefNum, v3WatchMin, v3WatchMax, 3), + OnOrderChange: onOrderChange, + GetDrawDownExitRate: makeDrawDownExit(0.8, [][2]float64{ + {0.625, 0.25}, + {0.3, 0.37}, + {0.15, 0.5}, + }), + } +} diff --git a/ptn/hammer/v4.go b/ptn/hammer/v4.go new file mode 100644 index 0000000..675f55b --- /dev/null +++ b/ptn/hammer/v4.go @@ -0,0 +1,35 @@ +package hammer + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" +) + +const ( + v4AtrLen = 14 + v4WatchMin = 5 // 入场后观察后续蜡烛的最少个数 + v4WatchMax = 100 // 入场后观察后续蜡烛的最多个数 + v4RefNum = 20 // 回顾历史回撤的数量 +) + +func MainV4(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 3000, + MinTfScore: 0.75, + DrawDownExit: true, + StopEnterBars: 10, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreHammer{ + odInfos: map[int64]*OdInfo{}, + } + }, + OnBar: makeOnBar(v4AtrLen, v4RefNum, v4WatchMin, v4WatchMax, 2.76), + OnOrderChange: onOrderChange, + GetDrawDownExitRate: makeDrawDownExit(0.8, [][2]float64{ + {0.625, 0.25}, + {0.3, 0.37}, + {0.15, 0.5}, + }), + } +} diff --git a/ptn/hammer/v5.go b/ptn/hammer/v5.go new file mode 100644 index 0000000..fd53e09 --- /dev/null +++ b/ptn/hammer/v5.go @@ -0,0 +1,96 @@ +package hammer + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg" + ta "github.com/banbox/banta" + "math" +) + +/* +加入RSI低于55限制,同时调低atr倍率,允许更多机会。 +测试效果不如V4,弃用 +*/ + +const ( + v5AtrLen = 14 + v5WatchMin = 5 // 入场后观察后续蜡烛的最少个数 + v5WatchMax = 100 // 入场后观察后续蜡烛的最多个数 + v5RefNum = 20 // 回顾历史回撤的数量 +) + +func makeV5OnBar(atrLen, refNum, watchMin, watchMax int, baseRate float64) func(s *strat.StratJob) { + return func(s *strat.StratJob) { + e := s.Env + ma5 := ta.SMA(e.Close, 5).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, atrLen) + baseAtrLen := int(math.Round(float64(atrLen) * 4.3)) + atrBase := ta.Lowest(ta.Highest(atr, atrLen), baseAtrLen).Get(0) + + rsiCol := ta.RSI(e.Close, 20) + rsi, rsiPrv := rsiCol.Get(0), rsiCol.Get(1) + + more, _ := s.More.(*MoreHammer) + + if math.IsNaN(atrBase) { + return + } + rsiOk := rsiPrv >= 70 && rsiPrv-rsi > 30 || (rsiPrv+rsi)/2 < 55 + if !rsiOk { + return + } + o, h, l, c := e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0) + v := e.Volume.Get(0) + solid := math.Abs(o - c) + btm := min(o, c) - l + up := h - max(o, c) + if btm > max(2.0*solid, up*5.0, atrBase*baseRate) && c < ma5 { + // 检查是否处于信号后观察模式 + CloseWatch(s, more, refNum) + // 开始新观察 + bar := &banexg.Kline{Time: e.TimeStart, Open: o, High: h, Low: l, Close: c, Volume: v} + StartWatch(e, more, bar, btm/atrBase) + // 创建入场订单 + limit := c - (c-l)*more.backRate*0.55 + stopLoss := c - atr.Get(0)*3.0 + if stopLoss >= limit { + stopLoss = limit - atr.Get(0) + } + err := s.OpenOrder(&strat.EnterReq{ + Tag: "long", + Limit: limit, + StopLoss: stopLoss, + }) + if err != nil { + fmt.Printf("open fail %v", err) + } + } else if more.lastEnt > 0 { + // 更新观察数据 + UpdateWatch(s, more, c, l, watchMin, watchMax, refNum) + } + } +} + +func MainV5(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 3000, + MinTfScore: 0.75, + DrawDownExit: true, + StopEnterBars: 10, + OnStartUp: func(s *strat.StratJob) { + s.More = &MoreHammer{ + odInfos: map[int64]*OdInfo{}, + } + }, + OnBar: makeV5OnBar(v5AtrLen, v5RefNum, v5WatchMin, v5WatchMax, 2.2), + OnOrderChange: onOrderChange, + GetDrawDownExitRate: makeDrawDownExit(0.8, [][2]float64{ + {0.625, 0.25}, + {0.3, 0.37}, + {0.15, 0.5}, + }), + } +} diff --git a/ptn/tunmo.go b/ptn/tunmo.go new file mode 100644 index 0000000..ef91a8e --- /dev/null +++ b/ptn/tunmo.go @@ -0,0 +1,110 @@ +package ptn + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +type tunmoMore struct { + topVal float64 + btmVal float64 + barNum int +} + +/* +检查是否是吞没形态 +*/ +func isTunMo(e *ta.BarEnv) (int, float64, float64) { + o, h, l, c := GetMains(e, 0) + o1, h1, l1, c1 := GetMains(e, 1) + solid := math.Abs(o - c) + if solid/(h-l) < 0.6 { + // 第二个蜡烛实体至少60% + return 0, 0, 0 + } + solid1 := math.Abs(o1 - c1) + if solid1/(h1-l1) <= 0.13 && solid > solid1*3 { + // 第一个是十字星,直接符合 + if o > c { + return -1, min(l, l1), max(h, h1) + } else { + return 1, min(l, l1), max(h, h1) + } + } + if o < c { + // 后一个是阳线,要求前一个是阴线 + if o1 > c1 && o1 <= c && c1 >= o { + // 前开<收盘 & 前收 > 开盘 + return 1, min(l, l1), max(h, h1) + } + } else if o1 < c1 && c1 <= o && o1 >= c { + // 后一个阴线,要求前一个阳线 + // 前收<开盘 & 前开 > 收盘 + return -1, min(l, l1), max(h, h1) + } + return 0, 0, 0 +} + +func TunMo(pol *config.RunPolicyConfig) *strat.TradeStrat { + const v1AtrLen = 14 + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 30, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &tunmoMore{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + // ma5 := ta.SMA(e.Close, 5).Get(1) + atr := ta.ATR(e.High, e.Low, e.Close, v1AtrLen) + baseAtrLen := int(math.Round(float64(v1AtrLen) * 4.3)) + atrBase := ta.Lowest(ta.Highest(atr, v1AtrLen), baseAtrLen).Get(0) + + //rsiCol := ta.RSI(e.Close, 14) + //rsi := rsiCol.Range(0, 2) + + if math.IsNaN(atrBase) || s.OrderNum > 0 { + return + } + m, _ := s.More.(*tunmoMore) + mode, btmVal, topVal := isTunMo(e) + if mode != 0 && atr.Get(0) > atrBase*2 { + m.btmVal = btmVal - atrBase + m.topVal = topVal + atrBase + m.barNum = e.BarNum + } + if e.BarNum-m.barNum > 10 { + // 忽略吞没形态10个之后的蜡烛 + return + } + c := e.Close.Get(0) + // slices.Max(rsi) > 70 + // && c > ma5 + if c > m.topVal { + stopLoss := max(m.topVal, c) + atr.Get(0) + err := s.OpenOrder(&strat.EnterReq{ + Tag: "short", + StopLoss: stopLoss, + Short: true, + }) + if err != nil { + fmt.Printf("open fail %v", err) + } + } else if c < m.btmVal { + stopLoss := min(m.btmVal, c) - atr.Get(0) + err := s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLoss: stopLoss, + }) + if err != nil { + fmt.Printf("open fail %v", err) + } + } + }, + } +} diff --git a/ptn/types.go b/ptn/types.go new file mode 100644 index 0000000..bc1a20f --- /dev/null +++ b/ptn/types.go @@ -0,0 +1,8 @@ +package ptn + +type PtnRes struct { + Type int + Dirt int // 此形态看多1还是看空-1 + Confidence float64 + Data interface{} +} diff --git a/rpc_fea/aifea.go b/rpc_fea/aifea.go new file mode 100644 index 0000000..9ef7b0a --- /dev/null +++ b/rpc_fea/aifea.go @@ -0,0 +1,121 @@ +package rpc_fea + +import ( + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + utils2 "github.com/banbox/banexg/utils" + "github.com/banbox/banta" + "go.uber.org/zap" + "gonum.org/v1/gonum/mat" + "math" +) + +type AIFeaState struct { + cg *banta.CGraph + cgBig *banta.CGraph + matBig [3]*mat.Dense + feasBig *mat.Dense + rateBig float64 + avgCostBigs []float64 //大周期的成本价 +} + +func pubAiFea(exsList []*orm.ExSymbol, req *biz.SubReq, rsp biz.FeaFeeder_SubFeaturesServer) error { + var sample = int(req.Sample) + if sample <= 0 { + sample = 10 + } + startDt := btime.ToDateStr(req.Start, core.DefaultDateFmt) + log.Info("start ai fea2", zap.Int("pair", len(exsList)), zap.String("start", startDt), + zap.Int("sample", sample)) + var seqNum = 150 // 特征序列长度 + var penNum = 120 // 笔矩阵长度 + var depth = 100 // 笔矩阵深度:100个深度 + tfSml, tfBig := "5m", "1h" + tfMSecs := int64(utils2.TFToSecs(tfSml) * 1000) + tfMSecsBig := int64(utils2.TFToSecs(tfBig) * 1000) + + states := make(map[string]*AIFeaState) + for _, exs := range exsList { + states[exs.Symbol] = &AIFeaState{ + cg: &banta.CGraph{ParseLevel: banta.ParseLevelPen, MaxPen: penNum * 3 / 2}, + cgBig: &banta.CGraph{ParseLevel: banta.ParseLevelPen, MaxPen: penNum * 3 / 2}, + } + } + + task := &GenFeaTask{ + TfSml: tfSml, + TfBig: tfBig, + WarmSml: 3000, + WarmBig: 3000, + TfMSecs: tfMSecs, + TfMSecsBig: tfMSecsBig, + NextObsNum: 30, + } + + batch := NewBatchMap(len(exsList)) + + task.OnBar = func(e *banta.BarEnv, b *banexg.Kline, futs []*banexg.Kline) *errs.Error { + //if !core.IsWarmUp { + // log.Info("OnBar", zap.Int64("time", e.TimeStop)) + //} + state := states[e.Symbol] + state.cg.AddBar(e) + state.cg.Parse() + feas := onAiFeatures(e, seqNum) + if e.BarNum%sample > 0 || len(state.cg.Pens) < penNum || state.rateBig == 0 || len(futs) == 0 { + // 预热时futs为nil + return nil + } + // 收集特征,发送到监听端 + minChgVal := banta.ATR(e.High, e.Low, e.Close, 30).Get(0) * 5 + futProbs := genFutProbsV1(futs, minChgVal, 0.4) + mats, rate := GenFeaMats(state.cg, penNum, depth, false) + avgCosts := avgCostN(e, 10, 20, 40, 80, 160, 320) + info := make([]float64, 0, 25) + info = append(info, float64(b.Time/1000), b.Open, b.High, b.Low, b.Close, b.Volume, + math.Log1p(rate), math.Log1p(state.rateBig)) + info = append(info, avgCosts...) + info = append(info, state.avgCostBigs...) + info = append(info, futProbs[0], futProbs[1], futProbs[2]) + return batch.Add(rsp, e, b.Time, map[string]*biz.NumArr{ + "time": matToNumArr(mats[0]), + "buy": matToNumArr(mats[1]), + "sell": matToNumArr(mats[2]), + "feas": matToNumArr(feas), + "time2": matToNumArr(state.matBig[0]), + "buy2": matToNumArr(state.matBig[1]), + "sell2": matToNumArr(state.matBig[2]), + "feas2": matToNumArr(state.feasBig), + "info": {Data: info, Shape: []int32{int32(len(info))}}, + }) + } + task.OnInfoBar = func(e *banta.BarEnv, b *banexg.Kline) *errs.Error { + //if !core.IsWarmUp { + // log.Info("OnInfoBar", zap.Int64("time", e.TimeStop)) + //} + state := states[e.Symbol] + state.cgBig.AddBar(e) + state.cgBig.Parse() + state.feasBig = onAiFeatures(e, seqNum) + state.avgCostBigs = avgCostN(e, 10, 20, 40, 80, 160, 320) + if len(state.cgBig.Pens) >= penNum { + state.matBig, state.rateBig = GenFeaMats(state.cgBig, penNum, depth, false) + } + return nil + } + task.OnEnvEnd = func(bar *banexg.PairTFKline, adj *orm.AdjInfo) { + if bar == nil { + return + } + state := states[bar.Symbol] + state.cg = &banta.CGraph{ParseLevel: banta.ParseLevelPen, MaxPen: penNum * 3 / 2} + state.cgBig = &banta.CGraph{ParseLevel: banta.ParseLevelPen, MaxPen: penNum * 3 / 2} + state.rateBig = 0 + } + return pubFeaBase(exsList, req, task) +} diff --git a/rpc_fea/aifea2.go b/rpc_fea/aifea2.go new file mode 100644 index 0000000..efd9f59 --- /dev/null +++ b/rpc_fea/aifea2.go @@ -0,0 +1,107 @@ +package rpc_fea + +import ( + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + utils2 "github.com/banbox/banexg/utils" + "github.com/banbox/banta" + "go.uber.org/zap" + "gonum.org/v1/gonum/mat" +) + +type AiFea2State struct { + cg *banta.CGraph + feasBig *mat.Dense + avgCostBigs []float64 //大周期的成本价 +} + +func pubAiFea2(exsList []*orm.ExSymbol, req *biz.SubReq, rsp biz.FeaFeeder_SubFeaturesServer) error { + var sample = int(req.Sample) + if sample <= 0 { + sample = 10 + } + startDt := btime.ToDateStr(req.Start, core.DefaultDateFmt) + endDt := btime.ToDateStr(req.End, core.DefaultDateFmt) + log.Info("start ai fea2", zap.Int("pair", len(exsList)), zap.String("start", startDt), + zap.String("end", endDt), zap.Int("sample", sample)) + var seqNum = 150 // 特征序列长度 + var penNum = 80 // 笔矩阵长度 + var depth = 100 // 笔矩阵深度:100个深度 + tfSml, tfBig := "5m", "1h" + tfMSecs := int64(utils2.TFToSecs(tfSml) * 1000) + tfMSecsBig := int64(utils2.TFToSecs(tfBig) * 1000) + + task := &GenFeaTask{ + TfSml: tfSml, + TfBig: tfBig, + WarmSml: 2000, + WarmBig: 300, + TfMSecs: tfMSecs, + TfMSecsBig: tfMSecsBig, + NextObsNum: 30, + } + + states := make(map[string]*AiFea2State) + for _, exs := range exsList { + states[exs.Symbol] = &AiFea2State{ + cg: &banta.CGraph{ParseLevel: banta.ParseLevelPen, MaxPen: penNum * 3 / 2}, + } + } + + batch := NewBatchMap(len(exsList)) + + task.OnBar = func(e *banta.BarEnv, b *banexg.Kline, futs []*banexg.Kline) *errs.Error { + state := states[e.Symbol] + state.cg.AddBar(e) + state.cg.Parse() + feas := onAiFeatures2(e, 30) + if e.BarNum%sample > 0 || len(state.cg.Pens) < penNum || len(futs) == 0 || state.feasBig == nil { + // 预热时futs为nil + return nil + } + // 收集特征,发送到监听端 + minChgVal := banta.ATR(e.High, e.Low, e.Close, 30).Get(0) * 5 + futProbs := genFutProbsV1(futs, minChgVal, 0.4) + mats, _ := GenPenMats2(state.cg, penNum, depth, false) + avgCosts := avgCostN(e, 10, 20, 40, 80, 160, 320) + info := make([]float64, 0, 25) + info = append(info, avgCosts...) + info = append(info, state.avgCostBigs...) + return batch.Add(rsp, e, b.Time, map[string]*biz.NumArr{ + "one": matToNumArr(mats[0]), + "two": matToNumArr(mats[1]), + "feas": matToNumArr(feas), + "feas2": matToNumArr(state.feasBig), + "bar": { + Data: []float64{float64(b.Time / 1000), b.Open, b.High, b.Low, b.Close, b.Volume}, + Shape: []int32{6}, + }, + "y": { + Data: []float64{futProbs[0], futProbs[1], futProbs[2]}, + Shape: []int32{3}, + }, + "info": { + Data: info, Shape: []int32{int32(len(info))}, + }, + }) + } + task.OnInfoBar = func(e *banta.BarEnv, b *banexg.Kline) *errs.Error { + state := states[e.Symbol] + state.feasBig = onAiFeatures2(e, seqNum) + state.avgCostBigs = avgCostN(e, 10, 20, 40, 80, 160, 320) + return nil + } + task.OnEnvEnd = func(bar *banexg.PairTFKline, adj *orm.AdjInfo) { + if bar == nil { + return + } + state := states[bar.Symbol] + state.cg = &banta.CGraph{ParseLevel: banta.ParseLevelPen, MaxPen: penNum * 3 / 2} + } + return pubFeaBase(exsList, req, task) +} diff --git a/rpc_fea/aifea_test.go b/rpc_fea/aifea_test.go new file mode 100644 index 0000000..b839d09 --- /dev/null +++ b/rpc_fea/aifea_test.go @@ -0,0 +1,176 @@ +package rpc_fea + +import ( + "context" + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg/log" + "go.uber.org/zap" + "gonum.org/v1/gonum/mat" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "math" + "slices" + "strings" + "testing" +) + +func TestDataClient(t *testing.T) { + addr := "127.0.0.1:6789" + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error("connect grpc fail", zap.Error(err)) + return + } + defer conn.Close() + c := biz.NewFeaFeederClient(conn) + ctx := context.Background() + startMS := int64(1682899200000) + endMS := int64(1682985600000) + task := "aifea2" + codes := []string{"1000BONK/USDT:USDT", "1000FLOKI/USDT:USDT", "1000PEPE/USDT:USDT", "1000RATS/USDT:USDT", + "1000SATS/USDT:USDT", "ACE/USDT:USDT", "AGIX/USDT:USDT", "ALICE/USDT:USDT", "API3/USDT:USDT", + "ARKM/USDT:USDT", "ARK/USDT:USDT", "AR/USDT:USDT", "ATA/USDT:USDT", "ATOM/USDT:USDT", + "AVAX/USDT:USDT", "AXS/USDT:USDT", "BADGER/USDT:USDT", "BAKE/USDT:USDT", "BAT/USDT:USDT", + "BEAMX/USDT:USDT", "BICO/USDT:USDT", "BNB/USDT:USDT", "BNT/USDT:USDT", "BNX/USDT:USDT", + "BTC/USDT:USDT", "CKB/USDT:USDT", "COMBO/USDT:USDT", "COMP/USDT:USDT", "CTK/USDT:USDT", + "DEFI/USDT:USDT", "DODOX/USDT:USDT", "DOT/USDT:USDT", "DUSK/USDT:USDT", "ETHW/USDT:USDT", + "FET/USDT:USDT", "FLM/USDT:USDT", "FLOW/USDT:USDT", "FRONT/USDT:USDT", "GLMR/USDT:USDT", + "GRT/USDT:USDT", "GTC/USDT:USDT", "HFT/USDT:USDT", "HIFI/USDT:USDT", "HIGH/USDT:USDT", "ID/USDT:USDT", + "ILV/USDT:USDT", "JASMY/USDT:USDT", "JOE/USDT:USDT", "JUP/USDT:USDT", "KLAY/USDT:USDT", + "KNC/USDT:USDT", "KSM/USDT:USDT", "LDO/USDT:USDT", "LINA/USDT:USDT", "LIT/USDT:USDT", + "LOOM/USDT:USDT", "LPT/USDT:USDT", "LSK/USDT:USDT", "LTC/USDT:USDT", "LUNA2/USDT:USDT", + "MAGIC/USDT:USDT", "MANA/USDT:USDT", "NEO/USDT:USDT", "OGN/USDT:USDT", "ORDI/USDT:USDT", + "RVN/USDT:USDT", "SFP/USDT:USDT", "SSV/USDT:USDT", "STRAX/USDT:USDT", "STX/USDT:USDT", + "TRB/USDT:USDT", "XLM/USDT:USDT", "XRP/USDT:USDT", "ZIL/USDT:USDT"} + // codes = []string{"AGIX/USDT:USDT"} //, "API3/USDT:USDT", "ARKM/USDT:USDT"} + matKeys := map[string]bool{"time": true, "buy": true, "sell": true, "one": true, "two": true, + "feas": true, "feas2": true} + staMap := map[string][]*ColSta{} + logSta := func(key string, sta *ColSta) { + items, ok := staMap[key] + if !ok { + items = make([]*ColSta, 0, 30) + } + items = append(items, sta) + staMap[key] = items + } + res, err := c.SubFeatures(ctx, &biz.SubReq{ + Exchange: "binance", + Market: "linear", + Codes: codes, + Start: startMS, + End: endMS, + Task: task, + Sample: 1, + }) + fmt.Printf("\nlisten for %s\n", codes) + if err != nil { + log.Error("call SubFeatures fail", zap.Error(err)) + return + } + for { + fea, err := res.Recv() + if err != nil { + if strings.Contains(err.Error(), "EOF") { + log.Info("done", zap.Strings("code", codes)) + } else { + log.Error("receive feature fail", zap.Strings("code", codes), zap.Error(err)) + } + break + } + for key, item := range fea.Mats { + if _, ok := matKeys[key]; ok { + row, col := int(item.Shape[0]), int(item.Shape[1]) + arr := mat.NewDense(row, col, item.Data) + for j := 0; j < col; j++ { + column := make([]float64, row) + for i := 0; i < row; i++ { + column[i] = arr.At(i, j) + } + sta := sampleArr(column) + if sta.Nan > 0 || sta.Min < -1 || sta.Max > 1 { + fmt.Printf("\n%v col %v: %v", key, j, staToStr(sta)) + } + logSta(fmt.Sprintf("%s_%v", key, j), sta) + } + } else { + sta := sampleArr(item.Data) + if sta.Nan > 0 || sta.Min < -1 || sta.Max > 1 { + fmt.Printf("\n%v: %v", key, staToStr(sta)) + } + logSta(key, sta) + } + } + break + } + keys := utils.KeysOfMap(staMap) + slices.Sort(keys) + fmt.Printf("\n\n\n sta cross all codes:\n") + for _, key := range keys { + items, _ := staMap[key] + nan := 0 + sum, minVal, maxVal := float64(0), math.MaxFloat64, -math.MaxFloat64 + for _, it := range items { + sum += it.Mean + nan += it.Nan + minVal = min(minVal, it.Min) + maxVal = max(maxVal, it.Max) + } + sta := &ColSta{Nan: nan, Min: minVal, Max: maxVal, Mean: sum / float64(len(items))} + if sta.Nan > 0 || sta.Min < -1 || sta.Max > 1 { + fmt.Printf("\n%v: %v", key, staToStr(sta)) + } + } +} + +func TestAiFea2(t *testing.T) { + addr := "127.0.0.1:6789" + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error("connect grpc fail", zap.Error(err)) + return + } + defer conn.Close() + c := biz.NewFeaFeederClient(conn) + ctx := context.Background() + startMS := int64(1682899200000) + endMS := int64(1682985600000) + task := "aifea2" + code := "AGIX/USDT:USDT" + res, err := c.SubFeatures(ctx, &biz.SubReq{ + Exchange: "binance", + Market: "linear", + Codes: []string{code}, + Start: startMS, + End: endMS, + Task: task, + Sample: 1, + }) + fmt.Printf("\nlisten for %s\n", code) + if err != nil { + log.Error("call SubFeatures fail", zap.Error(err)) + return + } + for { + fea, err := res.Recv() + if err != nil { + if strings.Contains(err.Error(), "EOF") { + log.Info("done", zap.String("code", code)) + } else { + log.Error("receive feature fail", zap.String("code", code), zap.Error(err)) + } + break + } + a := staToStr(sampleArr(fea.Mats["one"].Data)) + b := staToStr(sampleArr(fea.Mats["two"].Data)) + cc := staToStr(sampleArr(fea.Mats["feas"].Data)) + d := staToStr(sampleArr(fea.Mats["feas2"].Data)) + bar := fea.Mats["bar"].Data + curDate := btime.ToDateStr(int64(bar[0]), core.DefaultDateFmt) + fmt.Printf("%s, %.5f, %s, %s, %s, %s\n", curDate, bar[4], a, b, cc, d) + } +} diff --git a/rpc_fea/common.go b/rpc_fea/common.go new file mode 100644 index 0000000..a9fa047 --- /dev/null +++ b/rpc_fea/common.go @@ -0,0 +1,745 @@ +package rpc_fea + +import ( + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/utils" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + ta "github.com/banbox/banta" + "gonum.org/v1/gonum/floats" + "gonum.org/v1/gonum/mat" + "math" + "slices" + "sync" +) + +func genFutProbsV1(arr []*banexg.Kline, minChgVal float64, maxRevRate float64) [3]float64 { + first, upper, lower, chgVal := getFutBounds(arr) + total := upper - lower + chgRate := chgVal / first + upRate := (upper - first) / total + dnRate := (first - lower) / total + revRate := min(upRate, dnRate) / max(upRate, dnRate) + var zeroRate float64 + if revRate > maxRevRate { + // 反向波动过大,视为不确定 + zeroRate = min(1.0, max(0.55, revRate+0.2)) + } else if chgRate < 0.005 || chgVal < minChgVal { + // 波动太小,视为不确定 + var rateScore, valScore float64 + if chgRate < 0.01 { + rateScore = 1 - chgRate/0.01 + } + if chgVal < minChgVal*2 { + valScore = 1 - chgVal/minChgVal/2 + } + zeroRate = max(0.55, rateScore*0.5+valScore*0.5) + } else { + // 有方向的行情,将反向波动50%作为不确定的概率 + zeroRate = revRate * 0.5 + } + dirtRate := 1 - zeroRate + upRate *= dirtRate + dnRate *= dirtRate + return [3]float64{zeroRate, upRate, dnRate} +} + +/* +genFutProbs +生成未来行情概率:[波动幅度,上升,下降,类别] +*/ +func genFutProbs(arr []*banexg.Kline, minChgVal float64) [4]float64 { + first, upper, lower, chgVal := getFutBounds(arr) + total := upper - lower + chgRate := chgVal / first + upRate := (upper - first) / total + dnRate := (first - lower) / total + var cate float64 + if min(upRate, dnRate)/max(upRate, dnRate) < 0.4 { + if upRate > dnRate { + cate = 1 + } else { + cate = 2 + } + } + if chgVal < minChgVal { + // 低于最低限度,置为高低相同 + upRate, dnRate = 0.5, 0.5 + cate = 0 + } else if chgVal < minChgVal*2 { + // 不够高,逐渐调整 + overRate := (chgVal - minChgVal) / minChgVal // 这是保持概率 + sameRate := (1 - overRate) / 2 // 这是拉平的概率 + upRate = upRate*overRate + sameRate + dnRate = dnRate*overRate + sameRate + if cate != 0 && min(upRate, dnRate)/max(upRate, dnRate) > 0.6 { + cate = 0 + } + } + // chgRate的30个5m变化率平均是1%,则乘以100 + return [4]float64{math.Log1p(chgRate*100) / 2, upRate, dnRate, cate} +} + +func getFutBounds(arr []*banexg.Kline) (float64, float64, float64, float64) { + var first = arr[0].Open + var lower, upper = first, first + var lowAt, uppAt = 0, 0 + for i, b := range arr { + if b.Low < lower { + lowAt = i + lower = b.Low + } + if b.High > upper { + uppAt = i + upper = b.High + } + } + var chgVal float64 + if upper-first > first-lower { + // 属于上升趋势 + arr = arr[:uppAt+1] + lower = first + for _, b := range arr { + lower = min(lower, b.Low) + } + chgVal = upper - first + } else { + // 下降趋势 + arr = arr[:lowAt+1] + upper = first + for _, b := range arr { + upper = max(upper, b.High) + } + chgVal = first - lower + } + return first, upper, lower, chgVal +} + +/* +* +计算给定周期内的成本价 +*/ +func avgCostN(e *ta.BarEnv, periods ...int) []float64 { + maxPeriod := slices.Max(periods) + opens := e.Open.Range(0, maxPeriod) + highs := e.High.Range(0, maxPeriod) + lows := e.Low.Range(0, maxPeriod) + closes := e.Close.Range(0, maxPeriod) + vols := e.Volume.Range(0, maxPeriod) + slices.Sort(periods) + price := closes[0] + // 累计流入资金,累计流入成交量 + sumCost, sumVol := float64(0), float64(0) + results := make([]float64, 0, len(periods)) + hitIdx := 0 + for i := 0; i < len(opens); i++ { + if i >= periods[hitIdx] { + results = append(results, sumCost/sumVol/price-1) + hitIdx += 1 + } + o, h, l, c, v := opens[i], highs[i], lows[i], closes[i], vols[i] + barUp, barDn := max(o, c), min(o, c) + total := h - l + (h - barUp) + (barDn - l) + if total <= 0 { + continue + } + // 净买入或净卖出的比例 + sideRate := (barUp - barDn) * 0.33 / total + dirt := -1.0 + if o < c { + dirt = 1.0 + } + curVol := sideRate * v * dirt + sumVol += curVol + sumCost += curVol * (barUp*0.5 + barDn*0.5) + } + if len(results) < len(periods) && sumVol != 0 { + results = append(results, sumCost/sumVol/price-1) + } + for len(results) < len(periods) { + results = append(results, 0) + } + for i, v := range results { + results[i] = cutRange(v, -1, 1, 0) + } + return results +} + +func matToNumArr(m *mat.Dense) *biz.NumArr { + data := m.RawMatrix().Data + row, col := m.Dims() + return &biz.NumArr{Data: data, Shape: []int32{int32(row), int32(col)}} +} + +func onAiFeatures(e *ta.BarEnv, num int) *mat.Dense { + volMa5 := ta.SMA(e.Volume, 5) + volMa20 := ta.SMA(e.Volume, 20) + volMa70 := ta.SMA(e.Volume, 70) + vwma5 := ta.VWMA(e.Close, e.Volume, 5) + vwma20 := ta.VWMA(e.Close, e.Volume, 20) + vwma70 := ta.VWMA(e.Close, e.Volume, 70) + rsi14 := ta.RSI(e.Close, 14) + rsi50 := ta.RSI(e.Close, 50) + atr14 := ta.ATR(e.High, e.Low, e.Close, 14) + atr50 := ta.ATR(e.High, e.Low, e.Close, 30) + sma5 := ta.SMA(e.Close, 5) + sma20 := ta.SMA(e.Close, 20) + sma70 := ta.SMA(e.Close, 70) + macd, macdS := ta.MACD(e.Close, 12, 26, 9) + macd2, macdS2 := ta.MACD(e.Close, 40, 90, 30) + kdjK, kdjD, _ := ta.KDJ(e.High, e.Low, e.Close, 9, 3, 3) + kdjK2, kdjD2, _ := ta.KDJ(e.High, e.Low, e.Close, 48, 16, 12) + stdDev := ta.StdDev(e.Close, 10) + adx := ta.ADXBy(e.High, e.Low, e.Close, 14, 0, 1) + roc := ta.ROC(e.Close, 30) + if num == 0 { + return nil + } + // 这里的特征列数最好是4或10的倍数,否则模型端采样会有问题 + feaNum := 30 + res := mat.NewDense(num, feaNum, nil) + for i := 0; i < num; i++ { + closeVal := e.Close.Get(i) + volVal := e.Volume.Get(i) + volMa5Val := volMa5.Get(i) + volMa20Val := volMa20.Get(i) + sma5Val := sma5.Get(i) + sma20Val := sma20.Get(i) + vwma5Val := vwma5.Get(i) + vwma20Val := vwma20.Get(i) + row := []float64{ + e.High.Get(i), + e.Low.Get(i), + closeVal, + volVal, + math.Log1p(volVal/volMa5Val) / 3, + math.Log1p(volMa5Val/volMa20Val) / 3, // 5 + math.Log1p(volMa20Val/volMa70.Get(i)) / 3, + (closeVal - e.Close.Get(i+1)) / closeVal, + (closeVal - e.Close.Get(i+7)) / closeVal, // 8 此行超出(-1,1),调用方需截断 + rsi14.Get(i) / 100, + rsi50.Get(i) / 100, // 10 + atr14.Get(i) / closeVal, + atr50.Get(i) / closeVal, + sma5Val/closeVal - 1, + sma20Val/sma5Val - 1, + sma70.Get(i)/sma20Val - 1, // 15 + vwma5Val/closeVal - 1, // 16 此行超出(-1,1),调用方需截断 + vwma20Val/vwma5Val - 1, // 17 此行超出(-1,1),调用方需截断 + vwma70.Get(i)/vwma20Val - 1, + macd.Get(i) / closeVal, + (macd.Get(i) - macdS.Get(i)) / closeVal, // 20 + macd2.Get(i) / closeVal, + (macd2.Get(i) - macdS2.Get(i)) / closeVal, + kdjK.Get(i) / 100, + (kdjK.Get(i) - kdjD.Get(i)) / 100, + kdjK2.Get(i) / 100, // 25 + (kdjK2.Get(i) - kdjD2.Get(i)) / 100, + stdDev.Get(i) / closeVal, + adx.Get(i) / 100, + roc.Get(i) / 100, // 29 此行超出(-1,1),调用方需截断 + } + for j, v := range row { + if math.IsNaN(v) { + v = 0 + } + res.Set(i, j, v) + } + } + // 对前四列hlcv进行归一化 + for i := 0; i < 4; i++ { + col := res.ColView(i) + minVal := mat.Min(col) + maxVal := mat.Max(col) + if minVal == maxVal { + continue + } + for row := 0; row < num; row++ { + oldValue := res.At(row, i) + newValue := (oldValue - minVal) / (maxVal - minVal) + res.Set(row, i, newValue) + } + } + return res +} + +func onAiFeatures2(e *ta.BarEnv, num int) *mat.Dense { + if num <= 10 { + panic("onAiFeatures2.num must greate than 10") + } + volMa5 := ta.SMA(e.Volume, 5) + volMa20 := ta.SMA(e.Volume, 20) + volMa70 := ta.SMA(e.Volume, 70) + vwma5 := ta.VWMA(e.Close, e.Volume, 5) + vwma20 := ta.VWMA(e.Close, e.Volume, 20) + vwma70 := ta.VWMA(e.Close, e.Volume, 70) + rsi14 := ta.RSI(e.Close, 14) + rsiMa := ta.SMA(rsi14, 5) + rsi50 := ta.RSI(e.Close, 50) + atr14 := ta.ATR(e.High, e.Low, e.Close, 14) + atr50 := ta.ATR(e.High, e.Low, e.Close, 30) + sma5 := ta.SMA(e.Close, 5) + sma20 := ta.SMA(e.Close, 20) + sma70 := ta.SMA(e.Close, 70) + macd, macdS := ta.MACD(e.Close, 12, 26, 9) + macd2, macdS2 := ta.MACD(e.Close, 40, 90, 30) + kdjK, kdjD, _ := ta.KDJ(e.High, e.Low, e.Close, 9, 3, 3) + kdjK2, kdjD2, _ := ta.KDJ(e.High, e.Low, e.Close, 48, 16, 12) + stdDev := ta.StdDev(e.Close, 10) + bbUp, _, bbDn := ta.BBANDS(e.Close, 15, 1.6, 1.6) + bb2Up, _, bb2Dn := ta.BBANDS(e.Close, 30, 2.5, 2.5) + aroonUp, aroonOsc, aroonDn := ta.Aroon(e.High, e.Low, 14) + aroon2Up, aroon2Osc, aroon2Dn := ta.Aroon(e.High, e.Low, 30) + adx := ta.ADX(e.High, e.Low, e.Close, 14) + adxPlus, _ := ta.PluMinDI(e.High, e.Low, e.Close, 14) + adx2 := ta.ADX(e.High, e.Low, e.Close, 50) + adxPlus2, _ := ta.PluMinDI(e.High, e.Low, e.Close, 50) + roc := ta.ROC(e.Close, 9) + roc2 := ta.ROC(e.Close, 30) + tnr := ta.ER(e.Close, 15) + tnr2 := ta.ER(e.Close, 50) + cci := ta.CCI(e.Close, 15) + cci50 := ta.CCI(e.Close, 50) + cci100 := ta.CCI(e.Close, 100) + cmf := ta.CMF(e, 15) + cmf50 := ta.CMF(e, 50) + kama := ta.KAMA(e.Close, 15) + kama30 := ta.KAMA(e.Close, 30) + willr := ta.WillR(e, 15) + willr30 := ta.WillR(e, 30) + willr30_5 := ta.SMA(willr30, 5) + willr50 := ta.WillR(e, 50) + srsiK, srsiD := ta.StochRSI(e.Close, 14, 14, 3, 3) + srsi2K, srsi2D := ta.StochRSI(e.Close, 30, 30, 5, 5) + mfi := ta.MFI(e, 14) + mfi30 := ta.MFI(e, 30) + rmi := ta.RMI(e.Close, 14, 3) + rmi30 := ta.RMI(e.Close, 30, 5) + hh14 := ta.Highest(e.Close, 14) + hh50 := ta.Highest(e.Close, 50) + ll14 := ta.Lowest(e.Close, 14) + ll50 := ta.Lowest(e.Close, 50) + td := ta.TD(e.Close) + // 这里的特征列数最好是4或10的倍数,否则模型端采样会有问题 + feaNum := 72 + arrData := make([]float64, 0, num*feaNum) + offset := 0 + for i := 0; i < num; i++ { + closeVal := e.Close.Get(i) + volMa5Val := volMa5.Get(i) + volMa20Val := volMa20.Get(i) + sma20Val := sma20.Get(i) + vwma5Val := vwma5.Get(i) + vwma20Val := vwma20.Get(i) + ll14Val := ll14.Get(i) + hh14Val := hh14.Get(i) + kdjKVal := kdjK.Get(i) + kdjK2Val := kdjK2.Get(i) + arrData = append(arrData, + e.High.Get(i), + e.Low.Get(i), + closeVal, + e.Volume.Get(i), + math.Log1p(e.Volume.Get(i)/volMa5Val)/3, + math.Log1p(volMa5Val/volMa20Val)/3, // 5 + math.Log1p(volMa20Val/volMa70.Get(i))/3, + (closeVal-e.Close.Get(i+1))/closeVal, + (closeVal-e.Close.Get(i+7))/closeVal, // 8 此行超出(-1,1),调用方需截断 + rsi14.Get(i)/100, + rsi50.Get(i)/100, // 10 + (rsiMa.Get(i)-rsi14.Get(i))/100, + atr14.Get(i)/closeVal, + atr50.Get(i)/closeVal, + sma5.Get(i)/closeVal-1, + sma20Val/sma5.Get(i)-1, // 15 + sma70.Get(i)/sma20Val-1, + vwma5Val/closeVal-1, // 17 此行超出(-1,1),调用方需截断 + vwma20Val/vwma5Val-1, // 18 此行超出(-1,1),调用方需截断 + vwma70.Get(i)/vwma20Val-1, + macd.Get(i)/closeVal, // 20 + (macd.Get(i)-macdS.Get(i))/closeVal, + macd2.Get(i)/closeVal, + (macd2.Get(i)-macdS2.Get(i))/closeVal, + kdjKVal/100, + (kdjKVal-kdjD.Get(i))/100, // 25 + kdjK2Val/100, + (kdjK2Val-kdjD2.Get(i))/100, + stdDev.Get(i)/closeVal, + adx.Get(i)/100, + adxPlus.Get(i)/100, // 30 + adx2.Get(i)/100, + adxPlus2.Get(i)/100, + roc.Get(i)/100, // 33 此行超出(-1,1),调用方需截断 + roc2.Get(i)/100, // 34 此行超出(-1,1),调用方需截断 + (closeVal-bbUp.Get(i))/closeVal, + (closeVal-bbDn.Get(i))/closeVal, + (closeVal-bb2Up.Get(i))/closeVal, + (closeVal-bb2Dn.Get(i))/closeVal, + aroonUp.Get(i)/100, + aroonOsc.Get(i)/100, + aroonDn.Get(i)/100, + aroon2Up.Get(i)/100, + aroon2Osc.Get(i)/100, + aroon2Dn.Get(i)/100, + tnr.Get(i), + tnr2.Get(i), + cci.Get(i)/400, // 47 超出1,需截断 + cci50.Get(i)/400, // 48 超出1,需截断 + cci100.Get(i)/400, // 49 超出1,需截断 + cmf.Get(i), + cmf50.Get(i), + kama.Get(i)/closeVal-1, + kama30.Get(i)/closeVal-1, + willr.Get(i)/100, + willr30.Get(i)/100, + (willr30.Get(i)-willr30_5.Get(i))/100, + willr50.Get(i)/100, + srsiK.Get(i)/100, // 58 超出1,需截断 + (srsiK.Get(i)-srsiD.Get(i))/100, + srsi2K.Get(i)/100, + (srsi2K.Get(i)-srsi2D.Get(i))/100, + mfi.Get(i)/100, + mfi30.Get(i)/100, + rmi.Get(i)/100, + rmi30.Get(i)/100, + closeVal/hh14Val, + hh14Val/hh50.Get(i), + ll14Val/closeVal, + ll50.Get(i)/ll14Val, + (hh14Val-ll14Val)/closeVal, + td.Get(i)/30, + ) + end := len(arrData) + for j := offset + 4; j < end; j++ { + arrData[j] = cutRange(arrData[j], -1, 1, 0) + } + offset = end + } + res := mat.NewDense(num, feaNum, arrData) + // 对前四列hlcv进行归一化 + for i := 0; i < 4; i++ { + col := res.ColView(i) + minVal := mat.Min(col) + maxVal := mat.Max(col) + if minVal == maxVal { + continue + } + for row := 0; row < num; row++ { + oldValue := res.At(row, i) + newValue := (oldValue - minVal) / (maxVal - minVal) + res.Set(row, i, newValue) + } + } + return res +} + +/* +GenFeaMats 生成时间、买、卖的热图 +*/ +func GenFeaMats(graph *ta.CGraph, num, depth int, restGap bool) ([3]*mat.Dense, float64) { + times := mat.NewDense(num, depth, nil) + buy := mat.NewDense(num, depth, nil) + sell := mat.NewDense(num, depth, nil) + pens := make([]*ta.CPen, 0, num) + bar := graph.Bar(graph.ParseTo) + price := bar.Close + pen := graph.Pens[len(graph.Pens)-1] + pt := pen.End + if graph.ParseTo > pt.BarId { + if pen.Dirt*(price-pt.Price) > 0 { + // 未合并数据和最后一笔方向相同 + pen = pen.Clone() + pen.End = pt.Clone().Move(graph.ParseTo, price) + pens = append(pens, pen) + } else { + end := graph.NewPoint(-pt.Dirt, price, graph.ParseTo) + newPen := graph.NewPen(pt, end) + pens = append(pens, newPen, pen) + } + } else { + pens = append(pens, pen) + } + for len(pens) < num && pen.Prev != nil { + pens = append(pens, pen.Prev) + pen = pen.Prev + } + minIdx := len(graph.Bars) - (graph.BarNum - pens[len(pens)-1].Start.BarId) - 1 + maxEnd := len(graph.Bars) - (graph.BarNum - graph.ParseTo) + bars := graph.Bars[minIdx:maxEnd] + upper, lower, sumVol := price, price, float64(0) + for _, b := range bars { + lower = min(lower, b.Low) + upper = max(upper, b.High) + sumVol += b.Volume + } + barNum := len(bars) + avgVol := sumVol / float64(barNum) + sub := max(upper-price, price-lower) + width := sub * 2 / float64(depth) // 每一格的价格宽度 + lower = price - sub + tfMSecs := min(bars[1].Time-bars[0].Time, bars[barNum-1].Time-bars[barNum-2].Time) + barId, col := graph.ParseTo, num + prevMS := int64(0) + for _, p := range pens { + penId := p.Start.BarId + if barId > penId { + col -= 1 + if col < 0 { + break + } + } + for barId > penId { + bar = graph.Bar(barId) + o, h, l, c := bar.Open-lower, bar.High-lower, bar.Low-lower, bar.Close-lower + if restGap && prevMS > 0 && prevMS-bar.Time > tfMSecs*3/2 { + // 休市期间添加空白时间记录 + lackNum := math.Round(float64(prevMS-bar.Time) / float64(tfMSecs)) + for i := 0; i < depth; i++ { + times.Set(col, i, lackNum) + } + col -= 1 + if col < 0 { + break + } + } + barId -= 1 + prevMS = bar.Time + barUp, barDn := max(o, c), min(o, c) + total := h - l + (h - barUp) + (barDn - l) + if total > 0 { + upRate, dnRate := (h-barUp)*2/total, (barDn-l)*2/total + solidRate := (barUp - barDn) / total + addToArr(times, col, barUp, h, upRate*0.5, width) + addToArr(times, col, barDn, barUp, solidRate*0.5, width) + addToArr(times, col, l, barDn, dnRate*0.5, width) + volRate := math.Log(bar.Volume/avgVol + 1) + buyRate, sellRate := 0.7, 0.3 + if o > c { + buyRate, sellRate = 0.3, 0.7 + } + addToArr(buy, col, barUp, h, volRate*upRate*0.5, width) + addToArr(buy, col, barDn, barUp, volRate*solidRate*buyRate, width) + addToArr(buy, col, l, barDn, volRate*dnRate*0.5, width) + addToArr(sell, col, barUp, h, volRate*upRate*0.5, width) + addToArr(sell, col, barDn, barUp, volRate*solidRate*sellRate, width) + addToArr(sell, col, l, barDn, volRate*dnRate*0.5, width) + } + } + } + return [3]*mat.Dense{times, buy, sell}, sub / price +} + +/* +GenPenMats2 +经测试,80个pen,平均需要1k根K线,最少700根,最多3k根 +*/ +func GenPenMats2(graph *ta.CGraph, num, depth int, restGap bool) ([2]*mat.Dense, float64) { + mats, rate := GenFeaMats(graph, num, depth, restGap) + times, buy, sell := mats[0], mats[1], mats[2] + one := mat.NewDense(num, depth, nil) + two := mat.NewDense(num, depth, nil) + times.Apply(func(i, j int, v float64) float64 { + return revPow(v, 2, 0, 1, 0) + }, times) + one.Apply(func(i, j int, v float64) float64 { + mid := (buy.At(i, j) + sell.At(i, j)) / 2 * (1 - v) + return revPow(mid, 1.5, 0, 1, 0) + }, times) + two.Apply(func(i, j int, v float64) float64 { + return revPow(v-sell.At(i, j), 1.5, -1, 1, 0) + }, buy) + return [2]*mat.Dense{one, two}, rate +} + +func revPow(v, cutRate, minVal, maxVal, nanTo float64) float64 { + if math.IsNaN(v) || math.IsInf(nanTo, 0) { + if nanTo < 0 { + return v + } + return nanTo + } + powered := math.Pow(math.Abs(v), 0.33) / cutRate + if v >= 0 { + return min(maxVal, powered) + } + return max(minVal, -powered) +} + +func cutRange(v, minVal, maxVal, nanTo float64) float64 { + if math.IsNaN(v) || math.IsInf(nanTo, 0) { + if nanTo < 0 { + return v + } + return nanTo + } + if v >= 0 { + return min(maxVal, v) + } + return max(minVal, -v) +} + +/* +addToArr +将总值按low~high切分为格子加到arr上 +*/ +func addToArr(m *mat.Dense, col int, low, high, total, width float64) { + if high <= low || low < 0 { + return + } + low /= width + high /= width + all := high - low + idx, down := int(low), low + _, dimCol := m.Dims() + if high > float64(dimCol) { + high = float64(dimCol) + } + for float64(idx) < high { + upp := min(high, float64(idx+1)) + m.Set(col, idx, (upp-down)/all*total) + down = float64(idx) + idx += 1 + } +} + +func getMatColSum(m *mat.Dense, col int) float64 { + if col < 0 { + _, colNum := m.Dims() + col += colNum + } + arr := m.ColView(col) + num := arr.Len() + // res := make([]float64, 0, num) + res := float64(0) + for i := 0; i < num; i++ { + // res = append(res, arr.AtVec(i)) + res += arr.AtVec(i) + } + return res +} + +func getMatRowSum(m *mat.Dense, row int) float64 { + if row < 0 { + rowNum, _ := m.Dims() + row += rowNum + } + res := float64(0) + arr := m.RawRowView(row) + for _, v := range arr { + res += v + } + return res +} + +type ColSta struct { + Nan int // nan数量 + Min float64 + Max float64 + Mean float64 +} + +func staToStr(sta *ColSta) string { + return fmt.Sprintf("nan:%v, mean: %.5f, [%.5f, %.5f]", sta.Nan, sta.Mean, sta.Min, sta.Max) +} + +func sampleArr(arr []float64) *ColSta { + nanNum := 0 + sum := float64(0) + valid := 0 + minVal, maxVal := math.MaxFloat64, -math.MaxFloat64 + for _, v := range arr { + if math.IsNaN(v) || math.IsInf(v, 0) { + nanNum += 1 + continue + } + sum += v + valid += 1 + if v < minVal { + minVal = v + } + if v > maxVal { + maxVal = v + } + } + if valid > 0 { + sum /= float64(valid) + } + return &ColSta{Nan: nanNum, Min: minVal, Max: maxVal, Mean: sum} +} + +type BatchMap struct { + codes []string + envs []*ta.BarEnv + mux sync.Mutex + curBarMS int64 // 当前bar的13位毫秒时间戳 + data map[string]*biz.NumArr + maxSize int +} + +func NewBatchMap(size int) *BatchMap { + return &BatchMap{ + data: make(map[string]*biz.NumArr), + maxSize: size, + } +} + +func (b *BatchMap) Add(rsp biz.FeaFeeder_SubFeaturesServer, e *ta.BarEnv, barMs int64, items map[string]*biz.NumArr) *errs.Error { + b.mux.Lock() + defer b.mux.Unlock() + if barMs > b.curBarMS { + oldLen := len(b.codes) + if oldLen > 0 { + b.maxSize = max(b.maxSize, oldLen) + corrs, err_ := getEnvsCorr(b.envs, 100) + if err_ != nil { + return errs.New(errs.CodeRunTime, err_) + } + b.data["corr"] = &biz.NumArr{ + Data: corrs, + Shape: []int32{int32(oldLen), 2}, + } + err_ = rsp.Send(&biz.ArrMap{ + Codes: b.codes, + Mats: b.data, + }) + if err_ != nil { + return errs.New(errs.CodeRunTime, err_) + } + b.codes = make([]string, 0, b.maxSize) + b.data = make(map[string]*biz.NumArr) + b.envs = make([]*ta.BarEnv, 0, b.maxSize) + } + b.curBarMS = barMs + } + b.codes = append(b.codes, e.Symbol) + b.envs = append(b.envs, e) + for name, arr := range items { + old, ok := b.data[name] + if !ok { + arr.Shape = append([]int32{1}, arr.Shape...) + b.data[name] = arr + } else { + old.Data = append(old.Data, arr.Data...) + old.Shape = append([]int32{old.Shape[0] + 1}, old.Shape[1:]...) + } + } + return nil +} + +func getEnvsCorr(envs []*ta.BarEnv, hisNum int) ([]float64, error) { + if len(envs) < 2 { + res := make([]float64, 0, len(envs)*2) + for range envs { + res = append(res, 0, 0) + } + return res, nil + } + _, avgs, err := utils.CalcEnvsCorr(envs, hisNum) + res := make([]float64, 0, len(avgs)*2) + avg := floats.Sum(avgs) / float64(len(avgs)) + for _, v := range avgs { + res = append(res, avg, v) + } + return res, err +} diff --git a/rpc_fea/fea_base.go b/rpc_fea/fea_base.go new file mode 100644 index 0000000..4f85fff --- /dev/null +++ b/rpc_fea/fea_base.go @@ -0,0 +1,124 @@ +package rpc_fea + +import ( + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/orm" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banta" +) + +type GenFeaTask struct { + TfSml string + TfBig string + WarmSml int + WarmBig int + TfMSecs int64 + TfMSecsBig int64 + NextObsNum int + OnBar func(e *banta.BarEnv, b *banexg.Kline, futs []*banexg.Kline) *errs.Error + OnInfoBar func(e *banta.BarEnv, b *banexg.Kline) *errs.Error + OnEnvEnd func(bar *banexg.PairTFKline, adj *orm.AdjInfo) +} + +type CodeState struct { + env *banta.BarEnv + envBig *banta.BarEnv +} + +func pubFeaBase(exsList []*orm.ExSymbol, req *biz.SubReq, task *GenFeaTask) error { + var states = make(map[string]*CodeState) + for _, exs := range exsList { + states[exs.Symbol] = &CodeState{ + env: &banta.BarEnv{ + TimeFrame: task.TfSml, + TFMSecs: task.TfMSecs, + Exchange: exs.Exchange, + MarketType: exs.Market, + Symbol: exs.Symbol, + MaxCache: 1000, + }, + envBig: &banta.BarEnv{ + TimeFrame: task.TfBig, + TFMSecs: task.TfMSecsBig, + Exchange: exs.Exchange, + MarketType: exs.Market, + Symbol: exs.Symbol, + MaxCache: 500, + }, + } + } + var outErr *errs.Error + verCh := make(chan int, 5) + stopWithErr := func(err error) { + outErr = errs.New(errs.CodeRunTime, err) + select { + case verCh <- -1: + default: + } + } + onData := func(evt *orm.DataSeries, nexts []*orm.DataSeries) { + bar, err_ := orm.AsKline(evt) + if err_ != nil { + stopWithErr(err_) + return + } + state := states[bar.Symbol] + var err *errs.Error + if bar.TimeFrame == task.TfSml { + futs := make([]*banexg.Kline, 0, len(nexts)) + for _, n := range nexts { + nextBar, convErr := orm.AsKline(n) + if convErr != nil { + stopWithErr(convErr) + return + } + futs = append(futs, &nextBar.Kline) + } + state.env.OnBar(bar.Time, bar.Open, bar.High, bar.Low, bar.Close, bar.Volume, + bar.Quote, bar.BuyVolume, bar.TradeNum) + err = task.OnBar(state.env, &bar.Kline, futs) + } else { + state.envBig.OnBar(bar.Time, bar.Open, bar.High, bar.Low, bar.Close, bar.Volume, + bar.Quote, bar.BuyVolume, bar.TradeNum) + err = task.OnInfoBar(state.envBig, &bar.Kline) + } + if err != nil { + outErr = err + verCh <- -1 + } + } + err := biz.RunHistSeries(&biz.RunHistSeriesArgs{ + ExsList: exsList, + Start: req.Start, + End: req.End, + ViewNextNum: task.NextObsNum, + TfWarms: map[string]int{ + task.TfSml: task.WarmSml, + task.TfBig: task.WarmBig, + }, + OnEnvEnd: func(evt *orm.DataSeries) { + if evt == nil { + task.OnEnvEnd(nil, nil) + return + } + bar, err_ := orm.AsKline(evt) + if err_ != nil { + stopWithErr(err_) + return + } + if bar.PairTFKline != nil { + state := states[bar.Symbol] + state.env.Reset() + state.envBig.Reset() + } + task.OnEnvEnd(bar.PairTFKline, bar.Adj) + }, + VerCh: verCh, + OnData: onData, + }) + if err != nil { + return err + } + return outErr +} diff --git a/rpc_fea/fea_times.go b/rpc_fea/fea_times.go new file mode 100644 index 0000000..798644d --- /dev/null +++ b/rpc_fea/fea_times.go @@ -0,0 +1,107 @@ +package rpc_fea + +import ( + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm" + "github.com/banbox/banexg" + "github.com/banbox/banexg/errs" + "github.com/banbox/banexg/log" + utils2 "github.com/banbox/banexg/utils" + "github.com/banbox/banta" + "go.uber.org/zap" + "gonum.org/v1/gonum/mat" +) + +func pubFeaTimes(exsList []*orm.ExSymbol, req *biz.SubReq, rsp biz.FeaFeeder_SubFeaturesServer) error { + var sample = int(req.Sample) + if sample <= 0 { + sample = 10 + } + startDt := btime.ToDateStr(req.Start, core.DefaultDateFmt) + log.Info("start fea_times", zap.Int("pair", len(exsList)), zap.String("start", startDt), + zap.Int("sample", sample)) + tfSml, tfBig := "5m", "1h" + tfMSecs := int64(utils2.TFToSecs(tfSml) * 1000) + tfMSecsBig := int64(utils2.TFToSecs(tfBig) * 1000) + seqNum := 150 // 特征的历史长度 + + var feasBig *mat.Dense + var avgCostBigs []float64 //大周期的成本价 + + task := &GenFeaTask{ + TfSml: tfSml, + TfBig: tfBig, + WarmSml: 500, + WarmBig: 500, + TfMSecs: tfMSecs, + TfMSecsBig: tfMSecsBig, + NextObsNum: 30, // 预测后n个bar的行情 + } + + batch := NewBatchMap(len(exsList)) + //taskId := orm.GetTaskID("") + //outDir := fmt.Sprintf("%s/backtest/task_%d", config.GetDataDir(), taskId) + //var outputs []string + + task.OnBar = func(e *banta.BarEnv, b *banexg.Kline, futs []*banexg.Kline) *errs.Error { + feas := onAiFeatures(e, seqNum) + if e.BarNum%sample > 0 || feasBig == nil { + return nil + } + atr30 := banta.ATR(e.High, e.Low, e.Close, 30).Get(0) + minChgVal := atr30 * 5 + futProbs := genFutProbsV1(futs, minChgVal, 0.4) + + //if !core.IsWarmUp { + // curDate := btime.ToDateStr(e.TimeStart, core.DefaultDateFmt) + // curCloseVal := e.Close.Get(0) + // n30Close := futs[len(futs)-1].Close + // row := fmt.Sprintf("%s, %.5f, %.5f, %.4f, %.4f, %.4f, %.7f", curDate, curCloseVal, n30Close, + // futProbs[0], futProbs[1], futProbs[2], atr30) + // outputs = append(outputs, row) + //} + + avgCosts := avgCostN(e, 10, 20, 40, 80, 160, 320) + info := make([]float64, 0, 12) + info = append(info, avgCosts...) + info = append(info, avgCostBigs...) + return batch.Add(rsp, e, b.Time, map[string]*biz.NumArr{ + "feas": matToNumArr(feas), + "feas2": matToNumArr(feasBig), + "bar": { + Data: []float64{float64(b.Time / 1000), b.Open, b.High, b.Low, b.Close, b.Volume}, + Shape: []int32{6}, + }, + "y": { + Data: []float64{futProbs[0], futProbs[1], futProbs[2]}, + Shape: []int32{3}, + }, + "info": {Data: info, Shape: []int32{int32(len(info))}}, + }) + } + task.OnInfoBar = func(e *banta.BarEnv, b *banexg.Kline) *errs.Error { + feasBig = onAiFeatures(e, seqNum) + avgCostBigs = avgCostN(e, 10, 20, 40, 80, 160, 320) + return nil + } + task.OnEnvEnd = func(bar *banexg.PairTFKline, adj *orm.AdjInfo) { + //name := "fea_times" + //outPath := fmt.Sprintf("%s/%s.log", outDir, name) + //file, err := os.Create(outPath) + //if err != nil { + // log.Error("create strategy output file fail", zap.String("name", name), zap.Error(err)) + // return + //} + //_, err = file.WriteString(strings.Join(outputs, "\n")) + //if err != nil { + // log.Error("write strategy output fail", zap.String("name", name), zap.Error(err)) + //} + //err = file.Close() + //if err != nil { + // log.Error("close strategy output fail", zap.String("name", name), zap.Error(err)) + //} + } + return pubFeaBase(exsList, req, task) +} diff --git a/rpc_fea/fea_trade1.go b/rpc_fea/fea_trade1.go new file mode 100644 index 0000000..414b516 --- /dev/null +++ b/rpc_fea/fea_trade1.go @@ -0,0 +1,383 @@ +package rpc_fea + +import ( + "context" + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "gonum.org/v1/gonum/mat" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "math" +) + +type AIMore struct { + cg *ta.CGraph + cgBig *ta.CGraph + matBig [3]*mat.Dense // 大周期行情 + feasBig *mat.Dense // 大周期特征 + mat [3]*mat.Dense // 小周期行情 + feas *mat.Dense // 小周期特征 + rateBig float64 // 大周期边界倍率 + rate float64 // 边界倍率 + costBigs []float64 // 大周期成本价 + costs []float64 // 小周期成本价 + info []float64 + hisProbs []float64 // 历史行情概率 + diff []float64 // 历史行情概率与真实的差 + pftDoneLong float64 // 已平仓的做多利润 + pftDoneShort float64 // 已平仓做空利润 + pftUPolLong float64 // 未平仓做多利润 + pftUPolShort float64 // 未平仓做空利润 + atr float64 + trues []float64 // 记录最近probs是否相同 + age int // 已持仓bar数 +} + +func AITrade(pol *config.RunPolicyConfig) *strat.TradeStrat { + var seqNum = 150 // 特征序列长度 + var penNum = 120 // 笔矩阵长度 + var depth = 100 // 笔矩阵深度 + var hisProbLen = 100 // 历史行情置信度长度 + var maxOdNum = 1.0 // 单币种单方向最多开1单 + var nextObsNum = 30 // 预测后n个bar的收益 + var diffLen = hisProbLen - nextObsNum + var indsDepth = 30*2 + 14 + const maxMsgSize = 100 * 1024 * 1024 + const maxHoldAge = 300 // 持仓age超过此配置,强制平仓 + addr := "192.168.7.253:8080" + creds := grpc.WithTransportCredentials(insecure.NewCredentials()) + conn, err_ := grpc.NewClient(addr, creds, grpc.WithDefaultCallOptions( + grpc.MaxCallSendMsgSize(maxMsgSize), + grpc.MaxCallRecvMsgSize(maxMsgSize), + )) + if err_ != nil { + panic(err_) + } + client := biz.NewAInferClient(conn) + return &strat.TradeStrat{ + BatchInOut: true, + //DrawDownExit: true, + WarmupNum: 3000, + OnStartUp: func(s *strat.StratJob) { + s.More = &AIMore{ + cg: &ta.CGraph{ParseLevel: ta.ParseLevelPen}, + cgBig: &ta.CGraph{ParseLevel: ta.ParseLevelPen}, + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 3000}, + } + }, + OnBar: func(s *strat.StratJob) { + m, _ := s.More.(*AIMore) + e := s.Env + //if !core.IsWarmUp { + // log.Info("OnBar", zap.Int64("time", e.TimeStop)) + // defer log.Info("OnBarEnd", zap.Int64("time", e.TimeStop)) + //} + m.cg.AddBar(e) + m.cg.Parse() + m.feas = onAiFeatures(e, seqNum) + if len(m.cg.Pens) < 10 { + return + } + m.mat, m.rate = GenFeaMats(m.cg, penNum, depth, false) + m.costs = avgCostN(e, 10, 20, 40, 80, 160, 320) + o, h, l, c, v := e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0), e.Volume.Get(0) + info := []float64{float64(e.TimeStart / 1000), o, h, l, c, v, + math.Log1p(m.rate), math.Log1p(m.rateBig)} + info = append(info, m.costs...) + info = append(info, m.costBigs...) + info = append(info, 0, 0, 0) + m.info = info + m.atr = ta.ATR(e.High, e.Low, e.Close, 30).Get(0) + m.pftUPolShort = 0 + m.pftUPolLong = 0 + if len(s.LongOrders) == 0 && len(s.ShortOrders) == 0 { + m.pftDoneLong = 0 + m.pftDoneShort = 0 + } else { + for _, od := range s.LongOrders { + if od.Status >= ormo.InOutStatusFullEnter { + m.pftUPolLong += od.ProfitRate + } + } + for _, od := range s.ShortOrders { + if od.Status >= ormo.InOutStatusFullEnter { + m.pftUPolShort += od.ProfitRate + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + //if !core.IsWarmUp { + // log.Info("OnInfoBar", zap.Int64("time", e.TimeStop)) + // defer log.Info("OnInfoBarEnd", zap.Int64("time", e.TimeStop)) + //} + m, _ := s.More.(*AIMore) + m.cgBig.AddBar(e) + m.cgBig.Parse() + m.feasBig = onAiFeatures(e, seqNum) + m.costBigs = avgCostN(e, 10, 20, 40, 80, 160, 320) + if len(m.cgBig.Pens) >= penNum { + m.matBig, m.rateBig = GenFeaMats(m.cgBig, penNum, depth, false) + } + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + var valids []*strat.StratJob + var time1, buy1, sell1, feas1, time2, buy2, sell2, feas2, info []float64 + var graphLen, graphDepth, feasLen, feasDepth, infoDepth int + for _, j := range jobs { + m, _ := j.More.(*AIMore) + if m.matBig[0] == nil || m.mat[0] == nil || len(m.info) != 23 { + continue + } + if len(valids) == 0 { + graphLen, graphDepth = m.mat[0].Dims() + feasLen, feasDepth = m.feas.Dims() + infoDepth = len(m.info) + } + valids = append(valids, j) + time1 = append(time1, m.mat[0].RawMatrix().Data...) + buy1 = append(buy1, m.mat[1].RawMatrix().Data...) + sell1 = append(sell1, m.mat[2].RawMatrix().Data...) + feas1 = append(feas1, m.feas.RawMatrix().Data...) + time2 = append(time2, m.matBig[0].RawMatrix().Data...) + buy2 = append(buy2, m.matBig[1].RawMatrix().Data...) + sell2 = append(sell2, m.matBig[2].RawMatrix().Data...) + feas2 = append(feas2, m.feasBig.RawMatrix().Data...) + info = append(info, m.info...) + } + //if !core.IsWarmUp { + // log.Info("OnBatchJobs", zap.Int64("time", timeStop)) + // defer log.Info("OnBatchJobs", zap.Int64("time", timeStop)) + //} + if len(valids) == 0 { + return + } + bSize := len(valids) + klineShape := []int32{int32(bSize), int32(graphLen), int32(graphDepth)} + feaShape := []int32{int32(bSize), int32(feasLen), int32(feasDepth)} + ctx := context.Background() + + trend, err_ := client.Trend(ctx, &biz.ArrMap{ + Mats: map[string]*biz.NumArr{ + "time": {Data: time1, Shape: klineShape}, + "buy": {Data: buy1, Shape: klineShape}, + "sell": {Data: sell1, Shape: klineShape}, + "feas": {Data: feas1, Shape: feaShape}, + "time2": {Data: time2, Shape: klineShape}, + "buy2": {Data: buy2, Shape: klineShape}, + "sell2": {Data: sell2, Shape: klineShape}, + "feas2": {Data: feas2, Shape: feaShape}, + "info": {Data: info, Shape: []int32{int32(bSize), int32(infoDepth)}}, + }, + }) + if err_ != nil { + log.Panic("call ai trend fail", zap.Int("bSize", bSize), zap.Error(err_)) + } + probs := trend.Mats["probs"].Data + if len(probs) != bSize*3 { + panic(fmt.Sprintf("probs length error, expect: %v, got: %v", len(probs), bSize*3)) + } + infoMat := trend.Mats["info"] + rawInfo := infoMat.Data + rawInfoDepth := int(infoMat.Shape[1]) + hisMin := hisProbLen * 3 + var his, diff, inds []float64 + info = nil + var items []*strat.StratJob + for i, j := range valids { + m, _ := j.More.(*AIMore) + curProbs := probs[i*3 : (i+1)*3] + m.hisProbs = append(m.hisProbs, curProbs...) + if len(m.hisProbs) > nextObsNum*3 { + start := len(m.hisProbs) - (nextObsNum+1)*3 + p30Preds := m.hisProbs[start : start+3] + p30Trues := genCurFutProbs(j.Env) + m.diff = append(m.diff, math.Abs(p30Trues[0]-p30Preds[0]), + math.Abs(p30Trues[1]-p30Preds[1]), + math.Abs(p30Trues[2]-p30Preds[2])) + m.logTrue([3]float64{p30Trues[0], p30Trues[1], p30Trues[2]}, p30Preds) + } + if len(m.hisProbs) >= hisMin { + m.hisProbs = m.hisProbs[len(m.hisProbs)-hisMin:] + items = append(items, j) + his = append(his, m.hisProbs...) + m.diff = m.diff[len(m.diff)-diffLen*3:] + diff = append(diff, m.diff...) + m.trues = m.trues[len(m.trues)-diffLen:] + inds = append(inds, m.feas.RawRowView(feasLen-1)...) + inds = append(inds, m.feasBig.RawRowView(feasLen-1)...) + inds = append(inds, rawInfo[i*rawInfoDepth:(i+1)*rawInfoDepth]...) + info = append(info, m.pftDoneLong/maxOdNum, m.pftDoneShort/maxOdNum, + m.pftUPolLong/maxOdNum, m.pftUPolShort/maxOdNum, + float64(len(j.LongOrders))/maxOdNum, float64(len(j.ShortOrders))/maxOdNum, + avgArr(m.trues), + ) + if m.age > 0 && len(j.LongOrders) == 0 && len(j.ShortOrders) == 0 { + m.age = 0 + } + } + } + bSize = len(items) + if bSize == 0 { + return + } + trade, err_ := client.Trade(ctx, &biz.ArrMap{ + Mats: map[string]*biz.NumArr{ + "his": {Data: his, Shape: []int32{int32(bSize), int32(hisProbLen), 3}}, + "diff": {Data: diff, Shape: []int32{int32(bSize), int32(diffLen), 3}}, + "inds": {Data: inds, Shape: []int32{int32(bSize), int32(indsDepth)}}, + "info": {Data: info, Shape: []int32{int32(bSize), 7}}, + }, + }) + if err_ != nil { + panic(err_) + } + actions := trade.Mats["actions"].Data + for i, j := range items { + m, _ := j.More.(*AIMore) + act := int(math.Round(actions[i])) + truncated := false + if len(j.LongOrders) > 0 || len(j.ShortOrders) > 0 { + m.age += 1 + if m.age >= maxHoldAge { + truncated = true + } + } + if (act == 3 || truncated) && len(j.LongOrders) > 0 { + _ = j.CloseOrders(&strat.ExitReq{ + Tag: "exit_long", + Dirt: core.OdDirtLong, + }) + } + if truncated || act == 4 && len(j.ShortOrders) > 0 { + _ = j.CloseOrders(&strat.ExitReq{ + Tag: "exit_short", + Dirt: core.OdDirtShort, + }) + } else if act == 1 && len(j.LongOrders) < int(maxOdNum) { + //m, _ := j.More.(*AIMore) + _ = j.OpenOrder(&strat.EnterReq{ + Tag: "long", + //StopLossVal: m.atr * 10, + }) + } else if act == 2 && len(j.ShortOrders) < int(maxOdNum) { + //m, _ := j.More.(*AIMore) + _ = j.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + //StopLossVal: m.atr * 10, + }) + } + } + }, + //OnCheckExit: func(s *strategy.StratJob, od *ormo.InOutOrder) *strategy.ExitReq { + // slVal := od.GetInfoFloat64(orm.OdInfoStopLossVal) + // holdNum := int((s.Env.TimeStop - od.EnterAt) / s.Env.TFMSecs) + // rate := math.Abs(s.Env.Close.Get(0)-od.InitPrice) / slVal + // if holdNum > nextObsNum && (od.ProfitRate < 0 || rate < 0.3) { + // // 止损:持仓30个bar && (亏损状态 or 盈利不足最小预期30%) + // return &strategy.ExitReq{ + // Tag: "exit_bad", + // OrderID: od.ID, + // } + // } + // return nil + //}, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + sl := od.GetStopLoss() + if sl == nil || sl.Range == 0 { + return 0 + } + bestRate := maxChg * od.InitPrice / sl.Range + if bestRate > 1.2 { + // 最大盈利是止损区间2倍,以30%回撤止盈 + return 0.3 + } + return 0 + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + m, _ := s.More.(*AIMore) + if chgType == strat.OdChgExitFill { + if od.Short { + m.pftDoneShort += od.ProfitRate + } else { + m.pftDoneLong += od.ProfitRate + } + } + }, + } +} + +func compareProb(trues [3]float64, preds []float64) float64 { + var mx, maxv = 0, trues[0] + for i := 1; i < 3; i++ { + if trues[i] > maxv { + maxv = trues[i] + mx = i + } + } + var argMaxA = mx + mx, maxv = 0, preds[0] + for i := 1; i < 3; i++ { + if preds[i] > maxv { + maxv = preds[i] + mx = i + } + } + if argMaxA == mx { + return 1 + } else { + return 0 + } +} + +func (m *AIMore) logTrue(trues [3]float64, preds []float64) { + val := compareProb(trues, preds) + m.trues = append(m.trues, val) +} + +func genCurFutProbs(e *ta.BarEnv) [3]float64 { + var nextObsNum = 30 // 预测后n个bar的收益 + var bars = make([]*banexg.Kline, 0, nextObsNum) + // opens等都是逆序的,第0个是最新的 + opens := e.Open.Range(0, nextObsNum) + highs := e.High.Range(0, nextObsNum) + lows := e.Low.Range(0, nextObsNum) + closes := e.Close.Range(0, nextObsNum) + vols := e.Volume.Range(0, nextObsNum) + for i := len(opens) - 1; i >= 0; i-- { + // 从后往前,即按时间顺序 + bars = append(bars, &banexg.Kline{ + Open: opens[i], + High: highs[i], + Low: lows[i], + Close: closes[i], + Volume: vols[i], + }) + } + minChgVal := ta.ATR(e.High, e.Low, e.Close, 30).Get(nextObsNum) * 5 + return genFutProbsV1(bars, minChgVal, 0.4) +} + +func avgArr(data []float64) float64 { + if len(data) == 0 { + return 0 + } + var sum = float64(0) + for _, v := range data { + sum += v + } + return sum / float64(len(data)) +} diff --git a/rpc_fea/fea_trade2.go b/rpc_fea/fea_trade2.go new file mode 100644 index 0000000..e95d06d --- /dev/null +++ b/rpc_fea/fea_trade2.go @@ -0,0 +1,324 @@ +package rpc_fea + +import ( + "context" + "fmt" + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + "github.com/banbox/banexg/log" + ta "github.com/banbox/banta" + "go.uber.org/zap" + "gonum.org/v1/gonum/mat" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "math" +) + +type AIMore2 struct { + cg *ta.CGraph + feasBig *mat.Dense // 大周期特征 + mat [2]*mat.Dense // 小周期行情 + feas *mat.Dense // 小周期特征 + costBigs []float64 // 大周期成本价 + costs []float64 // 小周期成本价 + info []float64 + hisProbs []float64 // 历史行情概率 + diff []float64 // 历史行情概率与真实的差 + pftDoneLong float64 // 已平仓的做多利润 + pftDoneShort float64 // 已平仓做空利润 + pftUPolLong float64 // 未平仓做多利润 + pftUPolShort float64 // 未平仓做空利润 + atr float64 + trues []float64 // 记录最近probs是否相同 + age int // 已持仓bar数 +} + +func AITrade2(pol *config.RunPolicyConfig) *strat.TradeStrat { + var seqNum = 150 // 特征序列长度 + var penNum = 120 // 笔矩阵长度 + var depth = 100 // 笔矩阵深度 + var hisProbLen = 100 // 历史行情置信度长度 + var maxOdNum = 1.0 // 单币种单方向最多开1单 + var nextObsNum = 30 // 预测后n个bar的收益 + var diffLen = hisProbLen - nextObsNum + var indsDepth = 72 + 12 + const maxMsgSize = 100 * 1024 * 1024 + const maxHoldAge = 100 // 持仓age超过此配置,强制平仓 + addr := "192.168.7.253:8080" + creds := grpc.WithTransportCredentials(insecure.NewCredentials()) + conn, err_ := grpc.NewClient(addr, creds, grpc.WithDefaultCallOptions( + grpc.MaxCallSendMsgSize(maxMsgSize), + grpc.MaxCallRecvMsgSize(maxMsgSize), + )) + if err_ != nil { + panic(err_) + } + client := biz.NewAInferClient(conn) + return &strat.TradeStrat{ + BatchInOut: true, + DrawDownExit: true, + WarmupNum: 3000, + OnStartUp: func(s *strat.StratJob) { + s.More = &AIMore2{ + cg: &ta.CGraph{ParseLevel: ta.ParseLevelPen, MaxPen: penNum * 3 / 2}, + } + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1h", 300}, + } + }, + OnBar: func(s *strat.StratJob) { + m, _ := s.More.(*AIMore2) + e := s.Env + m.cg.AddBar(e) + m.cg.Parse() + m.feas = onAiFeatures2(e, 30) + if len(m.cg.Pens) < 10 { + return + } + m.mat, _ = GenPenMats2(m.cg, penNum, depth, false) + m.costs = avgCostN(e, 10, 20, 40, 80, 160, 320) + o, h, l, c, v := e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0), e.Volume.Get(0) + info := make([]float64, 0, 20) + info = append(info, float64(e.TimeStart/1000), o, h, l, c, v) + info = append(info, m.costs...) + info = append(info, m.costBigs...) + m.info = info + m.atr = ta.ATR(e.High, e.Low, e.Close, 30).Get(0) + m.pftUPolShort = 0 + m.pftUPolLong = 0 + if len(s.LongOrders) == 0 && len(s.ShortOrders) == 0 { + m.pftDoneLong = 0 + m.pftDoneShort = 0 + } else { + for _, od := range s.LongOrders { + if od.Status >= ormo.InOutStatusFullEnter { + m.pftUPolLong += od.ProfitRate + } + } + for _, od := range s.ShortOrders { + if od.Status >= ormo.InOutStatusFullEnter { + m.pftUPolShort += od.ProfitRate + } + } + } + + //if !s.IsWarmUp { + // curDate := btime.ToDateStr(e.TimeStart, core.DefaultDateFmt) + // curCloseVal := e.Close.Get(0) + // a := staToStr(sampleArr(m.mat[0].RawMatrix().Data)) + // b := staToStr(sampleArr(m.mat[1].RawMatrix().Data)) + // cc := staToStr(sampleArr(m.feas.RawMatrix().Data)) + // d := staToStr(sampleArr(m.feasBig.RawMatrix().Data)) + // row := fmt.Sprintf("%s, %.5f, %s, %s, %s, %s", curDate, curCloseVal, a, b, cc, d) + // s.Strat.Outputs = append(s.Strat.Outputs, row) + //} + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*AIMore2) + m.feasBig = onAiFeatures2(e, seqNum) + m.costBigs = avgCostN(e, 10, 20, 40, 80, 160, 320) + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + var valids []*strat.StratJob + var one, two, feas1, feas2, bars, info []float64 + var graphLen, graphDepth, feasLen, feasLen2, feasDepth, infoDepth int + for _, j := range jobs { + m, _ := j.More.(*AIMore2) + if m.mat[0] == nil || len(m.info) != 18 { + continue + } + if len(valids) == 0 { + graphLen, graphDepth = m.mat[0].Dims() + feasLen, feasDepth = m.feas.Dims() + feasLen2, _ = m.feasBig.Dims() + infoDepth = len(m.info) - 6 + } + valids = append(valids, j) + one = append(one, m.mat[0].RawMatrix().Data...) + two = append(two, m.mat[1].RawMatrix().Data...) + feas1 = append(feas1, m.feas.RawMatrix().Data...) + feas2 = append(feas2, m.feasBig.RawMatrix().Data...) + bars = append(bars, m.info[:6]...) + info = append(info, m.info[6:]...) + } + if len(valids) == 0 { + return + } + bSize := len(valids) + klineShape := []int32{int32(bSize), int32(graphLen), int32(graphDepth)} + feaShape1 := []int32{int32(bSize), int32(feasLen), int32(feasDepth)} + feaShape2 := []int32{int32(bSize), int32(feasLen2), int32(feasDepth)} + ctx := context.Background() + + trend, err_ := client.Trend(ctx, &biz.ArrMap{ + Mats: map[string]*biz.NumArr{ + "one": {Data: one, Shape: klineShape}, + "two": {Data: two, Shape: klineShape}, + "feas": {Data: feas1, Shape: feaShape1}, + "feas2": {Data: feas2, Shape: feaShape2}, + "bar": {Data: bars, Shape: []int32{int32(bSize), 6}}, + "info": {Data: info, Shape: []int32{int32(bSize), int32(infoDepth)}}, + }, + }) + if err_ != nil { + log.Panic("call ai trend fail", zap.Int("bSize", bSize), zap.Error(err_)) + } + probs := trend.Mats["probs"].Data + outDim := 12 + if len(probs) != bSize*outDim { + panic(fmt.Sprintf("probs length error, expect: %v, got: %v", bSize*outDim, len(probs))) + } + infoMat := trend.Mats["info"] + rawInfo := infoMat.Data + rawInfoDepth := int(infoMat.Shape[1]) + hisMin := hisProbLen * outDim + var his, diff, inds []float64 + info = nil + var items []*strat.StratJob + for i, j := range valids { + m, _ := j.More.(*AIMore2) + curProbs := probs[i*outDim : (i+1)*outDim] + m.hisProbs = append(m.hisProbs, curProbs...) + if len(m.hisProbs) > nextObsNum*outDim { + start := len(m.hisProbs) - (nextObsNum+1)*outDim + p30Preds := m.hisProbs[start : start+outDim] + p30Trues := genCurFutProbs(j.Env) + m.diff = append(m.diff, math.Abs(p30Trues[0]-p30Preds[0]), + math.Abs(p30Trues[1]-p30Preds[1]), + math.Abs(p30Trues[2]-p30Preds[2]), + math.Abs(p30Trues[0]-p30Preds[3]), + math.Abs(p30Trues[1]-p30Preds[4]), + math.Abs(p30Trues[2]-p30Preds[5]), + math.Abs(p30Trues[0]-p30Preds[6]), + math.Abs(p30Trues[1]-p30Preds[7]), + math.Abs(p30Trues[2]-p30Preds[8]), + math.Abs(p30Trues[0]-p30Preds[9]), + math.Abs(p30Trues[1]-p30Preds[10]), + math.Abs(p30Trues[2]-p30Preds[11])) + m.logTrue([3]float64{p30Trues[0], p30Trues[1], p30Trues[2]}, p30Preds) + } + if len(m.hisProbs) >= hisMin { + m.hisProbs = m.hisProbs[len(m.hisProbs)-hisMin:] + items = append(items, j) + his = append(his, m.hisProbs...) + m.diff = m.diff[len(m.diff)-diffLen*outDim:] + diff = append(diff, m.diff...) + m.trues = m.trues[len(m.trues)-diffLen:] + inds = append(inds, m.feasBig.RawRowView(feasLen-1)...) + inds = append(inds, rawInfo[i*rawInfoDepth:(i+1)*rawInfoDepth]...) + info = append(info, (m.pftDoneLong+m.pftUPolLong)/maxOdNum, + (m.pftDoneShort+m.pftUPolShort)/maxOdNum, + float64(len(j.LongOrders))/maxOdNum, float64(len(j.ShortOrders))/maxOdNum, + avgArr(m.trues), + ) + if m.age > 0 && len(j.LongOrders) == 0 && len(j.ShortOrders) == 0 { + m.age = 0 + } + } + } + bSize = len(items) + if bSize == 0 { + return + } + trade, err_ := client.Trade(ctx, &biz.ArrMap{ + Mats: map[string]*biz.NumArr{ + "his": {Data: his, Shape: []int32{int32(bSize), int32(hisProbLen), int32(outDim)}}, + "diff": {Data: diff, Shape: []int32{int32(bSize), int32(diffLen), int32(outDim)}}, + "inds": {Data: inds, Shape: []int32{int32(bSize), int32(indsDepth)}}, + "info": {Data: info, Shape: []int32{int32(bSize), 5}}, + }, + }) + if err_ != nil { + panic(err_) + } + actions := trade.Mats["actions"].Data + for i, j := range items { + m, _ := j.More.(*AIMore2) + act := int(math.Round(actions[i])) + truncated := false + if len(j.LongOrders) > 0 || len(j.ShortOrders) > 0 { + m.age += 1 + if m.age >= maxHoldAge { + truncated = true + } + } + if (truncated || act == 3 || act == 2) && len(j.LongOrders) > 0 { + _ = j.CloseOrders(&strat.ExitReq{ + Tag: "exit_long", + Dirt: core.OdDirtLong, + }) + } + if (truncated || act == 4 || act == 1) && len(j.ShortOrders) > 0 { + _ = j.CloseOrders(&strat.ExitReq{ + Tag: "exit_short", + Dirt: core.OdDirtShort, + }) + } + if !truncated { + if act == 1 && len(j.LongOrders) < int(maxOdNum) { + m, _ := j.More.(*AIMore2) + _ = j.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: m.atr * 5, + }) + } else if act == 2 && len(j.ShortOrders) < int(maxOdNum) { + m, _ := j.More.(*AIMore2) + _ = j.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + StopLossVal: m.atr * 5, + }) + } + } + } + }, + OnCheckExit: func(s *strat.StratJob, od *ormo.InOutOrder) *strat.ExitReq { + sl := od.GetStopLoss() + if sl == nil || sl.Range == 0 { + return nil + } + holdNum := int((s.Env.TimeStop - od.RealEnterMS()) / s.Env.TFMSecs) + rate := math.Abs(s.Env.Close.Get(0)-od.InitPrice) / sl.Range + if holdNum > nextObsNum && (od.ProfitRate < 0 || rate < 0.3) { + // 止损:持仓30个bar && (亏损状态 or 盈利不足最小预期30%) + return &strat.ExitReq{ + Tag: "exit_bad", + OrderID: od.ID, + } + } + return nil + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + sl := od.GetStopLoss() + if sl == nil || sl.Range == 0 { + return 0 + } + bestRate := maxChg * od.InitPrice / sl.Range + if bestRate > 1.2 { + // 最大盈利是止损区间2倍,以30%回撤止盈 + return 0.3 + } + return 0 + }, + OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) { + m, _ := s.More.(*AIMore2) + if chgType == strat.OdChgExitFill { + if od.Short { + m.pftDoneShort += od.ProfitRate + } else { + m.pftDoneLong += od.ProfitRate + } + } + }, + } +} + +func (m *AIMore2) logTrue(trues [3]float64, preds []float64) { + val := compareProb(trues, preds) + m.trues = append(m.trues, val) +} diff --git a/rpc_fea/main.go b/rpc_fea/main.go new file mode 100644 index 0000000..612de60 --- /dev/null +++ b/rpc_fea/main.go @@ -0,0 +1,14 @@ +package rpc_fea + +import ( + "github.com/banbox/banbot/biz" + "github.com/banbox/banbot/strat" +) + +func init() { + biz.FeaGenerators["aifea"] = pubAiFea + biz.FeaGenerators["aifea2"] = pubAiFea2 + biz.FeaGenerators["fea_times"] = pubFeaTimes + strat.StratMake["rpc_fea:ai"] = AITrade + strat.StratMake["rpc_fea:ai2"] = AITrade2 +} diff --git a/scripts/migrate_backtests.ps1 b/scripts/migrate_backtests.ps1 new file mode 100644 index 0000000..75a80b7 --- /dev/null +++ b/scripts/migrate_backtests.ps1 @@ -0,0 +1,1259 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [ValidateSet('Inventory', 'Migrate', 'Integrate', 'Build', 'Replay', 'Recent', 'All')] + [string]$Phase = 'Inventory', + [string]$ManifestPath = '', + [string]$BotPath = '', + [double]$FloatTolerance = 1e-9, + [ValidateRange(0, 10)] + [int]$MaxRepairAttempts = 3, + [ValidateRange(0, 5)] + [int]$MaxCodexValidationAttempts = 2, + [ValidateSet('low', 'medium', 'high')] + [string]$CodexReasoning = 'medium', + [string]$DatabaseUrl = $env:BANBOT_DATABASE_URL, + [string]$DatabaseOverlay = '', + [ValidateSet('', 'timescale', 'questdb')] + [string]$DatabaseType = '', + [ValidateRange(1, 1440)] + [int]$BacktestTimeoutMinutes = 120, + [switch]$Force +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$script:RepoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +if (-not $ManifestPath) { $ManifestPath = Join-Path $script:RepoRoot 'code\migration_manifest.json' } +if (-not $BotPath) { $BotPath = Join-Path $script:RepoRoot 'bot.exe' } +$script:ManifestPath = [IO.Path]::GetFullPath($ManifestPath) +$script:Manifest = Get-Content -LiteralPath $script:ManifestPath -Raw | ConvertFrom-Json +$script:StatePath = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.stateFile)) +$script:SummaryPath = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.summaryFile)) +$script:WorkRoot = Join-Path $script:RepoRoot 'code\.migration-work' +$script:LogRoot = Join-Path $script:RepoRoot 'code\migration-logs' +$script:RecentToday = [DateTime]::UtcNow.Date +$script:AllReportPolicies = @() +$script:UncoveredPolicies = @() + +function Write-JsonAtomic { + param([Parameter(Mandatory)]$Value, [Parameter(Mandatory)][string]$Path) + $parent = Split-Path -Parent $Path + New-Item -ItemType Directory -Force -Path $parent | Out-Null + $temp = "$Path.tmp" + $Value | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $temp -Encoding UTF8 + Move-Item -LiteralPath $temp -Destination $Path -Force +} + +function Get-RelativePath { + param([Parameter(Mandatory)][string]$Base, [Parameter(Mandatory)][string]$Path) + $baseFull = [IO.Path]::GetFullPath($Base).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar + $pathFull = [IO.Path]::GetFullPath($Path) + $baseUri = New-Object Uri($baseFull) + $pathUri = New-Object Uri($pathFull) + return [Uri]::UnescapeDataString($baseUri.MakeRelativeUri($pathUri).ToString()).Replace('/', [IO.Path]::DirectorySeparatorChar) +} + +function Assert-DescendantPath { + param([Parameter(Mandatory)][string]$Root, [Parameter(Mandatory)][string]$Path) + $rootFull = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar + $pathFull = [IO.Path]::GetFullPath($Path) + if (-not $pathFull.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase)) { + throw "Unsafe path outside expected root: $pathFull" + } + return $pathFull +} + +function Assert-NoReparsePath { + param([Parameter(Mandatory)][string]$Root, [Parameter(Mandatory)][string]$Path) + $rootFull = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/') + $pathFull = Assert-DescendantPath -Root $rootFull -Path $Path + $relative = $pathFull.Substring($rootFull.Length).TrimStart('\', '/') + $current = $rootFull + foreach ($part in @($relative -split '[\\/]' | Where-Object { $_ })) { + $current = Join-Path $current $part + if (Test-Path -LiteralPath $current) { + $item = Get-Item -LiteralPath $current -Force + if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { + throw "Refusing reparse-point path: $current" + } + } + } + return $pathFull +} + +function Read-State { + if (Test-Path -LiteralPath $script:StatePath) { + return Get-Content -LiteralPath $script:StatePath -Raw | ConvertFrom-Json + } + return [pscustomobject]@{ + schemaVersion = 1 + manifest = $script:ManifestPath + completed = @() + updatedUtc = [DateTime]::UtcNow.ToString('o') + } +} + +function Test-Completed { + param([Parameter(Mandatory)][string]$Key) + $state = Read-State + return @($state.completed) -contains $Key +} + +function Set-Completed { + param([Parameter(Mandatory)][string]$Key) + $state = Read-State + $completed = @($state.completed) + if ($completed -notcontains $Key) { + $completed += $Key + } + $state.completed = @($completed | Sort-Object -Unique) + $state.updatedUtc = [DateTime]::UtcNow.ToString('o') + Write-JsonAtomic -Value $state -Path $script:StatePath +} + +function Invoke-LoggedCommand { + param( + [Parameter(Mandatory)][string]$FilePath, + [Parameter(Mandatory)][string[]]$Arguments, + [Parameter(Mandatory)][string]$LogPath, + [string]$InputText = '', + [string]$WorkingDirectory = $script:RepoRoot, + [ValidateRange(1, 86400)][int]$TimeoutSeconds = 3600 + ) + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LogPath) | Out-Null + if (-not [IO.Path]::IsPathRooted($FilePath)) { + $application = Get-Command $FilePath -CommandType Application -ErrorAction SilentlyContinue | + Where-Object { $_.Source -match '\.(?:cmd|bat|com|exe)$' } | + Select-Object -First 1 + if ($null -eq $application) { throw "Executable not found: $FilePath" } + $FilePath = $application.Source + } + $quotedArgs = foreach ($argument in $Arguments) { + $escaped = [regex]::Replace([string]$argument, '(\\*)"', '$1$1\"') + $escaped = [regex]::Replace($escaped, '(\\+)$', '$1$1') + '"' + $escaped + '"' + } + $startInfo = New-Object Diagnostics.ProcessStartInfo + if ([IO.Path]::GetExtension($FilePath) -match '^\.(?:cmd|bat)$') { + $startInfo.FileName = $env:ComSpec + $startInfo.Arguments = '/d /s /c ""' + $FilePath + '" ' + ($quotedArgs -join ' ') + '"' + } else { + $startInfo.FileName = $FilePath + $startInfo.Arguments = $quotedArgs -join ' ' + } + $startInfo.WorkingDirectory = $WorkingDirectory + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $startInfo.RedirectStandardInput = $true + $utf8NoBom = New-Object Text.UTF8Encoding($false) + if ($startInfo.PSObject.Properties.Name -contains 'StandardInputEncoding') { $startInfo.StandardInputEncoding = $utf8NoBom } + if ($startInfo.PSObject.Properties.Name -contains 'StandardOutputEncoding') { $startInfo.StandardOutputEncoding = $utf8NoBom } + if ($startInfo.PSObject.Properties.Name -contains 'StandardErrorEncoding') { $startInfo.StandardErrorEncoding = $utf8NoBom } + $process = New-Object Diagnostics.Process + $process.StartInfo = $startInfo + try { + if (-not $process.Start()) { throw "Failed to start $FilePath" } + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + if ($InputText) { + $inputBytes = [Text.Encoding]::UTF8.GetBytes($InputText) + $process.StandardInput.BaseStream.Write($inputBytes, 0, $inputBytes.Length) + $process.StandardInput.BaseStream.Flush() + } + $process.StandardInput.BaseStream.Close() + $timedOut = -not $process.WaitForExit($TimeoutSeconds * 1000) + if ($timedOut) { + try { $process.Kill() } catch { } + $process.WaitForExit() + } + $stdout = $stdoutTask.Result + $stderr = $stderrTask.Result + $exitCode = $process.ExitCode + $output = @($stdout, $stderr) | Where-Object { $_ } + ($output -join [Environment]::NewLine) | Set-Content -LiteralPath $LogPath -Encoding UTF8 + if ($timedOut) { throw "Command timed out after $TimeoutSeconds seconds: $FilePath" } + } finally { + $process.Dispose() + } + return [pscustomobject]@{ ExitCode = $exitCode; Output = @($output); LogPath = $LogPath } +} + +function Get-ReportRoot { + param([Parameter(Mandatory)]$Report) + return [IO.Path]::GetFullPath((Join-Path $script:Manifest.reportsRoot $Report.id)) +} + +function Get-ConfigPolicyNames { + param([Parameter(Mandatory)][string]$Path) + $inside = $false + $names = New-Object Collections.Generic.List[string] + foreach ($line in Get-Content -LiteralPath $Path) { + if ($line -match '^run_policy\s*:') { + $inside = $true + continue + } + if ($inside -and $line -match '^\S') { $inside = $false } + if ($inside -and $line -match '^\s{2}-\s+name:\s*(?[^#\s]+)') { + $names.Add($Matches.name) + } + } + return @($names | Sort-Object -Unique) +} + +function Get-PolicyParts { + param([Parameter(Mandatory)][string]$Name) + $parts = $Name.Split(':', 2) + if ($parts.Count -eq 1) { + return [pscustomobject]@{ Group = $Name; Strategy = $Name } + } + return [pscustomobject]@{ Group = $parts[0]; Strategy = $parts[1] } +} + +function Get-TargetSourceFiles { + param([Parameter(Mandatory)][string]$ReportRoot, [Parameter(Mandatory)][string[]]$Policies) + $files = New-Object Collections.Generic.List[IO.FileInfo] + foreach ($group in @($Policies | ForEach-Object { (Get-PolicyParts $_).Group } | Sort-Object -Unique)) { + $groupDir = Join-Path $ReportRoot "strat_$group" + if (-not (Test-Path -LiteralPath $groupDir -PathType Container)) { return @() } + if ($group -eq 'llmjob') { + $main = Join-Path $groupDir 'main.go' + if (Test-Path -LiteralPath $main -PathType Leaf) { $files.Add((Get-Item -LiteralPath $main)) } + foreach ($policy in @($Policies | Where-Object { (Get-PolicyParts $_).Group -eq $group })) { + $strategy = (Get-PolicyParts $policy).Strategy + $relative = if ($strategy.StartsWith('H', [StringComparison]::Ordinal) -and $strategy.Length -gt 1) { + Join-Path 'his' ($strategy.Substring(1) + '.go') + } else { + $strategy + '.go' + } + $source = Join-Path $groupDir $relative + if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { return @() } + $files.Add((Get-Item -LiteralPath $source)) + } + } else { + foreach ($file in Get-ChildItem -LiteralPath $groupDir -File -Filter '*.go' -Recurse | Where-Object Name -notlike '*_test.go') { + $files.Add($file) + } + } + } + return @($files | Sort-Object FullName -Unique | ForEach-Object { + (Get-RelativePath -Base $ReportRoot -Path $_.FullName).Replace('\', '/') + }) +} + +function New-AutoRepresentative { + param([Parameter(Mandatory)][IO.DirectoryInfo]$Directory, [Parameter(Mandatory)][string[]]$Policies) + $sources = @(Get-TargetSourceFiles -ReportRoot $Directory.FullName -Policies $Policies) + if ($sources.Count -eq 0) { return $null } + $groupName = "migrated_$($Directory.Name)" + $policyMap = [ordered]@{} + foreach ($policy in $Policies) { + $policyMap[$policy] = "${groupName}:$((Get-PolicyParts $policy).Strategy)" + } + $target = [ordered]@{ + id = $Directory.Name + sourceFiles = $sources + generatedGroup = $groupName + policyMap = [pscustomobject]$policyMap + } + $configPath = Join-Path $Directory.FullName 'config.yml' + if (Select-String -LiteralPath $configPath -Pattern '^\s+use_latest\s*:\s*true\s*(?:#.*)?$' -Quiet) { + $target.dynamicUniverse = [pscustomobject]@{ source = 'pairmgr/use_latest'; sourceLog = 'out.log' } + } + return [pscustomobject]$target +} + +function Resolve-Representatives { + $resolved = New-Object Collections.Generic.List[object] + $covered = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal) + foreach ($report in @($script:Manifest.representatives)) { + $resolved.Add($report) + foreach ($policy in $report.policyMap.psobject.Properties.Name) { $null = $covered.Add($policy) } + } + + $root = [IO.Path]::GetFullPath($script:Manifest.reportsRoot) + $candidates = New-Object Collections.Generic.List[object] + $allPolicies = New-Object 'Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal) + foreach ($directory in Get-ChildItem -LiteralPath $root -Directory | Sort-Object Name) { + $configPath = Join-Path $directory.FullName 'config.yml' + $detailPath = Join-Path $directory.FullName 'detail.json' + if (-not (Test-Path -LiteralPath $configPath -PathType Leaf) -or -not (Test-Path -LiteralPath $detailPath -PathType Leaf)) { continue } + $policies = @(Get-ConfigPolicyNames -Path $configPath) + if ($policies.Count -eq 0) { continue } + foreach ($policy in $policies) { $null = $allPolicies.Add($policy) } + if (@($script:Manifest.representatives.id) -contains $directory.Name) { continue } + $candidate = New-AutoRepresentative -Directory $directory -Policies $policies + if ($null -ne $candidate) { $candidates.Add($candidate) } + } + + while (@($allPolicies | Where-Object { -not $covered.Contains($_) }).Count -gt 0) { + $best = $null + $bestNew = @() + foreach ($candidate in $candidates) { + $newPolicies = @($candidate.policyMap.psobject.Properties.Name | Where-Object { -not $covered.Contains($_) }) + if ($newPolicies.Count -gt $bestNew.Count) { + $best = $candidate + $bestNew = $newPolicies + } + } + if ($null -eq $best -or $bestNew.Count -eq 0) { + $missing = @($allPolicies | Where-Object { -not $covered.Contains($_) } | Sort-Object) + $script:UncoveredPolicies = $missing + Write-Warning "Skipping policies with no report source snapshot: $($missing -join ', ')" + break + } + $resolved.Add($best) + foreach ($policy in $bestNew) { $null = $covered.Add($policy) } + $remaining = @($candidates | Where-Object id -ne $best.id) + $candidates = New-Object Collections.Generic.List[object] + foreach ($candidate in $remaining) { $candidates.Add($candidate) } + } + $script:AllReportPolicies = @($allPolicies | Sort-Object) + if ($null -eq $script:UncoveredPolicies) { $script:UncoveredPolicies = @() } + return @($resolved | ForEach-Object { $_ }) +} + +function Assert-RepresentativeInputs { + param([Parameter(Mandatory)]$Report) + $reportRoot = Get-ReportRoot $Report + foreach ($required in @('config.yml', 'detail.json')) { + $path = Join-Path $reportRoot $required + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "Missing $required for report $($Report.id): $path" + } + } + foreach ($source in @($Report.sourceFiles)) { + $path = Join-Path $reportRoot $source + Assert-DescendantPath -Root $reportRoot -Path $path | Out-Null + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "Missing allowlisted source for report $($Report.id): $source" + } + } +} + +function Get-SourceFingerprint { + param([Parameter(Mandatory)][string]$ReportRoot, [Parameter(Mandatory)][AllowEmptyCollection()][IO.FileInfo[]]$Files) + if ($Files.Count -eq 0) { return $null } + $rows = foreach ($file in $Files) { + $relative = (Get-RelativePath -Base $ReportRoot -Path $file.FullName).Replace('\', '/') + "$relative|$((Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash)" + } + $payload = [Text.Encoding]::UTF8.GetBytes((($rows | Sort-Object) -join "`n")) + $sha = [Security.Cryptography.SHA256]::Create() + try { return ([BitConverter]::ToString($sha.ComputeHash($payload))).Replace('-', '').ToLowerInvariant() } + finally { $sha.Dispose() } +} + +function Invoke-Inventory { + $root = [IO.Path]::GetFullPath($script:Manifest.reportsRoot) + $reportDirs = @(Get-ChildItem -LiteralPath $root -Directory) + $allDirs = @(Get-ChildItem -LiteralPath $root -Directory -Recurse) + $configFiles = @(Get-ChildItem -LiteralPath $root -File -Filter 'config.yml' -Recurse) + $detailFiles = @(Get-ChildItem -LiteralPath $root -File -Filter 'detail.json' -Recurse) + $sourceFiles = @(Get-ChildItem -LiteralPath $root -File -Filter '*.go' -Recurse | Where-Object { + $_.FullName -match '[\\/]strat_[^\\/]+[\\/]' + }) + + $reports = foreach ($dir in $reportDirs) { + $sources = @(Get-ChildItem -LiteralPath $dir.FullName -File -Filter '*.go' -Recurse | Where-Object { + $_.FullName -match '[\\/]strat_[^\\/]+[\\/]' + }) + [pscustomobject]@{ + id = $dir.Name + configCount = @(Get-ChildItem -LiteralPath $dir.FullName -File -Filter 'config.yml' -Recurse).Count + detailCount = @(Get-ChildItem -LiteralPath $dir.FullName -File -Filter 'detail.json' -Recurse).Count + sourceCount = $sources.Count + sourceFingerprint = Get-SourceFingerprint -ReportRoot $dir.FullName -Files $sources + } + } + + $duplicates = @($reports | Where-Object sourceFingerprint | Group-Object sourceFingerprint | Where-Object Count -gt 1 | ForEach-Object { + [pscustomobject]@{ + fingerprint = $_.Name + count = $_.Count + reports = @($_.Group.id | Sort-Object) + } + }) + $inventory = [pscustomobject]@{ + schemaVersion = 1 + generatedUtc = [DateTime]::UtcNow.ToString('o') + root = $root + counts = [pscustomobject]@{ + reportDirectories = $reportDirs.Count + allDirectories = $allDirs.Count + configFiles = $configFiles.Count + detailFiles = $detailFiles.Count + strategySourceFiles = $sourceFiles.Count + duplicateFingerprints = $duplicates.Count + } + policyCoverage = [pscustomobject]@{ + uniquePolicies = $script:AllReportPolicies.Count + coveredPolicies = @($script:Representatives | ForEach-Object { + $_.policyMap.psobject.Properties.Name + } | Sort-Object -Unique).Count + missingSourcePolicies = @($script:UncoveredPolicies) + representativeReports = $script:Representatives.Count + } + representatives = @($script:Representatives.id) + migrationTargets = @($script:Representatives | ForEach-Object { + [pscustomobject]@{ + id = $_.id + policies = @($_.policyMap.psobject.Properties.Name) + generatedGroup = $_.generatedGroup + sourceFiles = @($_.sourceFiles) + dynamicUniverse = if ($_.psobject.Properties['dynamicUniverse']) { $_.dynamicUniverse } else { $null } + } + }) + duplicateFingerprints = $duplicates + reports = @($reports) + } + $path = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.inventoryFile)) + Write-JsonAtomic -Value $inventory -Path $path + Set-Completed 'inventory' + Write-Host "Inventory written: $path" + Write-Host "Reports=$($reportDirs.Count) configs=$($configFiles.Count) details=$($detailFiles.Count) sources=$($sourceFiles.Count) duplicateSets=$($duplicates.Count)" +} + +function Copy-MigrationInputs { + param([Parameter(Mandatory)]$Report, [Parameter(Mandatory)][string]$Scratch) + if (Test-Path -LiteralPath $Scratch) { + Assert-DescendantPath -Root $script:WorkRoot -Path $Scratch | Out-Null + Remove-Item -LiteralPath $Scratch -Recurse -Force + } + New-Item -ItemType Directory -Force -Path (Join-Path $Scratch 'input'), (Join-Path $Scratch 'output') | Out-Null + Copy-Item -LiteralPath $script:Manifest.knowledgeFile -Destination (Join-Path $Scratch 'input\banbot_cn.md') + $reportRoot = Get-ReportRoot $Report + foreach ($relative in @($Report.sourceFiles)) { + $destination = Join-Path (Join-Path $Scratch 'input\sources') $relative + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $destination) | Out-Null + Copy-Item -LiteralPath (Join-Path $reportRoot $relative) -Destination $destination + } +} + +function Get-PolicyInstructions { + param([Parameter(Mandatory)]$Report) + return @($Report.policyMap.psobject.Properties | ForEach-Object { "- $($_.Name) -> $($_.Value)" }) -join "`n" +} + +function Get-InlineFileBlock { + param([Parameter(Mandatory)][string]$Label, [Parameter(Mandatory)][string]$Path) + $content = Get-Content -LiteralPath $Path -Raw + return "<<>>`n$content`n<<>>" +} + +function Write-CodexFilesSchema { + param([Parameter(Mandatory)][string]$Path) + $schema = @' +{ + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["path", "content"], + "additionalProperties": false + } + } + }, + "required": ["files"], + "additionalProperties": false +} +'@ + [IO.File]::WriteAllText($Path, $schema, (New-Object Text.UTF8Encoding($false))) +} + +function Write-CodexResponseFiles { + param( + [Parameter(Mandatory)][string]$ResponsePath, + [Parameter(Mandatory)][string]$Destination + ) + try { $response = Get-Content -LiteralPath $ResponsePath -Raw | ConvertFrom-Json } + catch { throw "Codex response is not valid JSON: ${ResponsePath}: $($_.Exception.Message)" } + $files = @($response.files) + if ($files.Count -eq 0) { throw "Codex response contains no files: $ResponsePath" } + New-Item -ItemType Directory -Force -Path $Destination | Out-Null + foreach ($file in $files) { + $relative = ([string]$file.path).Replace('/', [IO.Path]::DirectorySeparatorChar) + if ([string]::IsNullOrWhiteSpace($relative) -or [IO.Path]::IsPathRooted($relative) -or + $relative -match '(^|[\\/])\.\.([\\/]|$)' -or [IO.Path]::GetExtension($relative) -ne '.go') { + throw "Unsafe or non-Go path in Codex response: $relative" + } + $target = [IO.Path]::GetFullPath((Join-Path $Destination $relative)) + Assert-DescendantPath -Root $Destination -Path $target | Out-Null + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null + Set-Content -LiteralPath $target -Value ([string]$file.content) -Encoding UTF8 + } +} + +function Assert-GeneratedPackage { + param([Parameter(Mandatory)]$Report, [Parameter(Mandatory)][string]$Directory) + $files = @(Get-ChildItem -LiteralPath $Directory -File -Filter '*.go' -Recurse) + if ($files.Count -eq 0) { throw "Codex produced no Go files for $($Report.id)" } + $text = ($files | ForEach-Object { Get-Content -LiteralPath $_.FullName -Raw }) -join "`n" + if ($text -match '\bOn(?:Info)?Bar\s*:') { + throw "Generated package still uses deprecated OnBar/OnInfoBar callbacks for $($Report.id)" + } + if ($text -notmatch '\bOnData\s*:') { + throw "Generated package does not implement OnData for $($Report.id)" + } + if ($Report.psobject.Properties['requiresRouteData'] -and $Report.requiresRouteData -and $text -notmatch '\bRouteData\s*\(') { + throw "Generated package must use RouteData for mixed main/info callbacks in $($Report.id)" + } + if ($text -notmatch [regex]::Escape("AddStratGroup(`"$($Report.generatedGroup)`"")) { + throw "Generated package does not register group $($Report.generatedGroup)" + } + foreach ($policy in $Report.policyMap.psobject.Properties) { + $strategyName = $policy.Value.Split(':', 2)[1] + if ($text -notmatch ('["'']' + [regex]::Escape($strategyName) + '["'']\s*:')) { + throw "Generated package does not register strategy key $strategyName" + } + } + $directoryFull = [IO.Path]::GetFullPath($Directory) + Assert-DescendantPath -Root $script:RepoRoot -Path $directoryFull | Out-Null + $relative = (Get-RelativePath -Base $script:RepoRoot -Path $directoryFull).Replace('\', '/') + $compile = Invoke-LoggedCommand -FilePath 'go' -Arguments @('test', "./$relative") -LogPath (Join-Path $script:LogRoot "go-test-generated-$($Report.id).log") -TimeoutSeconds 300 + if ($compile.ExitCode -ne 0) { + throw "Generated package does not compile for $($Report.id); see $($compile.LogPath)" + } +} + +function Invoke-CodexMigration { + param([Parameter(Mandatory)]$Report) + Assert-RepresentativeInputs $Report + $scratch = Join-Path $script:WorkRoot $Report.id + Copy-MigrationInputs -Report $Report -Scratch $scratch + $blocks = New-Object Collections.Generic.List[string] + $blocks.Add((Get-InlineFileBlock -Label 'banbot_cn.md' -Path (Join-Path $scratch 'input\banbot_cn.md'))) + foreach ($relative in @($Report.sourceFiles)) { + $blocks.Add((Get-InlineFileBlock -Label $relative.Replace('\', '/') -Path (Join-Path (Join-Path $scratch 'input\sources') $relative))) + } + $inlineInputs = $blocks -join "`n`n" + $prompt = @" +Do not call tools or inspect the filesystem. All allowed inputs are included below. +Return only JSON matching the supplied schema. Each file path must be relative and end in .go. +Generate the complete compilable Go package represented by those files. +Preserve trading behavior. Convert deprecated OnBar/OnInfoBar behavior to OnData using the documented main/info-data helpers so info data cannot execute main-bar logic twice. +The package must register exactly this unique group: $($Report.generatedGroup) +Register these policy mappings: +$(Get-PolicyInstructions $Report) + +Use package name report_$($Report.id). Remove registrations and code for strategies outside the mappings above. Resolve dependencies from the allowlisted files without importing the old banstagy strategy package. Do not add dependencies. +Do not include markdown fences or explanations. + +$inlineInputs +"@ + $logPath = Join-Path $script:LogRoot "codex-migrate-$($Report.id).log" + $schemaPath = Join-Path $scratch 'files-schema.json' + $responsePath = Join-Path $scratch 'files-response.json' + Write-CodexFilesSchema -Path $schemaPath + $reasoningConfig = 'model_reasoning_effort="' + $CodexReasoning + '"' + $result = Invoke-LoggedCommand -FilePath 'codex' -Arguments @('exec', '--disable', 'hooks', '--disable', 'multi_agent', '-c', $reasoningConfig, '--sandbox', 'read-only', '--ephemeral', '--skip-git-repo-check', '--output-schema', $schemaPath, '--output-last-message', $responsePath, '-C', $scratch, '-') -LogPath $logPath -InputText $prompt -WorkingDirectory $scratch + if ($result.ExitCode -ne 0) { throw "Codex migration failed for $($Report.id); see $logPath" } + $output = Join-Path $scratch 'output' + Write-CodexResponseFiles -ResponsePath $responsePath -Destination $output + $formatArgs = @('-w') + @(Get-ChildItem -LiteralPath $output -File -Filter '*.go' -Recurse | Select-Object -ExpandProperty FullName) + $format = Invoke-LoggedCommand -FilePath 'gofmt' -Arguments $formatArgs -LogPath (Join-Path $script:LogRoot "gofmt-migrate-$($Report.id).log") + if ($format.ExitCode -ne 0) { throw "gofmt failed for $($Report.id); see $($format.LogPath)" } + Assert-GeneratedPackage -Report $Report -Directory $output + + $generatedRoot = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.generatedRoot)) + $destination = Join-Path $generatedRoot $Report.id + if (Test-Path -LiteralPath $destination) { + Assert-DescendantPath -Root $generatedRoot -Path $destination | Out-Null + Remove-Item -LiteralPath $destination -Recurse -Force + } + New-Item -ItemType Directory -Force -Path $destination | Out-Null + Copy-Item -Path (Join-Path $output '*') -Destination $destination -Recurse + Set-Completed "migrate:$($Report.id)" + Write-Host "Migrated $($Report.id) -> $destination" +} + +function Invoke-Migrate { + foreach ($report in $script:Representatives) { + $destination = Join-Path (Join-Path $script:RepoRoot $script:Manifest.generatedRoot) $report.id + if (-not $Force -and (Test-Completed "migrate:$($report.id)") -and (Test-Path -LiteralPath $destination)) { + Write-Host "Resume: migration already complete for $($report.id)" + continue + } + Invoke-CodexMigration -Report $report + } +} + +function Invoke-Integrate { + foreach ($report in $script:Representatives) { + $destination = Join-Path (Join-Path $script:RepoRoot $script:Manifest.generatedRoot) $report.id + Assert-GeneratedPackage -Report $report -Directory $destination + } + $imports = @($script:Representatives | Sort-Object id | ForEach-Object { + "`t_ `"github.com/banbox/banstagy/code/generated/$($_.id)`"" + }) -join "`r`n" + $registry = @" +// Code generated by scripts/migrate_backtests.ps1; DO NOT EDIT. +package code + +import ( +$imports +) +"@ + $registryPath = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.registryFile)) + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $registryPath) | Out-Null + Set-Content -LiteralPath $registryPath -Value $registry -Encoding UTF8 + + $mainPath = Join-Path $script:RepoRoot 'main.go' + $mainText = Get-Content -LiteralPath $mainPath -Raw + $importLine = "`t_ `"github.com/banbox/banstagy/code`"" + if ($mainText -notmatch [regex]::Escape('github.com/banbox/banstagy/code')) { + $mainText = [regex]::Replace($mainText, 'import \(\r?\n', "import (`r`n$importLine`r`n", 1) + Set-Content -LiteralPath $mainPath -Value $mainText -Encoding UTF8 + } + $formatLog = Join-Path $script:LogRoot 'gofmt-integrate.log' + $format = Invoke-LoggedCommand -FilePath 'gofmt' -Arguments @('-w', $registryPath, $mainPath) -LogPath $formatLog + if ($format.ExitCode -ne 0) { throw "gofmt failed; see $formatLog" } + Set-Completed 'integrate' + Write-Host "Generated registry and integrated blank import in main.go" +} + +function Invoke-Build { + $botFull = [IO.Path]::GetFullPath($BotPath) + $result = Invoke-LoggedCommand -FilePath 'go' -Arguments @('build', '-o', $botFull, '.') -LogPath (Join-Path $script:LogRoot 'build.log') + if ($result.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $botFull)) { + throw "Build failed; see $($result.LogPath)" + } + Set-Completed 'build' + Write-Host "Build succeeded: $botFull" +} + +function Remove-YamlTopLevelBlock { + param([string[]]$Lines, [Parameter(Mandatory)][string]$Name) + $result = New-Object Collections.Generic.List[string] + $skipping = $false + foreach ($line in $Lines) { + if ($line -match ('^' + [regex]::Escape($Name) + '\s*:')) { + $skipping = $true + continue + } + if ($skipping -and $line -match '^\S') { $skipping = $false } + if (-not $skipping) { $result.Add($line) } + } + return @($result) +} + +function Set-YamlTopLevelScalar { + param([string[]]$Lines, [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Value) + $filtered = @(Remove-YamlTopLevelBlock -Lines $Lines -Name $Name) + return @($filtered) + @("${Name}: $Value") +} + +function Get-LegacyUniverseFromLog { + param([Parameter(Mandatory)]$Report) + $spec = $Report.dynamicUniverse + $logPath = Join-Path (Get-ReportRoot $Report) ([string]$spec.sourceLog) + $matches = New-Object Collections.Generic.List[object] + foreach ($line in Get-Content -LiteralPath $logPath) { + if ($line -match 'USDT\((?\d+)\):\s+(?[A-Z0-9]+(?:\s+[A-Z0-9]+)*)\s*$') { + $bases = @($Matches.bases -split '\s+' | Where-Object { $_ }) + if ($bases.Count -eq [int]$Matches.count) { + $matches.Add([pscustomobject]@{ Count = $bases.Count; Bases = $bases }) + } + } + } + if ($spec.psobject.Properties['expectedWarmupPairs']) { + $filtered = @($matches | Where-Object Count -eq [int]$spec.expectedWarmupPairs) + $matches = New-Object Collections.Generic.List[object] + foreach ($match in $filtered) { $matches.Add($match) } + } + $selected = @($matches | Sort-Object Count -Descending | Select-Object -First 1) + if ($selected.Count -gt 0) { + return @($selected[0].Bases | ForEach-Object { "$_/USDT:USDT" }) + } + $configPath = Join-Path (Get-ReportRoot $Report) 'config.yml' + foreach ($line in Get-Content -LiteralPath $configPath) { + if ($line -match '^pairs\s*:\s*\[(?[^]]+)\]') { + $pairs = @($Matches.pairs -split ',' | ForEach-Object { $_.Trim().Trim("'", '"') } | Where-Object { $_ }) + if ($pairs.Count -gt 0) { + return @($pairs | ForEach-Object { if ($_ -match '/') { $_ } else { "$_/USDT:USDT" } }) + } + } + } + throw "Could not recover the legacy universe from $logPath or top-level pairs in $configPath" +} + +function ConvertTo-SanitizedConfig { + param([Parameter(Mandatory)]$Report, [switch]$PinLegacyUniverse) + $sourcePath = Join-Path (Get-ReportRoot $Report) 'config.yml' + $lines = @(Get-Content -LiteralPath $sourcePath) + foreach ($block in @('api_server', 'rpc_channels', 'webhook', 'mail', 'llm_models')) { + $lines = @(Remove-YamlTopLevelBlock -Lines $lines -Name $block) + } + $sensitive = '(?i)^(?\s*)(?api_key|api_secret|secret|token|password|passwd|pwd|jwt_secret_key|corp_secret|private_key|url)\s*:' + $lines = @($lines | ForEach-Object { + if ($_ -match $sensitive) { "$($Matches.indent)$($Matches.key): `"`"" } else { $_ } + }) + foreach ($mapping in $Report.policyMap.psobject.Properties) { + $old = [regex]::Escape($mapping.Name) + $replacement = [string]$mapping.Value + $pattern = "^(?\s*(?:-\s*)?name:\s*)$old(?\s*(?:#.*)?)$" + $lines = @($lines | ForEach-Object { + if ($_ -match $pattern) { "$($Matches.prefix)$replacement$($Matches.suffix)" } else { $_ } + }) + } + $detail = Get-Content -LiteralPath (Join-Path (Get-ReportRoot $Report) 'detail.json') -Raw | ConvertFrom-Json + foreach ($field in @('timerange', 'time_start', 'time_end')) { + $lines = @(Remove-YamlTopLevelBlock -Lines $lines -Name $field) + } + $lines += @("time_start: '$([int64]$detail.startMS)'", "time_end: '$([int64]$detail.endMS)'") + if ($PinLegacyUniverse -and $Report.psobject.Properties['dynamicUniverse']) { + $pairs = @(Get-LegacyUniverseFromLog -Report $Report) + $lines = @(Remove-YamlTopLevelBlock -Lines $lines -Name 'pairs') + $lines = @(Remove-YamlTopLevelBlock -Lines $lines -Name 'pairlists') + $lines = @($lines | ForEach-Object { + if ($_ -match '^(?\s*)use_latest\s*:') { "$($Matches.indent)use_latest: false" } else { $_ } + }) + $quotedPairs = @($pairs | ForEach-Object { "'" + $_.Replace("'", "''") + "'" }) -join ', ' + $lines += "pairs: [$quotedPairs]" + } + $lines += @('', 'api_server:', ' enable: false', 'rpc_channels: {}', 'webhook: {}', 'llm_models: {}') + $configRoot = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.sanitizedConfigRoot)) + New-Item -ItemType Directory -Force -Path $configRoot | Out-Null + $destination = Join-Path $configRoot "$($Report.id).yml" + Set-Content -LiteralPath $destination -Value $lines -Encoding UTF8 + return $destination +} + +function Get-NestedYamlScalar { + param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$Section, [Parameter(Mandatory)][string]$Key) + $inside = $false + foreach ($line in Get-Content -LiteralPath $Path) { + if ($line -match ('^' + [regex]::Escape($Section) + '\s*:')) { $inside = $true; continue } + if ($inside -and $line -match '^\S') { break } + if ($inside -and $line -match ('^\s+' + [regex]::Escape($Key) + '\s*:\s*(?.*?)\s*$')) { + return $Matches.value.Trim('''', '"') + } + } + return '' +} + +function Resolve-DatabaseUrl { + if ($DatabaseUrl) { return $DatabaseUrl } + if ($DatabaseOverlay) { + $overlayFull = [IO.Path]::GetFullPath($DatabaseOverlay) + if (-not (Test-Path -LiteralPath $overlayFull -PathType Leaf)) { throw "Database overlay not found: $overlayFull" } + $value = Get-NestedYamlScalar -Path $overlayFull -Section 'database' -Key 'url' + if ($value) { return $value } + } + throw 'Database connection is required. Pass a database-only -DatabaseOverlay, -DatabaseUrl, or set BANBOT_DATABASE_URL.' +} + +function Get-DatabaseEndpoint { + param([Parameter(Mandatory)][string]$Url) + try { + $uri = [Uri]$Url + if ($uri.Host) { + $port = $uri.Port + if ($port -le 0) { $port = if ($DatabaseType -eq 'questdb') { 8812 } else { 5432 } } + return [pscustomobject]@{ Host = $uri.Host; Port = $port } + } + } catch { } + $hostMatch = [regex]::Match($Url, '(?:^|\s)host=(?[^\s]+)', 'IgnoreCase') + $portMatch = [regex]::Match($Url, '(?:^|\s)port=(?\d+)', 'IgnoreCase') + if ($hostMatch.Success) { + $port = if ($portMatch.Success) { [int]$portMatch.Groups['port'].Value } elseif ($DatabaseType -eq 'questdb') { 8812 } else { 5432 } + return [pscustomobject]@{ Host = $hostMatch.Groups['host'].Value; Port = $port } + } + throw 'Cannot determine database host/port from the supplied URL.' +} + +function Assert-DatabaseAvailable { + param([Parameter(Mandatory)][string]$Url) + $endpoint = Get-DatabaseEndpoint $Url + $client = New-Object Net.Sockets.TcpClient + try { + $task = $client.ConnectAsync($endpoint.Host, $endpoint.Port) + $available = $task.Wait(3000) -and $client.Connected + } catch { + $available = $false + } finally { + $client.Dispose() + } + if ($available) { + Write-Host "Database preflight passed at $($endpoint.Host):$($endpoint.Port)" + } else { + Write-Warning "Database is not currently reachable at $($endpoint.Host):$($endpoint.Port); banbot may auto-start the configured QuestDB service. The backtest exit and artifacts remain the hard check." + } + return $available +} + +function New-DatabaseRuntimeOverlay { + param([Parameter(Mandatory)][string]$Url) + if ($DatabaseOverlay) { + $overlayFull = [IO.Path]::GetFullPath($DatabaseOverlay) + $topKeys = @(Get-Content -LiteralPath $overlayFull | Where-Object { $_ -match '^\S[^#]*:' } | ForEach-Object { ($_ -split ':', 2)[0].Trim() } | Sort-Object -Unique) + if ($topKeys.Count -ne 1 -or $topKeys[0] -ne 'database') { + throw '-DatabaseOverlay must contain only the top-level database section; full local configs are rejected to prevent credential merging.' + } + return [pscustomobject]@{ Path = $overlayFull; Temporary = $false } + } + $quotedUrl = "'" + $Url.Replace("'", "''") + "'" + $lines = @('database:', " url: $quotedUrl") + if ($DatabaseType) { $lines += " db_type: $DatabaseType" } + $runtimePath = Join-Path ([IO.Path]::GetTempPath()) ("banstagy-$([Guid]::NewGuid().ToString('N')).yml") + Set-Content -LiteralPath $runtimePath -Value $lines -Encoding UTF8 + return [pscustomobject]@{ Path = $runtimePath; Temporary = $true } +} + +function Set-RecentWindow { + param([Parameter(Mandatory)][string]$ConfigPath, [Parameter(Mandatory)][string]$Destination) + $lines = @(Get-Content -LiteralPath $ConfigPath) + foreach ($field in @('timerange', 'time_start', 'time_end')) { + $lines = @(Remove-YamlTopLevelBlock -Lines $lines -Name $field) + } + $today = $script:RecentToday + $start = $today.AddYears(-2) + $lines += @("time_start: '$($start.ToString('yyyyMMdd'))'", "time_end: '$($today.ToString('yyyyMMdd'))'") + Set-Content -LiteralPath $Destination -Value $lines -Encoding UTF8 + return [pscustomobject]@{ Start = $start.ToString('yyyyMMdd'); End = $today.ToString('yyyyMMdd') } +} + +function Get-RequiredProperty { + param([Parameter(Mandatory)]$Object, [Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Path) + $property = $Object.psobject.Properties[$Name] + if ($null -eq $property) { throw "Required detail field missing: $Path.$Name" } + return $property.Value +} + +function Get-ComparableGroupTitle { + param( + [Parameter(Mandatory)]$Report, + [Parameter(Mandatory)][string]$GroupName, + [Parameter(Mandatory)][string]$Title, + [switch]$Expected + ) + if (-not $Expected -or $GroupName -notin @('enterGrps', 'exitGrps')) { return $Title } + foreach ($mapping in $Report.policyMap.psobject.Properties) { + $legacy = [string]$mapping.Name + if ($Title -eq $legacy) { return [string]$mapping.Value } + if ($Title.StartsWith("${legacy}:", [StringComparison]::Ordinal)) { + return ([string]$mapping.Value) + $Title.Substring($legacy.Length) + } + } + return $Title +} + +function Compare-Detail { + param( + [Parameter(Mandatory)]$Report, + [Parameter(Mandatory)][string]$ExpectedPath, + [Parameter(Mandatory)][string]$ActualPath + ) + $expected = Get-Content -LiteralPath $ExpectedPath -Raw | ConvertFrom-Json + $actual = Get-Content -LiteralPath $ActualPath -Raw | ConvertFrom-Json + $diffs = New-Object Collections.Generic.List[object] + $universeDiverged = $false + + foreach ($field in @($script:Manifest.comparison.integerFields)) { + $left = Get-RequiredProperty -Object $expected -Name $field -Path '$' + $right = Get-RequiredProperty -Object $actual -Name $field -Path '$' + if ([decimal]$left -ne [decimal]$right) { + $diffs.Add([pscustomobject]@{ path = "`$.$field"; kind = 'integer'; expected = $left; actual = $right; delta = ([decimal]$right - [decimal]$left) }) + } + } + foreach ($field in @($script:Manifest.comparison.floatFields)) { + $left = Get-RequiredProperty -Object $expected -Name $field -Path '$' + $right = Get-RequiredProperty -Object $actual -Name $field -Path '$' + $delta = [Math]::Abs([double]$right - [double]$left) + if ($delta -gt $FloatTolerance) { + $diffs.Add([pscustomobject]@{ path = "`$.$field"; kind = 'float'; expected = $left; actual = $right; delta = $delta }) + } + } + foreach ($groupName in @($script:Manifest.comparison.groups)) { + $leftProperty = $expected.psobject.Properties[$groupName] + $rightProperty = $actual.psobject.Properties[$groupName] + if ($null -eq $leftProperty -and $null -eq $rightProperty) { continue } + if ($null -eq $leftProperty -or $null -eq $rightProperty) { + $diffs.Add([pscustomobject]@{ path = "`$.$groupName"; kind = 'schema'; expected = ($null -ne $leftProperty); actual = ($null -ne $rightProperty); delta = $null }) + continue + } + $leftGroup = @($leftProperty.Value) + $rightGroup = @($rightProperty.Value) + $key = [string]$script:Manifest.comparison.groupKey + $leftMap = @{}; $rightMap = @{} + foreach ($row in $leftGroup) { + $title = [string](Get-RequiredProperty $row $key "`$.$groupName") + $title = Get-ComparableGroupTitle -Report $Report -GroupName $groupName -Title $title -Expected + $leftMap[$title] = $row + } + foreach ($row in $rightGroup) { $rightMap[[string](Get-RequiredProperty $row $key "`$.$groupName")] = $row } + foreach ($title in @($leftMap.Keys + $rightMap.Keys | Sort-Object -Unique)) { + if (-not $leftMap.ContainsKey($title) -or -not $rightMap.ContainsKey($title)) { + $diffs.Add([pscustomobject]@{ path = "`$.$groupName[$title]"; kind = 'group'; expected = $leftMap.ContainsKey($title); actual = $rightMap.ContainsKey($title); delta = $null }) + if ($groupName -eq 'pairGrps') { $universeDiverged = $true } + continue + } + foreach ($field in @($script:Manifest.comparison.groupIntegerFields)) { + $left = Get-RequiredProperty $leftMap[$title] $field "`$.$groupName[$title]" + $right = Get-RequiredProperty $rightMap[$title] $field "`$.$groupName[$title]" + if ([decimal]$left -ne [decimal]$right) { + $diffs.Add([pscustomobject]@{ path = "`$.$groupName[$title].$field"; kind = 'integer'; expected = $left; actual = $right; delta = ([decimal]$right - [decimal]$left) }) + } + } + foreach ($field in @($script:Manifest.comparison.groupFloatFields)) { + $left = Get-RequiredProperty $leftMap[$title] $field "`$.$groupName[$title]" + $right = Get-RequiredProperty $rightMap[$title] $field "`$.$groupName[$title]" + $delta = [Math]::Abs([double]$right - [double]$left) + if ($delta -gt $FloatTolerance) { + $diffs.Add([pscustomobject]@{ path = "`$.$groupName[$title].$field"; kind = 'float'; expected = $left; actual = $right; delta = $delta }) + } + } + } + } + return [pscustomobject]@{ matched = ($diffs.Count -eq 0); differenceCount = $diffs.Count; universeDiverged = $universeDiverged; differences = @($diffs | ForEach-Object { $_ }) } +} + +function Get-RedactedLogTail { + param([Parameter(Mandatory)][string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { return '' } + $text = (@(Get-Content -LiteralPath $Path -Tail 250) -join "`n") + $text = $text -replace '(?i)(postgres(?:ql)?://[^:/\s]+:)[^@\s]+@', '$1@' + $text = $text -replace '(?i)(api[_-]?key|api[_-]?secret|token|password|pwd|secret)\s*[:=]\s*\S+', '$1=' + return $text +} + +function Clear-DatabaseUrlInConfig { + param([Parameter(Mandatory)][string]$Path) + $lines = @(Get-Content -LiteralPath $Path | ForEach-Object { + if ($_ -match '^(?\s*)url\s*:') { "$($Matches.indent)url: `"`"" } else { $_ } + }) + Set-Content -LiteralPath $Path -Value $lines -Encoding UTF8 +} + +function Assert-YamlParseable { + param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$LogPath) + $helper = Join-Path $script:WorkRoot 'yamlcheck.go' + if (-not (Test-Path -LiteralPath $helper)) { + @' +package main + +import ( + "os" + "gopkg.in/yaml.v3" +) + +func main() { + data, err := os.ReadFile(os.Args[1]) + if err != nil { panic(err) } + var value any + if err = yaml.Unmarshal(data, &value); err != nil { panic(err) } +} +'@ | Set-Content -LiteralPath $helper -Encoding UTF8 + } + $result = Invoke-LoggedCommand -FilePath 'go' -Arguments @('run', $helper, $Path) -LogPath $LogPath -TimeoutSeconds 120 + if ($result.ExitCode -ne 0) { throw "Generated config is not parseable YAML; see $LogPath" } +} + +function Assert-BacktestArtifacts { + param([Parameter(Mandatory)][string]$OutputDir, [Parameter(Mandatory)][DateTime]$StartedUtc) + $paths = @{ + detail = Join-Path $OutputDir 'detail.json' + config = Join-Path $OutputDir 'config.yml' + outLog = Join-Path $OutputDir 'out.log' + } + foreach ($entry in $paths.GetEnumerator()) { + if (-not (Test-Path -LiteralPath $entry.Value -PathType Leaf)) { throw "Backtest did not produce $($entry.Key): $($entry.Value)" } + if ((Get-Item -LiteralPath $entry.Value).LastWriteTimeUtc -lt $StartedUtc.AddSeconds(-2)) { throw "Backtest artifact is stale: $($entry.Value)" } + } + Clear-DatabaseUrlInConfig -Path $paths.config + Assert-YamlParseable -Path $paths.config -LogPath (Join-Path $OutputDir 'yaml-parse.log') + try { $detail = Get-Content -LiteralPath $paths.detail -Raw | ConvertFrom-Json } + catch { throw "detail.json is not parseable: $($paths.detail): $($_.Exception.Message)" } + $expectedOut = [IO.Path]::GetFullPath($OutputDir).TrimEnd('\', '/') + $actualOut = [IO.Path]::GetFullPath([string](Get-RequiredProperty $detail 'outDir' '$')).TrimEnd('\', '/') + if (-not $actualOut.Equals($expectedOut, [StringComparison]::OrdinalIgnoreCase)) { + throw "detail.outDir mismatch: expected $expectedOut, got $actualOut" + } + $numericFields = @($script:Manifest.comparison.integerFields) + @($script:Manifest.comparison.floatFields) + foreach ($field in $numericFields) { + $value = [double](Get-RequiredProperty $detail $field '$') + if ([double]::IsNaN($value) -or [double]::IsInfinity($value)) { throw "Non-finite core metric in detail.json: $field" } + } + $calcDiff = $null + if ($detail.psobject.Properties['calcDiff']) { + $calcDiff = [double]$detail.calcDiff + if ([Math]::Abs($calcDiff) -gt $FloatTolerance) { Write-Warning "Backtest reports calcDiff=$calcDiff in $OutputDir" } + } + return [pscustomobject]@{ detail = $paths.detail; config = $paths.config; outLog = $paths.outLog; calcDiff = $calcDiff } +} + +function Invoke-Backtest { + param([Parameter(Mandatory)]$Report, [Parameter(Mandatory)][string]$SanitizedConfig, [Parameter(Mandatory)][string]$OutputDir, [Parameter(Mandatory)][string]$Label, [Parameter(Mandatory)][string]$DbUrl, [switch]$RequireOrders, [switch]$NetworkOff) + $runRoot = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.runRoot)) + Assert-DescendantPath -Root $runRoot -Path $OutputDir | Out-Null + New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null + $uniqueName = "$Label-$([DateTime]::UtcNow.ToString('yyyyMMddTHHmmssfffZ'))-$([Guid]::NewGuid().ToString('N').Substring(0, 8))" + $OutputDir = Join-Path $OutputDir $uniqueName + Assert-DescendantPath -Root $runRoot -Path $OutputDir | Out-Null + if (Test-Path -LiteralPath $OutputDir) { throw "Unique output already exists: $OutputDir" } + New-Item -ItemType Directory -Path $OutputDir | Out-Null + $runtimeOverlay = New-DatabaseRuntimeOverlay -Url $DbUrl + $startedUtc = [DateTime]::UtcNow + try { + $captureLog = Join-Path $OutputDir 'process.log' + $args = @('backtest', '-datadir', [string]$script:Manifest.dataDir, '-no-default', '-config', $SanitizedConfig, '-config', $runtimeOverlay.Path, '-out', $OutputDir) + if ($NetworkOff) { $args += '-net-off' } + $result = Invoke-LoggedCommand -FilePath ([IO.Path]::GetFullPath($BotPath)) -Arguments $args -LogPath $captureLog -TimeoutSeconds ($BacktestTimeoutMinutes * 60) + if ($result.ExitCode -ne 0) { throw "Backtest $Label failed for $($Report.id); see $captureLog and $(Join-Path $OutputDir 'out.log')" } + $artifacts = Assert-BacktestArtifacts -OutputDir $OutputDir -StartedUtc $startedUtc + if ($RequireOrders) { + foreach ($name in @('orders.csv', 'orders.gob')) { + if (-not (Test-Path -LiteralPath (Join-Path $OutputDir $name) -PathType Leaf)) { throw "Recent backtest did not produce required $name in $OutputDir" } + } + } + return [pscustomobject]@{ output = $OutputDir; detail = $artifacts.detail; botLog = $artifacts.outLog; processLog = $captureLog; startedUtc = $startedUtc.ToString('o'); exitCode = $result.ExitCode; calcDiff = $artifacts.calcDiff } + } finally { + $emittedConfig = Join-Path $OutputDir 'config.yml' + if (Test-Path -LiteralPath $emittedConfig -PathType Leaf) { Clear-DatabaseUrlInConfig -Path $emittedConfig } + if ($runtimeOverlay.Temporary -and (Test-Path -LiteralPath $runtimeOverlay.Path)) { Remove-Item -LiteralPath $runtimeOverlay.Path -Force } + } +} + +function Invoke-CodexRepair { + param([Parameter(Mandatory)]$Report, [Parameter(Mandatory)]$Comparison, [Parameter(Mandatory)][string]$BotLog) + $scratch = Join-Path $script:WorkRoot "repair-$($Report.id)" + Copy-MigrationInputs -Report $Report -Scratch $scratch + $generated = Join-Path (Join-Path $script:RepoRoot $script:Manifest.generatedRoot) $Report.id + Copy-Item -LiteralPath $generated -Destination (Join-Path $scratch 'current') -Recurse + $diffJson = $Comparison | ConvertTo-Json -Depth 20 + $logTail = Get-RedactedLogTail -Path $BotLog + $blocks = New-Object Collections.Generic.List[string] + $blocks.Add((Get-InlineFileBlock -Label 'banbot_cn.md' -Path (Join-Path $scratch 'input\banbot_cn.md'))) + foreach ($relative in @($Report.sourceFiles)) { + $blocks.Add((Get-InlineFileBlock -Label "legacy/$($relative.Replace('\', '/'))" -Path (Join-Path (Join-Path $scratch 'input\sources') $relative))) + } + foreach ($file in @(Get-ChildItem -LiteralPath (Join-Path $scratch 'current') -File -Filter '*.go' -Recurse)) { + $relative = Get-RelativePath -Base (Join-Path $scratch 'current') -Path $file.FullName + $blocks.Add((Get-InlineFileBlock -Label "current/$($relative.Replace('\', '/'))" -Path $file.FullName)) + } + $inlineInputs = $blocks -join "`n`n" + $basePrompt = @" +Do not call tools or inspect the filesystem. All allowed inputs are included below. +Return only JSON matching the supplied schema, containing the full replacement Go package. Each path must be relative and end in .go. +Repair the migrated banbot strategy so its backtest reproduces the legacy metrics. +Preserve group $($Report.generatedGroup) and these mappings: +$(Get-PolicyInstructions $Report) + +Diagnose OnData main-vs-info routing, warmup, timeframe, order timing, and state behavior. Make the smallest behavior-preserving fix. Do not weaken comparisons or alter metrics. Do not add dependencies. Do not include markdown fences or explanations. + +METRIC DIFFERENCES: +$diffJson + +REDACTED LOG TAIL: +$logTail + +$inlineInputs +"@ + $schemaPath = Join-Path $scratch 'files-schema.json' + $replacement = Join-Path $scratch 'replacement' + Write-CodexFilesSchema -Path $schemaPath + $reasoningConfig = 'model_reasoning_effort="' + $CodexReasoning + '"' + $validationFeedback = '' + $validated = $false + for ($validationAttempt = 0; $validationAttempt -le $MaxCodexValidationAttempts; $validationAttempt++) { + $prompt = $basePrompt + if ($validationFeedback) { + $prompt += @" + +PREVIOUS RESPONSE VALIDATION ERROR: +$validationFeedback + +Return a corrected full replacement package. Deprecated OnBar and OnInfoBar callbacks must not appear as struct fields. +"@ + } + $attemptStamp = [DateTime]::UtcNow.ToString('yyyyMMddHHmmssfff') + $logPath = Join-Path $script:LogRoot "codex-repair-$($Report.id)-$attemptStamp-attempt$validationAttempt.log" + $responsePath = Join-Path $scratch "files-response-$validationAttempt.json" + try { + $result = Invoke-LoggedCommand -FilePath 'codex' -Arguments @('exec', '--disable', 'hooks', '--disable', 'multi_agent', '-c', $reasoningConfig, '--sandbox', 'read-only', '--ephemeral', '--skip-git-repo-check', '--output-schema', $schemaPath, '--output-last-message', $responsePath, '-C', $scratch, '-') -LogPath $logPath -InputText $prompt -WorkingDirectory $scratch + if ($result.ExitCode -ne 0) { throw "Codex repair command failed; see $logPath" } + if (Test-Path -LiteralPath $replacement) { Remove-Item -LiteralPath $replacement -Recurse -Force } + Write-CodexResponseFiles -ResponsePath $responsePath -Destination $replacement + $formatArgs = @('-w') + @(Get-ChildItem -LiteralPath $replacement -File -Filter '*.go' -Recurse | Select-Object -ExpandProperty FullName) + $formatLog = Join-Path $script:LogRoot "gofmt-repair-$($Report.id)-attempt$validationAttempt.log" + $format = Invoke-LoggedCommand -FilePath 'gofmt' -Arguments $formatArgs -LogPath $formatLog + if ($format.ExitCode -ne 0) { throw "gofmt repair failed; see $formatLog" } + Assert-GeneratedPackage -Report $Report -Directory $replacement + $validated = $true + break + } catch { + $validationFeedback = $_.Exception.Message + if ($validationAttempt -ge $MaxCodexValidationAttempts) { + throw "Codex repair validation failed for $($Report.id) after $($validationAttempt + 1) attempts: $validationFeedback" + } + Write-Warning "Retrying Codex repair for $($Report.id): $validationFeedback" + } + } + if (-not $validated) { throw "Codex repair did not produce a validated package for $($Report.id)" } + Assert-DescendantPath -Root (Join-Path $script:RepoRoot $script:Manifest.generatedRoot) -Path $generated | Out-Null + Remove-Item -LiteralPath $generated -Recurse -Force + Copy-Item -LiteralPath $replacement -Destination $generated -Recurse + Invoke-Build +} + +function Read-Summary { + if (Test-Path -LiteralPath $script:SummaryPath) { + return Get-Content -LiteralPath $script:SummaryPath -Raw | ConvertFrom-Json + } + return [pscustomobject]@{ + schemaVersion = 1 + generatedUtc = [DateTime]::UtcNow.ToString('o') + floatTolerance = $FloatTolerance + recentWindow = $null + reports = @() + } +} + +function Save-ReportSummary { + param([Parameter(Mandatory)]$ReportResult, $RecentWindow = $null) + $summary = Read-Summary + $summary.generatedUtc = [DateTime]::UtcNow.ToString('o') + $summary.floatTolerance = $FloatTolerance + if ($null -ne $RecentWindow) { $summary.recentWindow = $RecentWindow } + $summary.reports = @(@($summary.reports | Where-Object id -ne $ReportResult.id) + $ReportResult | Sort-Object id) + Write-JsonAtomic -Value $summary -Path $script:SummaryPath +} + +function Invoke-Replay { + $dbUrl = Resolve-DatabaseUrl + $null = Assert-DatabaseAvailable -Url $dbUrl + $failures = New-Object Collections.Generic.List[string] + foreach ($report in $script:Representatives) { + if (-not $Force -and (Test-Completed "replay:$($report.id)")) { + Write-Host "Resume: baseline replay already matched for $($report.id)" + continue + } + $sanitized = ConvertTo-SanitizedConfig -Report $report -PinLegacyUniverse + $attempts = New-Object Collections.Generic.List[object] + $comparison = $null + $exactReplayBlocker = $null + for ($attempt = 0; $attempt -le $MaxRepairAttempts; $attempt++) { + $output = Join-Path (Join-Path (Join-Path $script:RepoRoot $script:Manifest.runRoot) $report.id) 'baseline' + $run = Invoke-Backtest -Report $report -SanitizedConfig $sanitized -OutputDir $output -Label "baseline-$attempt" -DbUrl $dbUrl -NetworkOff + $comparison = Compare-Detail -Report $report -ExpectedPath (Join-Path (Get-ReportRoot $report) 'detail.json') -ActualPath $run.detail + $attempts.Add([pscustomobject]@{ attempt = $attempt; startedUtc = $run.startedUtc; exitCode = $run.exitCode; matched = $comparison.matched; differenceCount = $comparison.differenceCount; calcDiffWarning = $run.calcDiff; output = $run.output; differences = @($comparison.differences) }) + if ($comparison.matched) { break } + $eventCountDiffs = @($comparison.differences | Where-Object { $_.path -in @('$.barNum', '$.timeNum') }) + if ($eventCountDiffs.Count -gt 0) { + $exactReplayBlocker = "Archived event counts differ from the current banbot/data environment ($(@($eventCountDiffs | ForEach-Object { $_.path + ': ' + $_.expected + ' -> ' + $_.actual }) -join ', ')); strategy repair is disabled." + break + } + if ($comparison.universeDiverged -and $Report.psobject.Properties['dynamicUniverse']) { + $exactReplayBlocker = "The title-keyed pair set diverged for dynamic $($report.dynamicUniverse.source) after pinning the universe from $($report.dynamicUniverse.sourceLog); strategy repair is not appropriate." + break + } + if ($attempt -lt $MaxRepairAttempts) { Invoke-CodexRepair -Report $report -Comparison $comparison -BotLog $run.botLog } + } + $baselineDetail = Get-Content -LiteralPath (Join-Path (Get-ReportRoot $report) 'detail.json') -Raw | ConvertFrom-Json + $universePin = if ($Report.psobject.Properties['dynamicUniverse']) { $Report.dynamicUniverse } else { $null } + $result = [pscustomobject]@{ + id = $report.id + baselineMatched = $comparison.matched + baselineWindowOverride = [pscustomobject]@{ startMS = [int64]$baselineDetail.startMS; endMS = [int64]$baselineDetail.endMS } + dynamicUniverse = $universePin + exactReplayBlocker = $exactReplayBlocker + baselineAttempts = @($attempts | ForEach-Object { $_ }) + recent = $null + } + Save-ReportSummary -ReportResult $result + if (-not $comparison.matched) { + if ($exactReplayBlocker) { + Set-Completed "replay:$($report.id)" + Set-Completed "replay-blocked:$($report.id)" + Write-Warning "Baseline evaluated with an environment blocker for $($report.id): $exactReplayBlocker" + continue + } + $failures.Add($report.id) + Write-Warning "Baseline did not match for $($report.id); see $script:SummaryPath" + continue + } + Set-Completed "replay:$($report.id)" + Write-Host "Baseline matched: $($report.id)" + } + if ($failures.Count -gt 0) { throw "Baseline replay incomplete for: $($failures -join ', '). See $script:SummaryPath" } +} + +function Remove-RecentOrderArtifacts { + param([Parameter(Mandatory)][string]$OutputDir) + $recentRoot = [IO.Path]::GetFullPath((Join-Path $script:RepoRoot $script:Manifest.runRoot)) + Assert-NoReparsePath -Root $recentRoot -Path $OutputDir | Out-Null + foreach ($name in @('orders.csv', 'orders.gob')) { + $path = Join-Path $OutputDir $name + Assert-NoReparsePath -Root $OutputDir -Path $path | Out-Null + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Expected recent artifact before cleanup: $path" } + Remove-Item -LiteralPath $path -Force + if (Test-Path -LiteralPath $path) { throw "Failed to delete recent artifact: $path" } + } +} + +function Invoke-Recent { + $dbUrl = Resolve-DatabaseUrl + $null = Assert-DatabaseAvailable -Url $dbUrl + foreach ($report in $script:Representatives) { + if (-not (Test-Completed "replay:$($report.id)")) { throw "Recent run requires a matched baseline for $($report.id)" } + if (-not $Force -and (Test-Completed "recent:$($report.id)")) { + Write-Host "Resume: recent replay already complete for $($report.id)" + continue + } + $sanitized = ConvertTo-SanitizedConfig -Report $report + $recentConfig = Join-Path ([IO.Path]::GetTempPath()) ("banstagy-recent-$([Guid]::NewGuid().ToString('N')).yml") + try { + $window = Set-RecentWindow -ConfigPath $sanitized -Destination $recentConfig + $output = Join-Path (Join-Path (Join-Path $script:RepoRoot $script:Manifest.runRoot) $report.id) 'recent' + $run = Invoke-Backtest -Report $report -SanitizedConfig $recentConfig -OutputDir $output -Label 'recent' -DbUrl $dbUrl -RequireOrders + $recentDetail = Get-Content -LiteralPath $run.detail -Raw | ConvertFrom-Json + $startUtc = [DateTime]::SpecifyKind($script:RecentToday.AddYears(-2), [DateTimeKind]::Utc) + $endUtc = [DateTime]::SpecifyKind($script:RecentToday, [DateTimeKind]::Utc) + $startMS = ([DateTimeOffset]$startUtc).ToUnixTimeMilliseconds() + $endMS = ([DateTimeOffset]$endUtc).ToUnixTimeMilliseconds() + if ([int64]$recentDetail.startMS -ne $startMS -or [int64]$recentDetail.endMS -ne $endMS) { + throw "Recent detail range does not exactly match UTC [$startMS,$endMS): actual $($recentDetail.startMS)-$($recentDetail.endMS)" + } + Remove-RecentOrderArtifacts -OutputDir $run.output + $summary = Read-Summary + $existing = @($summary.reports | Where-Object id -eq $report.id | Select-Object -First 1) + if ($existing.Count -eq 0) { throw "Missing baseline summary for $($report.id)" } + $existing[0].recent = [pscustomobject]@{ output = $run.output; detail = $run.detail; startedUtc = $run.startedUtc; exitCode = $run.exitCode; calcDiffWarning = $run.calcDiff; start = $window.Start; endExclusive = $window.End; ordersRemoved = $true } + Save-ReportSummary -ReportResult $existing[0] -RecentWindow ([pscustomobject]@{ start = $window.Start; endExclusive = $window.End; timezone = 'UTC' }) + Set-Completed "recent:$($report.id)" + Write-Host "Recent two-year replay complete: $($report.id)" + } finally { + if (Test-Path -LiteralPath $recentConfig) { Remove-Item -LiteralPath $recentConfig -Force } + } + } +} + +New-Item -ItemType Directory -Force -Path $script:WorkRoot, $script:LogRoot | Out-Null +$script:Representatives = @(Resolve-Representatives) +foreach ($report in $script:Representatives) { Assert-RepresentativeInputs $report } + +switch ($Phase) { + 'Inventory' { Invoke-Inventory } + 'Migrate' { Invoke-Migrate } + 'Integrate' { Invoke-Integrate } + 'Build' { Invoke-Build } + 'Replay' { Invoke-Replay } + 'Recent' { Invoke-Recent } + 'All' { + Invoke-Inventory + Invoke-Migrate + Invoke-Integrate + Invoke-Build + Invoke-Replay + Invoke-Recent + } +} diff --git a/surge/alldown.go b/surge/alldown.go new file mode 100644 index 0000000..d81dfd6 --- /dev/null +++ b/surge/alldown.go @@ -0,0 +1,157 @@ +package surge + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "sort" +) + +/* +全市场瞬时暴跌做空策略。适用于十分钟跌去70%的情况。 +可使用下面脚本回测:surge/down_test.go +E:\trade\go\bandata\backtest\bdb35c1d91\surge_alldown.log +*/ + +type AllDownSta struct { + volBase float64 // 平均成交量 + dirtRate float64 // K线方向倍率,范围约:[-3,3] + barRate float64 // K线比率 + volRate float64 // 成交量倍率 +} + +func AllDown(p *config.RunPolicyConfig) *strat.TradeStrat { + //var allowLog = false + + lenAtr, lenAtrS := 20, 30 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + baseAtrLenS := baseAtrLen * 3 / 2 + + env, _ := ta.NewBarEnv("", "", "", "1m") + lenS := env.NewSeries(nil) + volS := env.NewSeries(nil) + + var prevLen float64 + var volThres = 2.8 + var openDirt float64 + + return &strat.TradeStrat{ + WarmupNum: 150, + BatchInOut: true, + TimeFrames: "1m", + OnStartUp: func(s *strat.StratJob) { + s.More = &AllDownSta{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "15m", 100}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*AllDownSta) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtrS) + atrBase := ta.Lowest(ta.Highest(atr, lenAtrS), baseAtrLenS).Get(0) + o, c := e.Open.Get(0), e.Close.Get(0) + h, l := e.High.Get(0), e.Low.Get(0) + m.dirtRate = (c - o) / atrBase + m.barRate = (c - o) / (h - l) + m.volRate = ta.EMA(e.Volume, lenAtr).Get(0) / m.volBase + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*AllDownSta) + maVol := ta.EMA(e.Volume, lenAtr) + m.volBase = ta.Lowest(ta.Highest(maVol, lenAtr), baseAtrLen).Get(0) / 3 + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + jobNum := len(jobs) + volRates := make([]float64, 0, jobNum) + lenRates := make([]float64, 0, jobNum) + solidRates := make([]float64, 0, jobNum) + volIds := make([]*core.Int64Flt, 0, jobNum) + var timeMS int64 + for i, j := range jobs { + m, _ := j.More.(*AllDownSta) + volRates = append(volRates, m.volRate) + lenRates = append(lenRates, m.dirtRate) + solidRates = append(solidRates, m.barRate) + timeMS = max(timeMS, j.Env.TimeStop) + volIds = append(volIds, &core.Int64Flt{ + Int: int64(i), + Val: m.volRate, + }) + } + sort.Float64s(volRates) + sort.Float64s(lenRates) + sort.Float64s(solidRates) + vol := volRates[jobNum/2] + solid := solidRates[jobNum/2] + lenMid := lenRates[jobNum/2] + //len33 := lenRates[jobNum/3] + len66 := lenRates[jobNum*2/3] + + val := lenMid + vol + env.TimeStop = max(timeMS, env.TimeStop) + if math.IsNaN(val) || math.IsInf(val, 0) || lenS.Cached() { + return + } + lenS.Append(lenMid) + volS.Append(vol) + dataLen := volS.Len() + if dataLen > env.MaxCache*2 { + volS.Cut(env.MaxCache) + lenS.Cut(env.MaxCache) + } + volMa1 := ta.SMA(volS, 3).Get(1) + low5len := ta.Lowest(lenS, 30).Get(1) + if openDirt >= -1 && vol > volThres && lenMid < low5len && len66 < 0 && solid < -0.5 && prevLen < 0 { + // 成交量突破阈值,全市场大阴线,前一个也是阴线 + prevRate := lenMid / prevLen + if prevRate < 3 && prevRate > 1.5 && vol/volMa1 < 2 { + // 与前一个k线比率不超过3,成交量缓慢增长 + openDirt -= 1 + costRate := 2.0 + if vol < 4 { + costRate *= vol / 4 + } + sort.Slice(volIds, func(i, j int) bool { + return volIds[i].Val < volIds[j].Val + }) + // 做空成交量最大的1/3部分,获利最丰(收益45%, 55%, 66%) + items := volIds[jobNum*2/3:] + for _, it := range items { + jobs[it.Int].OpenOrder(&strat.EnterReq{ + Tag: "short", + CostRate: costRate, + Short: true, + }) + } + } + } + if openDirt < 0 { + // 正在做空/多 + if lenMid*openDirt < 0 { + openDirt = 0 + for _, job := range jobs { + job.CloseOrders(&strat.ExitReq{ + Tag: "exit", + Force: true, + }) + } + } + } + prevLen = lenMid + //if !allowLog && vol > 2.5 { + // allowLog = true + //} + //if !math.IsNaN(vol) { + // jobs[0].Strat.WriteOutput(fmt.Sprintf("%.2f %.2f %.2f %.2f %.2f", vol, solid, lenMid, len33, len66), true) + // if vol < 1.4 { + // allowLog = false + // } + //} + }, + } +} diff --git a/surge/common.go b/surge/common.go new file mode 100644 index 0000000..e7d03fb --- /dev/null +++ b/surge/common.go @@ -0,0 +1,10 @@ +package surge + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("surge", map[string]strat.FuncMakeStrat{ + "maxVol": goMaxVol, + "alldown": AllDown, + }) +} diff --git a/surge/down_test.go b/surge/down_test.go new file mode 100644 index 0000000..818b27c --- /dev/null +++ b/surge/down_test.go @@ -0,0 +1,108 @@ +package surge + +import ( + "bufio" + "fmt" + "github.com/banbox/banbot/btime" + "github.com/banbox/banbot/core" + ta "github.com/banbox/banta" + "math" + "os" + "strconv" + "strings" + "testing" + "time" +) + +/* +测试顺势暴跌做空。应对2025-10-10 +*/ + +func TestDownQwen(t *testing.T) { + stamps, data, err := ParseFile("E:\\trade\\go\\bandata\\backtest\\bdb35c1d91\\surge_alldown.log") + if err != nil { + panic(err) + } + + env, _ := ta.NewBarEnv("", "", "", "1m") + lenS := env.NewSeries(nil) + volS := env.NewSeries(nil) + + var prevLen float64 + var volThres = 2.8 + var holdBars int + var holdAmt, openDirt float64 + for i, row := range data { + timeMS := stamps[i] + vol, solid, lenMid, _, len66 := row[0], row[1], row[2], row[3], row[4] + if math.IsNaN(lenMid) { + continue + } + env.TimeStop = timeMS + lenS.Append(lenMid) + volS.Append(vol) + volMa1 := ta.SMA(volS, 3).Get(1) + low5len := ta.Lowest(lenS, 30).Get(1) + if openDirt != 0 { + // 正在做空/多 + holdAmt += lenMid + holdBars += 1 + if lenMid*openDirt < 0 { + dateStr := btime.ToDateStr(timeMS, core.DefaultDateFmt) + fmt.Printf("%s close, hold %d total: %.2f\n", dateStr, holdBars, holdAmt*openDirt) + openDirt, holdBars = 0, 0 + holdAmt = 0 + } + } else if vol > volThres && lenMid < low5len && len66 < 0 && solid < -0.5 && prevLen < 0 { + // 成交量突破阈值,中位数阴线,3个len全部阴线,中位数创新低 + // 与前一个k线比率不超过3 + prevRate := lenMid / prevLen + if prevRate < 3 && prevRate > 1.5 && vol/volMa1 < 2 { + openDirt = -1 + fmt.Printf("%s short, thres: %.2f vol: %.2f len: %.2f solid: %.2f\n", + btime.ToDateStr(timeMS, core.DefaultDateFmt), volThres, vol, lenMid, solid) + } + } + prevLen = lenMid + } +} + +func ParseFile(path string) ([]int64, [][5]float64, error) { + f, err := os.Open(path) + if err != nil { + return nil, nil, err + } + defer f.Close() + + var ts []int64 + var vals [][5]float64 + scanner := bufio.NewScanner(f) + for scanner.Scan() { + t, v := ParseLine(scanner.Text()) + if t != 0 { + ts = append(ts, t) + vals = append(vals, v) + } + } + return ts, vals, scanner.Err() +} + +func ParseLine(s string) (int64, [5]float64) { + var arr [5]float64 + f := strings.Fields(s) + if len(f) < 7 { + return 0, arr + } + t, err := time.ParseInLocation("2006-01-02 15:04:05", f[0]+" "+f[1], time.UTC) + if err != nil { + return 0, arr + } + for i := 0; i < 5; i++ { + v, err := strconv.ParseFloat(f[2+i], 64) + if err != nil { + return t.UnixMilli(), arr + } + arr[i] = v + } + return t.UnixMilli(), arr +} diff --git a/surge/max_vol.go b/surge/max_vol.go new file mode 100644 index 0000000..d95d199 --- /dev/null +++ b/surge/max_vol.go @@ -0,0 +1,43 @@ +package surge + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +type volSta struct { + rate float64 +} + +func goMaxVol(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + BatchInOut: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &volSta{} + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + more, _ := s.More.(*volSta) + bigVolMa := ta.SMA(e.Volume, 100).Get(0) + more.rate = e.Volume.Get(0)/bigVolMa - 1 + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + var maxIn, maxOut *strat.StratJob + var inVal, outVal float64 + for _, j := range jobs { + sta, _ := j.More.(*volSta) + if sta.rate < outVal { + outVal = sta.rate + maxOut = j + } else if sta.rate > inVal { + inVal = sta.rate + maxIn = j + } + } + fmt.Printf("%v %v", maxIn, maxOut) + }, + } +} diff --git a/trend/20250208/MACDEMAV2.go b/trend/20250208/MACDEMAV2.go new file mode 100644 index 0000000..1008ab7 --- /dev/null +++ b/trend/20250208/MACDEMAV2.go @@ -0,0 +1,113 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +备份于2025/02/25 +测试加入1d的大周期过滤,对做多或做空都未获得更好效果,反而略差 + +20210101-20250101 +只做多:1776%,回撤28.8%,夏普3.93 +多空双开:1913%,回撤22%,夏普4.53 +做空部分单独运行波动很大,但和做多一起运行可减少做多的回撤 + +参考SMACross1加入海龟资金和ErDelta过滤后: +170品种下:夏普从7.97提升到8.91,显示最大回撤从18.71%下降到17.65%;收益从718%变为707% +*/ +type entExitMore struct { + enter *strat.EnterReq + exit *strat.ExitReq +} + +func MACDEMAV2(p *config.RunPolicyConfig) *strat.TradeStrat { + // 初始HMA 25 4 + maType := com.DefMaType(p, "ma", com.MaHMA) // hma 包揽优化列表头部 + maLen := p.DefInt("maLen", 25, core.PNormF(7, 100, 25, 0)) // 超过40表现不佳,20多表现最好 + maRate := p.Def("maRate", 4.1, core.PNorm(1.5, 7)) + emaLen := p.DefInt("emaLen", 100, core.PNormF(30, 240, 100, 0)) + macdRt := p.Def("macdRt", 1, core.PNormF(0.7, 3, 1.2, 0)) + maBigLen := int(math.Round(float64(maLen) * maRate)) + mc1, mc2, mc3 := int(math.Round(12*macdRt)), int(math.Round(26*macdRt)), int(math.Round(9*macdRt)) + p.KeepHyperOnly("ma", "maLen", "maRate") + return &strat.TradeStrat{ + WarmupNum: 500, + EachMaxLong: 1, + EachMaxShort: 1, + //OnStartUp: func(s *strat.StratJob) { + // s.More = &entExitMore{} + //}, + //OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + // return []*strat.PairSub{ + // {"_cur_", "4h", 500}, + // } + //}, + //OnBar: func(s *strat.StratJob) { + // m, _ := s.More.(*entExitMore) + // if m.enter != nil { + // s.OpenOrder(m.enter) + // m.enter = nil + // } + // if m.exit != nil { + // s.CloseOrders(m.exit) + // m.exit = nil + // } + //}, + //OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // m, _ := s.More.(*entExitMore) + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } else { + openRate = 0 + } + + ema := ta.EMA(e.Close, emaLen).Get(0) + macd, signal := ta.MACD(e.Close, mc1, mc2, mc3) + macdSub := macd.Get(0) - signal.Get(0) + + if c > ema && macdSub > 0 && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + CostRate: openRate, + }) + } else if len(s.LongOrders) > 0 && (macdSub < 0 && c < ema) { + s.CloseOrders(&strat.ExitReq{ + Tag: "closeL", + Dirt: core.OdDirtLong, + }) + } + + ma1 := com.GetMA(e.Close, e.Volume, maType, maLen) + ma2 := com.GetMA(e.Close, e.Volume, maType, maBigLen) + cross := ma1.Cross(ma2) + if cross == 1 && len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "closeS", + Dirt: core.OdDirtShort, + }) + } else if cross == -1 && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + CostRate: openRate, + }) + } + }, + } +} diff --git a/trend/20250208/SMACrossV1.go b/trend/20250208/SMACrossV1.go new file mode 100644 index 0000000..b4bc489 --- /dev/null +++ b/trend/20250208/SMACrossV1.go @@ -0,0 +1,107 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +自研均线趋势策略,多空都开。适合1h +在15m上,需把均线周期乘以4 +尝试加止损、止盈、限价入场,都会导致整体收益减少 +48个品种,21年到24年11月1日,累计收益1560%,最大回撤25%,夏普5.8 + +总收益93%来自多头 +但加入空头可使夏普从4.8提高到5.8 +多空的参数独立,比同一套参数,收益更稳定些 + +基于连续信号的商品期货交易策略 +https://mp.weixin.qq.com/s/6MS94OhGYhtjBQ1Ufa5Rag +下面基于170固定品种: +使用1.5%*Close/atr作为开仓倍数,夏普从8.12提升到9.63 +增加ErDelta过滤,夏普从9.63提升到11.03;最大回撤从21.8%降低到20.8% + +有效信号权重测试没有明显改善: +OnStartUp: func(s *strat.StratJob) { + s.More = []float64{0.5, 0.5} // long, short +}, +probs := s.More.([]float64) +if x1 == 1{ + newShort := probs[1] * 0.5 + s.More = []float64{1 - newShort, newShort} + sigDelta := math.Abs(1 - newShort*2) + if sigDelta > 0.2 { + openRate *= (1 - newShort) * 1.7 + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate}) + } +} +if x2 == -1{ + newLong := probs[0] * 0.5 + s.More = []float64{newLong, 1 - newLong} + sigDelta := math.Abs(1 - newLong*2) + if sigDelta > 0.2 { + openRate *= (1 - newLong) * 1.7 + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate}) + } +} +*/ + +func SMACrossV1(p *config.RunPolicyConfig) *strat.TradeStrat { + // 初始:两个SMA 做空:SMA 15 8 + maLType := com.DefMaType(p, "maL", com.MaSMA) + maSType := com.DefMaType(p, "maS", com.MaSMA) + maLLen := p.DefInt("maLLen", 20, core.PNormF(7, 70, 20, 0)) + maSLen := p.DefInt("maSLen", 15, core.PNormF(5, 50, 15, 0)) + maLRate := p.Def("maLRate", 9, core.PNorm(2, 15)) + maSRate := p.Def("maSRate", 8, core.PNorm(2, 14)) + maLBig := int(math.Round(float64(maLLen) * maLRate)) + maSBig := int(math.Round(float64(maSLen) * maSRate)) + p.KeepHyperOnly("maSLen", "maSRate") + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxShort: 1, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } else { + // 过滤掉ER下降的 + openRate = 0 + } + + // 方案一:多空SMA:略微平滑,收益略小一点 + // 方案二:多空VWMA:略微波动大点,收益略好一点 + price := ta.AvgPrice(e) + ma1 := com.GetMA(price, e.Volume, maLType, maLLen) + ma2 := com.GetMA(price, e.Volume, maLType, maLBig) + x1 := ma1.Cross(ma2) + if x1 == 1 && openRate > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate}) + } else if x1 == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "xlong", Dirt: core.OdDirtLong}) + } + + ma3 := com.GetMA(price, e.Volume, maSType, maSLen) + ma4 := com.GetMA(price, e.Volume, maSType, maSBig) + x2 := ma3.Cross(ma4) + if x2 == -1 && openRate > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate}) + } else if x2 == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "xshort", Dirt: core.OdDirtShort}) + } + }, + } +} diff --git a/trend/20250208/SuperFollow.go b/trend/20250208/SuperFollow.go new file mode 100644 index 0000000..e42c627 --- /dev/null +++ b/trend/20250208/SuperFollow.go @@ -0,0 +1,98 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "strconv" +) + +/* +版本1:备份:2025/02/23 + +原始灵感:https://www.fmz.com/strategy/442547 +已改动了大部分 + +测试在4h上表现最佳;210101-241101,48个品种 +收益1919%,回撤23%,夏普5.5 +添加adx过滤,收益增加 +测试使用ATRBase显著收益下降 +*/ +type supFlwMore struct { + enter *strat.EnterReq + exitIds []int64 +} + +func SuperFollow(p *config.RunPolicyConfig) *strat.TradeStrat { + atrLen := 14 // 默认12 + mult := p.Def("mult", 1.4, core.PNorm(0.7, 2.5)) + sRate := p.Def("sRate", 1.1, core.PNorm(0.7, 1.8)) + slAtr := p.Def("slAtr", 1.6, core.PNorm(1, 2.5)) + tpRate := p.Def("tpRate", 3, core.PNorm(1.7, 5.5)) + shortMult := -mult * sRate + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxShort: 1, + EachMaxLong: 1, + //OnStartUp: func(s *strat.StratJob) { + // s.More = &supFlwMore{} + //}, + //OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + // return []*strat.PairSub{ + // {"_cur_", "4h", 200}, + // } + //}, + //OnBar: func(s *strat.StratJob) { + // m, _ := s.More.(*supFlwMore) + // if m.enter != nil { + // s.OpenOrder(m.enter) + // m.enter = nil + // } else if len(m.exitIds) > 0 { + // for _, id := range m.exitIds { + // s.CloseOrders(&strat.ExitReq{Tag: "tooLong", OrderID: id}) + // } + // m.exitIds = nil + // } + //}, + //OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // m, _ := s.More.(*supFlwMore) + OnBar: func(s *strat.StratJob) { + e := s.Env + o, c := e.Open.Get(0), e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, atrLen).Get(0) + dist := (c - o) / atr + + adx := ta.ADX(e.High, e.Low, e.Close, 30).Get(0) + + openDirt := 0 + overRate := 0.0 + if len(s.ShortOrders) == 0 && dist < shortMult && adx < 20 { + openDirt = -1 + overRate = dist / shortMult + } else if len(s.LongOrders) == 0 && dist > mult { + openDirt = 1 + overRate = dist / mult + } + if openDirt != 0 { + costRate := overRate * 1.1 + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(openDirt), + Short: openDirt < 0, + TakeProfitVal: atr * slAtr * tpRate * overRate, + StopLossVal: atr * slAtr * overRate, + CostRate: costRate, + }) + } else { + // 长期未关闭的,超过200个平仓 + odList := s.GetOrders(core.OdDirtBoth) + for _, od := range odList { + holdNum := e.BarCount(od.EnterAt) + if holdNum > 200 { + s.CloseOrders(&strat.ExitReq{Tag: "tooLong", OrderID: od.ID}) + } + } + } + }, + } +} diff --git a/trend/20250208/main.go b/trend/20250208/main.go new file mode 100644 index 0000000..a2a6606 --- /dev/null +++ b/trend/20250208/main.go @@ -0,0 +1,11 @@ +package trend + +import "github.com/banbox/banbot/strat" + +func init() { + strat.AddStratGroup("20250208", map[string]strat.FuncMakeStrat{ + "m01": SuperFollow, + "m02": MACDEMAV2, + "m03": SMACrossV1, + }) +} diff --git a/trend/20250225/MACDEMAV2.go b/trend/20250225/MACDEMAV2.go new file mode 100644 index 0000000..a4407fe --- /dev/null +++ b/trend/20250225/MACDEMAV2.go @@ -0,0 +1,113 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +测试加入1d的大周期过滤,对做多或做空都未获得更好效果,反而略差 + +20210101-20250101 +只做多:1776%,回撤28.8%,夏普3.93 +多空双开:1913%,回撤22%,夏普4.53 +做空部分单独运行波动很大,但和做多一起运行可减少做多的回撤 + +参考SMACross1加入海龟资金和ErDelta过滤后: +170品种下:夏普从7.97提升到8.91,显示最大回撤从18.71%下降到17.65%;收益从718%变为707% + +2025/02/25 ERChg<1时保留ATR比率开单,整体*1.15 +*/ +type entExitMore struct { + enter *strat.EnterReq + exit *strat.ExitReq +} + +func MACDEMAV2(p *config.RunPolicyConfig) *strat.TradeStrat { + // 初始HMA 25 4 + maType := com.DefMaType(p, "ma", com.MaHMA) // hma 包揽优化列表头部 + maLen := p.DefInt("maLen", 25, core.PNormF(7, 100, 25, 0)) // 超过40表现不佳,20多表现最好 + maRate := p.Def("maRate", 4.1, core.PNorm(1.5, 7)) + emaLen := p.DefInt("emaLen", 100, core.PNormF(30, 240, 100, 0)) + macdRt := p.Def("macdRt", 1, core.PNormF(0.7, 3, 1.2, 0)) + maBigLen := int(math.Round(float64(maLen) * maRate)) + mc1, mc2, mc3 := int(math.Round(12*macdRt)), int(math.Round(26*macdRt)), int(math.Round(9*macdRt)) + p.KeepHyperOnly("ma", "maLen", "maRate") + return &strat.TradeStrat{ + WarmupNum: 500, + EachMaxLong: 1, + EachMaxShort: 1, + //OnStartUp: func(s *strat.StratJob) { + // s.More = &entExitMore{} + //}, + //OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + // return []*strat.PairSub{ + // {"_cur_", "4h", 500}, + // } + //}, + //OnBar: func(s *strat.StratJob) { + // m, _ := s.More.(*entExitMore) + // if m.enter != nil { + // s.OpenOrder(m.enter) + // m.enter = nil + // } + // if m.exit != nil { + // s.CloseOrders(m.exit) + // m.exit = nil + // } + //}, + //OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // m, _ := s.More.(*entExitMore) + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + openRate *= 1.15 + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } + + ema := ta.EMA(e.Close, emaLen).Get(0) + macdCols := ta.MACD(e.Close, mc1, mc2, mc3).Cols + macdSub := macdCols[0].Get(0) - macdCols[1].Get(0) + + if c > ema && macdSub > 0 && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + CostRate: openRate, + }) + } else if len(s.LongOrders) > 0 && (macdSub < 0 && c < ema) { + s.CloseOrders(&strat.ExitReq{ + Tag: "closeL", + Dirt: core.OdDirtLong, + }) + } + + ma1 := com.GetMA(e.Close, e.Volume, maType, maLen) + ma2 := com.GetMA(e.Close, e.Volume, maType, maBigLen) + cross := ma1.Cross(ma2) + if cross == 1 && len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "closeS", + Dirt: core.OdDirtShort, + }) + } else if cross == -1 && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + CostRate: openRate, + }) + } + }, + } +} diff --git a/trend/20250225/MACDX.go b/trend/20250225/MACDX.go new file mode 100644 index 0000000..2b1b099 --- /dev/null +++ b/trend/20250225/MACDX.go @@ -0,0 +1,91 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +视频号:山居听雨楼 2025/02/19 +“有没有一眼就能看清楚,而且很好用的交易方法。。。。。这里分享一个只用macd的交易方法。” + +【主要思想】 +大周期(1d)MACD快线和慢线都处于0轴上方看多,都处于0轴下方看空; +小周期(8h)MACD: + 看多时:快线上穿0轴或金叉,做多;死叉平仓。 + 看空时:快线下穿0轴或死叉,做空,死叉平仓。 + +测试小周期金叉死叉无需要求0轴上下,略好一点。 +测试小周期当前参数4h盈利最佳。大周期1d最佳。 +*/ + +func MACDX2(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 200, + RunTimeFrames: []string{"4h"}, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 100}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = 0 + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + dirt := s.More.(int) + macdCols := ta.MACD(e.Close, 26, 56, 20).Cols + macdX := macdCols[0].Cross(macdCols[1]) + macdX2 := macdCols[0].Cross(0) + + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * e.Close.Get(0) / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + openRate *= 1.3 + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } + if len(s.LongOrders) > 0 { + if macdX == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "closeL", Dirt: core.OdDirtLong}) + } + } else if dirt > 0 && openRate > 0 { + if macdX == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate}) + } else if macdX2 == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "long2", CostRate: openRate}) + } + } + if len(s.ShortOrders) > 0 { + if macdX == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "closeS", Dirt: core.OdDirtShort}) + } + } else if dirt < 0 && openRate > 0 { + if macdX == -1 { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate}) + } else if macdX2 == -1 { + s.OpenOrder(&strat.EnterReq{Tag: "short2", Short: true, CostRate: openRate}) + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + macdCols := ta.MACD(e.Close, 12, 26, 9).Cols + macd := macdCols[0].Get(0) + signal := macdCols[1].Get(0) + if macd > 0 && signal > 0 { + s.More = 1 + } else if macd < 0 && signal < 0 { + s.More = -1 + } else { + s.More = 0 + } + }, + } +} diff --git a/trend/20250225/SMACrossV1.go b/trend/20250225/SMACrossV1.go new file mode 100644 index 0000000..b4bc489 --- /dev/null +++ b/trend/20250225/SMACrossV1.go @@ -0,0 +1,107 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +自研均线趋势策略,多空都开。适合1h +在15m上,需把均线周期乘以4 +尝试加止损、止盈、限价入场,都会导致整体收益减少 +48个品种,21年到24年11月1日,累计收益1560%,最大回撤25%,夏普5.8 + +总收益93%来自多头 +但加入空头可使夏普从4.8提高到5.8 +多空的参数独立,比同一套参数,收益更稳定些 + +基于连续信号的商品期货交易策略 +https://mp.weixin.qq.com/s/6MS94OhGYhtjBQ1Ufa5Rag +下面基于170固定品种: +使用1.5%*Close/atr作为开仓倍数,夏普从8.12提升到9.63 +增加ErDelta过滤,夏普从9.63提升到11.03;最大回撤从21.8%降低到20.8% + +有效信号权重测试没有明显改善: +OnStartUp: func(s *strat.StratJob) { + s.More = []float64{0.5, 0.5} // long, short +}, +probs := s.More.([]float64) +if x1 == 1{ + newShort := probs[1] * 0.5 + s.More = []float64{1 - newShort, newShort} + sigDelta := math.Abs(1 - newShort*2) + if sigDelta > 0.2 { + openRate *= (1 - newShort) * 1.7 + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate}) + } +} +if x2 == -1{ + newLong := probs[0] * 0.5 + s.More = []float64{newLong, 1 - newLong} + sigDelta := math.Abs(1 - newLong*2) + if sigDelta > 0.2 { + openRate *= (1 - newLong) * 1.7 + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate}) + } +} +*/ + +func SMACrossV1(p *config.RunPolicyConfig) *strat.TradeStrat { + // 初始:两个SMA 做空:SMA 15 8 + maLType := com.DefMaType(p, "maL", com.MaSMA) + maSType := com.DefMaType(p, "maS", com.MaSMA) + maLLen := p.DefInt("maLLen", 20, core.PNormF(7, 70, 20, 0)) + maSLen := p.DefInt("maSLen", 15, core.PNormF(5, 50, 15, 0)) + maLRate := p.Def("maLRate", 9, core.PNorm(2, 15)) + maSRate := p.Def("maSRate", 8, core.PNorm(2, 14)) + maLBig := int(math.Round(float64(maLLen) * maLRate)) + maSBig := int(math.Round(float64(maSLen) * maSRate)) + p.KeepHyperOnly("maSLen", "maSRate") + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxShort: 1, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } else { + // 过滤掉ER下降的 + openRate = 0 + } + + // 方案一:多空SMA:略微平滑,收益略小一点 + // 方案二:多空VWMA:略微波动大点,收益略好一点 + price := ta.AvgPrice(e) + ma1 := com.GetMA(price, e.Volume, maLType, maLLen) + ma2 := com.GetMA(price, e.Volume, maLType, maLBig) + x1 := ma1.Cross(ma2) + if x1 == 1 && openRate > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate}) + } else if x1 == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "xlong", Dirt: core.OdDirtLong}) + } + + ma3 := com.GetMA(price, e.Volume, maSType, maSLen) + ma4 := com.GetMA(price, e.Volume, maSType, maSBig) + x2 := ma3.Cross(ma4) + if x2 == -1 && openRate > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate}) + } else if x2 == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "xshort", Dirt: core.OdDirtShort}) + } + }, + } +} diff --git a/trend/20250225/SuperFollow.go b/trend/20250225/SuperFollow.go new file mode 100644 index 0000000..068dc9e --- /dev/null +++ b/trend/20250225/SuperFollow.go @@ -0,0 +1,111 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "strconv" +) + +/* +原始灵感:https://www.fmz.com/strategy/442547 +已改动了大部分 + +测试在4h上表现最佳;210101-241101,48个品种 +收益1919%,回撤23%,夏普5.5 +添加adx过滤,收益增加 +测试使用ATRBase显著收益下降 +添加跟踪止损收益下降 +2025/02/24添加大周期方向开单权重,15品种夏普从9.43增长到10.48;最大回撤14.7%变为12.4% +*/ +type supFlwMore struct { + enter *strat.EnterReq + exitIds []int64 +} + +func SuperFollow(p *config.RunPolicyConfig) *strat.TradeStrat { + atrLen := 14 // 默认12 + mult := p.Def("mult", 1.4, core.PNorm(0.7, 2.5)) + sRate := p.Def("sRate", 1.1, core.PNorm(0.7, 1.8)) + slAtr := p.Def("slAtr", 1.6, core.PNorm(1, 2.5)) + tpRate := p.Def("tpRate", 3, core.PNorm(1.7, 5.5)) + shortMult := -mult * sRate + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxShort: 1, + EachMaxLong: 1, + OnStartUp: func(s *strat.StratJob) { + s.More = 0 + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 100}, + } + }, + //OnBar: func(s *strat.StratJob) { + // m, _ := s.More.(*supFlwMore) + // if m.enter != nil { + // s.OpenOrder(m.enter) + // m.enter = nil + // } else if len(m.exitIds) > 0 { + // for _, id := range m.exitIds { + // s.CloseOrders(&strat.ExitReq{Tag: "tooLong", OrderID: id}) + // } + // m.exitIds = nil + // } + //}, + //OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // m, _ := s.More.(*supFlwMore) + OnBar: func(s *strat.StratJob) { + e := s.Env + o, c := e.Open.Get(0), e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, atrLen).Get(0) + dist := (c - o) / atr + + adx := ta.ADX(e.High, e.Low, e.Close, 30).Get(0) + //bigDirt := s.More.(int) + + openDirt := 0 + overRate := 0.0 + if dist < shortMult && adx < 20 { + if len(s.ShortOrders) == 0 { + openDirt = -1 + } + overRate = dist / shortMult + } else if dist > mult { + if len(s.LongOrders) == 0 { + openDirt = 1 + } + overRate = dist / mult + } + if openDirt != 0 { + costRate := overRate * 1.1 + //if openDirt*bigDirt < 0 { + // costRate *= 0.5 + //} + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(openDirt), + Short: openDirt < 0, + TakeProfitVal: atr * slAtr * tpRate * overRate, + StopLossVal: atr * slAtr * overRate, + CostRate: costRate, + }) + } else { + // 长期未关闭的,超过200个平仓 + odList := s.GetOrders(core.OdDirtBoth) + for _, od := range odList { + holdNum := e.BarCount(od.EnterAt) + if holdNum > 200 { + s.CloseOrders(&strat.ExitReq{Tag: "tooLong", OrderID: od.ID}) + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + maSml := ta.SMA(e.Close, 15) + maBig := ta.SMA(e.Close, 70) + s.More = maSml.Cross(maBig) + }, + } +} diff --git a/trend/MACDEMAV2.go b/trend/MACDEMAV2.go new file mode 100644 index 0000000..e84563d --- /dev/null +++ b/trend/MACDEMAV2.go @@ -0,0 +1,113 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +测试加入1d的大周期过滤,对做多或做空都未获得更好效果,反而略差 + +20210101-20250101 +只做多:1776%,回撤28.8%,夏普3.93 +多空双开:1913%,回撤22%,夏普4.53 +做空部分单独运行波动很大,但和做多一起运行可减少做多的回撤 + +参考SMACross1加入海龟资金和ErDelta过滤后: +170品种下:夏普从7.97提升到8.91,显示最大回撤从18.71%下降到17.65%;收益从718%变为707% + +2025/02/25 ERChg<1时保留ATR比率开单,整体*1.15 +*/ +type entExitMore struct { + enter *strat.EnterReq + exit *strat.ExitReq +} + +func MACDEMAV2(p *config.RunPolicyConfig) *strat.TradeStrat { + // 初始HMA 25 4 + maType := com.DefMaType(p, "ma", com.MaHMA) // hma 包揽优化列表头部 + maLen := p.DefInt("maLen", 15, core.PNormF(7, 100, 25, 0)) // 超过40表现不佳,20多表现最好 + maRate := p.Def("maRate", 4.1, core.PNorm(1.5, 7)) + emaLen := p.DefInt("emaLen", 100, core.PNormF(30, 240, 100, 0)) + macdRt := p.Def("macdRt", 1, core.PNormF(0.7, 3, 1.2, 0)) + maBigLen := int(math.Round(float64(maLen) * maRate)) + mc1, mc2, mc3 := int(math.Round(12*macdRt)), int(math.Round(26*macdRt)), int(math.Round(9*macdRt)) + p.KeepHyperOnly("ma", "maLen", "maRate") + return &strat.TradeStrat{ + WarmupNum: 500, + EachMaxLong: 1, + EachMaxShort: 1, + //OnStartUp: func(s *strat.StratJob) { + // s.More = &entExitMore{} + //}, + //OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + // return []*strat.PairSub{ + // {"_cur_", "4h", 500}, + // } + //}, + //OnBar: func(s *strat.StratJob) { + // m, _ := s.More.(*entExitMore) + // if m.enter != nil { + // s.OpenOrder(m.enter) + // m.enter = nil + // } + // if m.exit != nil { + // s.CloseOrders(m.exit) + // m.exit = nil + // } + //}, + //OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // m, _ := s.More.(*entExitMore) + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + openRate *= 1.15 + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } + + ema := ta.EMA(e.Close, emaLen).Get(0) + macd, signal := ta.MACD(e.Close, mc1, mc2, mc3) + macdSub := macd.Get(0) - signal.Get(0) + + if c > ema && macdSub > 0 && len(s.LongOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + CostRate: openRate, + }) + } else if len(s.LongOrders) > 0 && (macdSub < 0 && c < ema) { + s.CloseOrders(&strat.ExitReq{ + Tag: "closeL", + Dirt: core.OdDirtLong, + }) + } + + ma1 := com.GetMA(e.Close, e.Volume, maType, maLen) + ma2 := com.GetMA(e.Close, e.Volume, maType, maBigLen) + cross := ma1.Cross(ma2) + if cross == 1 && len(s.ShortOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "closeS", + Dirt: core.OdDirtShort, + }) + } else if cross == -1 && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + CostRate: openRate, + }) + } + }, + } +} diff --git a/trend/MACDEMAV3.go b/trend/MACDEMAV3.go new file mode 100644 index 0000000..0337ba3 --- /dev/null +++ b/trend/MACDEMAV3.go @@ -0,0 +1,114 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +基于MACDEMAV2,修改做空部分。做空的表现和SMACrossV1很相似,导致趋同性回撤 + +压力位开空,止损为atr +盈利空间超过atr,设置止损为c+atr +盈利超过3倍atr,设置止损0.6 +10个bar时,小亏,直接平仓 +*/ + +func MACDEMAV3(p *config.RunPolicyConfig) *strat.TradeStrat { + emaLen := p.DefInt("emaLen", 100, core.PNormF(30, 240, 100, 0)) + macdRt := p.Def("macdRt", 1, core.PNormF(0.7, 3, 1.2, 0)) + + lenAtr := int(p.Def("atr", 29, core.PNorm(7, 40))) + baseAtrLen := int(float64(lenAtr) * 4.3) + mc1, mc2, mc3 := int(math.Round(12*macdRt)), int(math.Round(26*macdRt)), int(math.Round(9*macdRt)) + p.KeepHyperOnly("emaLen", "macdRt") + + stateKey := "state" + return &strat.TradeStrat{ + WarmupNum: 500, + EachMaxLong: 1, + EachMaxShort: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + atrVal := atr.Get(0) + atrMa := (atrBase + atrVal) / 2 + + ema := ta.EMA(e.Close, emaLen).Get(0) + macd, signal := ta.MACD(e.Close, mc1, mc2, mc3) + macdSub := macd.Get(0) - signal.Get(0) + + hh3 := ta.Highest(e.High, 3).Get(0) + _, kdjD, _ := ta.KDJBy(e.High, e.Low, e.Close, 13, 3, 3, "sma") + dSMA := ta.SMA(kdjD, 3) + isYaLi := dSMA.Cross(80.5) == 1 // crossover + + if (c-ema)*macdSub > 0 { + if macdSub < 0 { + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "closeL", + Dirt: core.OdDirtLong, + }) + } + } else { + //s.OpenOrder(&strat.EnterReq{ + // Tag: "long", + //}) + } + } + + if len(s.ShortOrders) > 0 { + low := e.Low.Get(0) + for _, od := range s.ShortOrders { + if od.Status < ormo.InOutStatusFullEnter { + continue + } + // state: 0初始止损 1平衡止损c+atr 2盈利宽止损 + state := od.GetInfoFloat64(stateKey) + bestRange := od.InitPrice - low + sl := od.GetStopLoss() + if bestRange > atrMa*2 || state == 2 { + // 处于较好盈利 + newSl := c + bestRange*0.35 + if newSl < sl.Price { + od.SetStopLoss(&ormo.ExitTrigger{Price: newSl, Tag: "good"}) + od.SetInfo(stateKey, 2) + } + continue + } + holdNum := e.BarCount(od.EnterAt) + if holdNum > 9.5 && od.Profit < 0 { + // 超过10个bar,依然亏,直接平 + s.CloseOrders(&strat.ExitReq{Tag: "exit10", OrderID: od.ID}) + continue + } + if bestRange > atrMa*0.6 || state == 1 { + // 处于平衡状态 + newSL := c + atrMa*0.3 + if newSL < sl.Price { + od.SetStopLoss(&ormo.ExitTrigger{Price: newSL, Tag: "zero"}) + od.SetInfo(stateKey, 1) + } + } + } + } + + if isYaLi && len(s.ShortOrders) == 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + Limit: hh3 - atrMa*0.5, + StopLoss: hh3 + atrMa, + StopBars: 7, + }) + } + }, + } +} diff --git a/trend/MACDX.go b/trend/MACDX.go new file mode 100644 index 0000000..03bc7dd --- /dev/null +++ b/trend/MACDX.go @@ -0,0 +1,105 @@ +package trend + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +视频号:山居听雨楼 2025/02/19 +“有没有一眼就能看清楚,而且很好用的交易方法。。。。。这里分享一个只用macd的交易方法。” + +【主要思想】 +大周期(1d)MACD快线和慢线都处于0轴上方看多,都处于0轴下方看空; +小周期(8h)MACD: + 看多时:快线上穿0轴或金叉,做多;死叉平仓。 + 看空时:快线下穿0轴或死叉,做空,死叉平仓。 + +测试小周期金叉死叉无需要求0轴上下,略好一点。 +测试小周期当前参数4h盈利最佳。大周期1d最佳。 +*/ + +func MACDX2(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 300, + RunTimeFrames: []string{"4h"}, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 150}, + } + }, + OnStartUp: func(s *strat.StratJob) { + s.More = 0 + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + dirt := s.More.(int) + macd, signal := ta.MACD(e.Close, 26, 56, 20) + macdX := macd.Cross(signal) + macdX2 := macd.Cross(0) + + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * e.Close.Get(0) / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + openRate *= 1.3 + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } + if len(s.LongOrders) > 0 { + if macdX == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "closeL", Dirt: core.OdDirtLong, Log: true}) + } + } else if dirt > 0 && openRate > 0 { + if macdX == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate, Log: true}) + } else if macdX2 == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "long2", CostRate: openRate, Log: true}) + } + } + if len(s.ShortOrders) > 0 { + if macdX == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "closeS", Dirt: core.OdDirtShort, Log: true}) + } + } else if dirt < 0 && openRate > 0 { + if macdX == -1 { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate, Log: true}) + } else if macdX2 == -1 { + s.OpenOrder(&strat.EnterReq{Tag: "short2", Short: true, CostRate: openRate, Log: true}) + } + } + if !s.IsWarmUp { + line := fmt.Sprintf("%s %s %s %d %d %f %f %f %f %f macd:%f signal:%f atr:%f er:%f x1:%v x2:%v dirt:%v rate:%f", + s.Account, s.Symbol.Symbol, s.TimeFrame, len(s.LongOrders), len(s.ShortOrders), + e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0), e.Volume.Get(0), + macd.Get(0), signal.Get(0), atr.Get(0), er.Get(0), macdX, macdX2, dirt, openRate) + s.Strat.WriteOutput(line, true) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + macdS, signalS := ta.MACD(e.Close, 12, 26, 9) + macd := macdS.Get(0) + signal := signalS.Get(0) + if macd > 0 && signal > 0 { + s.More = 1 + } else if macd < 0 && signal < 0 { + s.More = -1 + } else { + s.More = 0 + } + if !s.IsWarmUp { + line := fmt.Sprintf("%s %s %f %f %f %f %f macd:%f signal:%f", + pair, tf, e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0), e.Volume.Get(0), + macd, signal) + s.Strat.WriteOutput(line, true) + } + }, + } +} diff --git a/trend/SMACrossV1.go b/trend/SMACrossV1.go new file mode 100644 index 0000000..bdfd61e --- /dev/null +++ b/trend/SMACrossV1.go @@ -0,0 +1,116 @@ +package trend + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +自研均线趋势策略,多空都开。适合1h +在15m上,需把均线周期乘以4 +尝试加止损、止盈、限价入场,都会导致整体收益减少 +48个品种,21年到24年11月1日,累计收益1560%,最大回撤25%,夏普5.8 + +总收益93%来自多头 +但加入空头可使夏普从4.8提高到5.8 +多空的参数独立,比同一套参数,收益更稳定些 + +基于连续信号的商品期货交易策略 +https://mp.weixin.qq.com/s/6MS94OhGYhtjBQ1Ufa5Rag +下面基于170固定品种: +使用1.5%*Close/atr作为开仓倍数,夏普从8.12提升到9.63 +增加ErDelta过滤,夏普从9.63提升到11.03;最大回撤从21.8%降低到20.8% + +有效信号权重测试没有明显改善: +OnStartUp: func(s *strat.StratJob) { + s.More = []float64{0.5, 0.5} // long, short +}, +probs := s.More.([]float64) +if x1 == 1{ + newShort := probs[1] * 0.5 + s.More = []float64{1 - newShort, newShort} + sigDelta := math.Abs(1 - newShort*2) + if sigDelta > 0.2 { + openRate *= (1 - newShort) * 1.7 + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate}) + } +} +if x2 == -1{ + newLong := probs[0] * 0.5 + s.More = []float64{newLong, 1 - newLong} + sigDelta := math.Abs(1 - newLong*2) + if sigDelta > 0.2 { + openRate *= (1 - newLong) * 1.7 + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate}) + } +} +*/ + +func SMACrossV1(p *config.RunPolicyConfig) *strat.TradeStrat { + // 初始:两个SMA 做空:SMA 15 8 + maLType := com.DefMaType(p, "maL", com.MaSMA) + maSType := com.DefMaType(p, "maS", com.MaSMA) + maLLen := p.DefInt("maLLen", 20, core.PNormF(7, 70, 20, 0)) + maSLen := p.DefInt("maSLen", 15, core.PNormF(5, 50, 15, 0)) + maLRate := p.Def("maLRate", 9, core.PNorm(2, 15)) + maSRate := p.Def("maSRate", 8, core.PNorm(2, 14)) + maLBig := int(math.Round(float64(maLLen) * maLRate)) + maSBig := int(math.Round(float64(maSLen) * maSRate)) + p.KeepHyperOnly("maSLen", "maSRate") + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxShort: 1, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } else { + // 过滤掉ER下降的 + openRate = 0 + } + + // 方案一:多空SMA:略微平滑,收益略小一点 + // 方案二:多空VWMA:略微波动大点,收益略好一点 + price := ta.AvgPrice(e) + ma1 := com.GetMA(price, e.Volume, maLType, maLLen) + ma2 := com.GetMA(price, e.Volume, maLType, maLBig) + x1 := ma1.Cross(ma2) + if x1 == 1 && openRate > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate, Log: true}) + } else if x1 == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "xlong", Dirt: core.OdDirtLong, Log: true}) + } + + ma3 := com.GetMA(price, e.Volume, maSType, maSLen) + ma4 := com.GetMA(price, e.Volume, maSType, maSBig) + x2 := ma3.Cross(ma4) + if x2 == -1 && openRate > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate, Log: true}) + } else if x2 == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "xshort", Dirt: core.OdDirtShort, Log: true}) + } + if !s.IsWarmUp { + line := fmt.Sprintf("%s %s %d %d %f %f %f %f %f atr:%f er:%f m1:%f m2:%f m3:%f m4:%f x1:%v x2:%v rate:%f", + s.Account, s.Symbol.Symbol, len(s.LongOrders), len(s.ShortOrders), + e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0), e.Volume.Get(0), + atr.Get(0), er.Get(0), ma1.Get(0), ma2.Get(0), ma3.Get(0), ma4.Get(0), + x1, x2, openRate) + s.Strat.WriteOutput(line, true) + } + }, + } +} diff --git a/trend/SMACrossV2.go b/trend/SMACrossV2.go new file mode 100644 index 0000000..5904a40 --- /dev/null +++ b/trend/SMACrossV2.go @@ -0,0 +1,144 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +/* +自研均线趋势策略,多空都开。适合1h +尝试加止损、止盈、限价入场,都会导致整体收益减少 +48个品种,21年到24年11月1日,累计收益1291%,最大回撤66%,夏普9.2 + +总收益90%来自多头 +但加入空投可使夏普从5.64提高到9.2 + +TODO 添加相似矩阵过滤,趋同的品种不开单 +结合MACDEMA,优化做多逻辑 + +相比SMACrossV1,略微不平滑;且收益降低;优先使用SMACrossV1 + +*/ + +type maSta struct { + openPrice float64 + bigRsi float64 + bigLong bool +} + +func SMACrossV2(p *config.RunPolicyConfig) *strat.TradeStrat { + // 初始:两个SMA 做空:SMA 15 8 + maLType := com.DefMaType(p, "maL", com.MaSMA) + maSType := com.DefMaType(p, "maS", com.MaSMA) + maLLen := p.DefInt("maLLen", 20, core.PNormF(7, 70, 20, 0)) + maSLen := p.DefInt("maSLen", 15, core.PNormF(5, 50, 15, 0)) + maLRate := p.Def("maLRate", 9, core.PNorm(2, 15)) + maSRate := p.Def("maSRate", 8, core.PNorm(2, 14)) + maLBig := int(math.Round(float64(maLLen) * maLRate)) + maSBig := int(math.Round(float64(maSLen) * maSRate)) + p.KeepHyperOnly("maSLen", "maSRate") + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxShort: 1, + EachMaxLong: 1, + BatchInOut: true, + OnStartUp: func(s *strat.StratJob) { + s.More = &maSta{} + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "4h", 30}, + } + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + m, _ := s.More.(*maSta) + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + er := ta.ER(e.Close, 50) + erDelta := min(1.3, er.Get(0)/er.Get(12)) + if erDelta > 1 { + openRate *= erDelta + } else { + // 过滤掉ER下降的 + openRate = 0 + } + + // 方案一:多空SMA:略微平滑,收益略小一点 + // 方案二:多空VWMA:略微波动大点,收益略好一点 + price := ta.AvgPrice(e) + ma1 := com.GetMA(price, e.Volume, maLType, maLLen) + ma2 := com.GetMA(price, e.Volume, maLType, maLBig) + x1 := ma1.Cross(ma2) + if x1 == 1 && openRate > 0 { + s.OpenOrder(&strat.EnterReq{Tag: "long", CostRate: openRate, Log: true}) + } else if x1 == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "xlong", Dirt: core.OdDirtLong, Log: true}) + } + + if len(s.LongOrders) > 0 && m.openPrice < c && m.bigRsi > 80 && ma1.Get(0)-ma1.Get(2) > atr.Get(0)*0.5 { + // 多仓,盈利,小均线变向平仓 + m.bigLong = true + } + //if m.bigLong && m.bigRsi > 70 { + // m.bigLong = false + // s.OpenOrder(&strat.EnterReq{Tag: "short2", Short: true, CostRate: openRate, Log: true}) + //} + + ma3 := com.GetMA(price, e.Volume, maSType, maSLen) + ma4 := com.GetMA(price, e.Volume, maSType, maSBig) + x2 := ma3.Cross(ma4) + if len(s.ShortOrders) == 0 && x2 == -1 && openRate > 0 { + m.bigLong = false + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, CostRate: openRate, Log: true}) + } else if x2 == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "xshort", Dirt: core.OdDirtShort, Log: true}) + } + if len(s.ShortOrders) > 0 && m.bigRsi < 30 && s.ShortOrders[0].ProfitRate > 0 { + s.CloseOrders(&strat.ExitReq{Tag: "xshort2", Dirt: core.OdDirtShort, Log: true}) + } + //if !s.IsWarmUp { + // line := fmt.Sprintf("%s %s %d %d %f %f %f %f %f atr:%f er:%f m1:%f m2:%f m3:%f m4:%f x1:%v x2:%v rate:%f", + // s.Account, s.Symbol.Symbol, len(s.LongOrders), len(s.ShortOrders), + // e.Open.Get(0), e.High.Get(0), e.Low.Get(0), e.Close.Get(0), e.Volume.Get(0), + // atr.Get(0), er.Get(0), ma1.Get(0), ma2.Get(0), ma3.Get(0), ma4.Get(0), + // x1, x2, openRate) + // s.Strat.WriteOutput(line, true) + //} + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + m, _ := s.More.(*maSta) + m.bigRsi = ta.RSI(e.Close, 9).Get(0) + }, + OnBatchJobs: func(jobs []*strat.StratJob) { + longNum := float64(0) + bigDown := float64(0) + for _, s := range jobs { + if len(s.LongOrders) == 0 { + continue + } + longNum += 1 + m, _ := s.More.(*maSta) + if m.bigLong { + bigDown += 1 + } + } + if bigDown*1.5 > longNum && longNum*2 > float64(len(jobs)) { + for _, s := range jobs { + if len(s.LongOrders) == 0 { + continue + } + s.CloseOrders(&strat.ExitReq{Tag: "xlong2", Dirt: core.OdDirtLong, Log: true}) + } + } + }, + } +} diff --git a/trend/SuperFollow.go b/trend/SuperFollow.go new file mode 100644 index 0000000..068dc9e --- /dev/null +++ b/trend/SuperFollow.go @@ -0,0 +1,111 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "strconv" +) + +/* +原始灵感:https://www.fmz.com/strategy/442547 +已改动了大部分 + +测试在4h上表现最佳;210101-241101,48个品种 +收益1919%,回撤23%,夏普5.5 +添加adx过滤,收益增加 +测试使用ATRBase显著收益下降 +添加跟踪止损收益下降 +2025/02/24添加大周期方向开单权重,15品种夏普从9.43增长到10.48;最大回撤14.7%变为12.4% +*/ +type supFlwMore struct { + enter *strat.EnterReq + exitIds []int64 +} + +func SuperFollow(p *config.RunPolicyConfig) *strat.TradeStrat { + atrLen := 14 // 默认12 + mult := p.Def("mult", 1.4, core.PNorm(0.7, 2.5)) + sRate := p.Def("sRate", 1.1, core.PNorm(0.7, 1.8)) + slAtr := p.Def("slAtr", 1.6, core.PNorm(1, 2.5)) + tpRate := p.Def("tpRate", 3, core.PNorm(1.7, 5.5)) + shortMult := -mult * sRate + return &strat.TradeStrat{ + WarmupNum: 200, + EachMaxShort: 1, + EachMaxLong: 1, + OnStartUp: func(s *strat.StratJob) { + s.More = 0 + }, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 100}, + } + }, + //OnBar: func(s *strat.StratJob) { + // m, _ := s.More.(*supFlwMore) + // if m.enter != nil { + // s.OpenOrder(m.enter) + // m.enter = nil + // } else if len(m.exitIds) > 0 { + // for _, id := range m.exitIds { + // s.CloseOrders(&strat.ExitReq{Tag: "tooLong", OrderID: id}) + // } + // m.exitIds = nil + // } + //}, + //OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + // m, _ := s.More.(*supFlwMore) + OnBar: func(s *strat.StratJob) { + e := s.Env + o, c := e.Open.Get(0), e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, atrLen).Get(0) + dist := (c - o) / atr + + adx := ta.ADX(e.High, e.Low, e.Close, 30).Get(0) + //bigDirt := s.More.(int) + + openDirt := 0 + overRate := 0.0 + if dist < shortMult && adx < 20 { + if len(s.ShortOrders) == 0 { + openDirt = -1 + } + overRate = dist / shortMult + } else if dist > mult { + if len(s.LongOrders) == 0 { + openDirt = 1 + } + overRate = dist / mult + } + if openDirt != 0 { + costRate := overRate * 1.1 + //if openDirt*bigDirt < 0 { + // costRate *= 0.5 + //} + s.OpenOrder(&strat.EnterReq{ + Tag: strconv.Itoa(openDirt), + Short: openDirt < 0, + TakeProfitVal: atr * slAtr * tpRate * overRate, + StopLossVal: atr * slAtr * overRate, + CostRate: costRate, + }) + } else { + // 长期未关闭的,超过200个平仓 + odList := s.GetOrders(core.OdDirtBoth) + for _, od := range odList { + holdNum := e.BarCount(od.EnterAt) + if holdNum > 200 { + s.CloseOrders(&strat.ExitReq{Tag: "tooLong", OrderID: od.ID}) + } + } + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + maSml := ta.SMA(e.Close, 15) + maBig := ta.SMA(e.Close, 70) + s.More = maSml.Cross(maBig) + }, + } +} diff --git a/trend/cci100.go b/trend/cci100.go new file mode 100644 index 0000000..ca180bc --- /dev/null +++ b/trend/cci100.go @@ -0,0 +1,41 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +视频号:量化牛牛:CCI指标修改一个参数,“单车”变“汽车” +收益很不稳定,至少8H盈利。 +*/ + +func CCI100(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + cci := ta.CCI(e.Close, 50) + cciN100 := cci.Cross(-100) + cciP100 := cci.Cross(100) + + if len(s.LongOrders) > 0 { + if cciP100 == 1 { + // 上穿100,减仓 + s.CloseOrders(&strat.ExitReq{Tag: "half", ExitRate: 0.5}) + } else if cciP100 == -1 { + // 下穿100,平仓 + s.CloseOrders(&strat.ExitReq{Tag: "exit"}) + } else if cciN100 == -1 { + // 止损离场 + s.CloseOrders(&strat.ExitReq{Tag: "stop"}) + } + } + if cciN100 == 1 { + // 上穿-100,入场 + s.OpenOrder(&strat.EnterReq{Tag: "long"}) + } + }, + } +} diff --git a/trend/cciMa.go b/trend/cciMa.go new file mode 100644 index 0000000..713fd58 --- /dev/null +++ b/trend/cciMa.go @@ -0,0 +1,53 @@ +package trend + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +来自视频号:落升聊股:周K线+CCI指标,操作简单,只看执行力 +适用于4H,48品种,210101-241101,收益725%,回撤23%,夏普5.14 +熊市期间有回撤 +*/ + +func CciMa(p *config.RunPolicyConfig) *strat.TradeStrat { + cciLen := p.DefInt("cciLen", 89, core.PNorm(50, 200)) + maLen := p.DefInt("maLen", 20, core.PNorm(10, 40)) + tpRate := p.Def("tpRate", 3, core.PNorm(1, 5)) + slRate := p.Def("slRate", 2, core.PNorm(1, 3)) + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + cci := ta.CCI(e.Close, cciLen).Get(0) + ma := ta.SMA(e.Close, maLen).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + dirt := float64(0) + if c > ma && cci > 0 && len(s.LongOrders) == 0 { + dirt = 1 + } else if c < ma && cci < 0 && len(s.ShortOrders) == 0 { + dirt = -1 + } + if dirt != 0 { + sl := atr * slRate + tp := sl * tpRate + //limit, sl, tp := atr*0.7, atr*3, atr*9 + //if dirt < 0 { + // limit, sl, tp = atr*0.2, atr*1.6, atr*4 + //} + s.OpenOrder(&strat.EnterReq{ + Tag: fmt.Sprintf("%v", dirt), + //Limit: c - limit*dirt, + Short: dirt < 0, + StopLossVal: sl, + TakeProfitVal: tp, + }) + } + }, + } +} diff --git a/trend/common.go b/trend/common.go new file mode 100644 index 0000000..aae984f --- /dev/null +++ b/trend/common.go @@ -0,0 +1,39 @@ +package trend + +import ( + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/trend/ma1" +) + +func init() { + strat.AddStratGroup("trend", map[string]strat.FuncMakeStrat{ + "t01v1": T01V1, + "macdx": MACDX, + "fastio": fastIO, + "maCross": MaCross, + "ma1": ma1.MainV2_1, + "ma1_v3": ma1.MainV3, + "SMACrossV1": SMACrossV1, + "SMACrossV2": SMACrossV2, + "MACDX2": MACDX2, + "MACDEMAV2": MACDEMAV2, + "MACDEMAV3": MACDEMAV3, + "SuperFollow": SuperFollow, + "CciMa": CciMa, + "dvrsi": DVRSI, + "cci100": CCI100, + "KdjBig": KdjBig, + "KdjX": KdjX, + "MaMulti": MaMulti, + "pcmp": pcmp, + "tdi": eelfranzTDI, + "randCut": randCut, + "smp1": smp1, + "swing": swing, + + "m01": SuperFollow, + "m02": MACDEMAV2, + "m03": SMACrossV1, + "m04": MACDX2, + }) +} diff --git a/trend/dv2.go b/trend/dv2.go new file mode 100644 index 0000000..1830aef --- /dev/null +++ b/trend/dv2.go @@ -0,0 +1,59 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +基于DV2超卖且大均线上方时做多 +基于RSI4百分位排名超卖,且大均线上方时做多 + +效果好于双均线仅做多。 + +DV2加入做空部分持续亏钱 +RSI加入做空变化不大 +*/ + +func DVRSI(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + RunTimeFrames: []string{"4h"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + dv := ta.DV2(e.High, e.Low, e.Close, 126, 2).Get(0) + sma := ta.SMA(e.Close, 50).Get(0) + ma10 := ta.SMA(e.Close, 10).Get(0) + rsi := ta.PercentRank(ta.RSI(e.Close, 4), 126).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + if len(s.LongOrders) > 0 { + if c < ma10 { + for _, o := range s.LongOrders { + holdNum := e.BarCount(o.EnterAt) + if holdNum > 5 { + s.CloseOrders(&strat.ExitReq{Tag: "exitL", OrderID: o.ID}) + } + } + } + } else if len(s.ShortOrders) > 0 { + if c > ma10 { + for _, o := range s.ShortOrders { + holdNum := e.BarCount(o.EnterAt) + if holdNum > 5 { + s.CloseOrders(&strat.ExitReq{Tag: "exitS", OrderID: o.ID}) + } + } + } + } else if dv <= 10 && c > sma { + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLossVal: atr * 3}) + } else if rsi < 10 && c > sma { + s.OpenOrder(&strat.EnterReq{Tag: "long", StopLossVal: atr * 3}) + } + //else if rsi > 90 && c < sma { + // s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true, StopLossVal: atr}) + //} + }, + } +} diff --git a/trend/eelfranzTDI.go b/trend/eelfranzTDI.go new file mode 100644 index 0000000..1a48bd4 --- /dev/null +++ b/trend/eelfranzTDI.go @@ -0,0 +1,86 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +https://www.forexfactory.com/thread/291622-trading-made-simple +在1d周期,大多数品种上长期较好;稳定性一般 +*/ + +func eelfranzTDI(p *config.RunPolicyConfig) *strat.TradeStrat { + var rsiLen = p.DefInt("rsiLen", 13, core.PNorm(8, 25)) + var rsiMaLen = p.DefInt("rsiMa", 34, core.PNorm(20, 40)) + var fastLen = p.DefInt("fast", 2, core.PNorm(2, 5)) + var slowLen = p.DefInt("slow", 7, core.PNorm(4, 15)) + return &strat.TradeStrat{ + WarmupNum: 50, + OnBar: func(s *strat.StratJob) { + e := s.Env + rsi := ta.RSI(e.Close, rsiLen) + rsiMa := ta.SMA(rsi, rsiMaLen).Get(0) + + stDev := StDev(rsi, rsiMaLen).Get(0) + upZone := rsiMa + 1.6185*stDev + dnZone := rsiMa - 1.6185*stDev + // market base line + marketBase := (upZone + dnZone) / 2 + + // rsi price line: green + fast := ta.SMA(rsi, fastLen).Get(0) + // trade signal line: red + slow := ta.SMA(rsi, slowLen).Get(0) + + if len(s.LongOrders) > 0 { + if fast < slow { + s.CloseOrders(&strat.ExitReq{ + Tag: "exitL", + Dirt: core.OdDirtLong, + }) + } + } else if fast > max(slow, marketBase) { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + if len(s.ShortOrders) > 0 { + if fast > slow { + s.CloseOrders(&strat.ExitReq{ + Tag: "exitS", + Dirt: core.OdDirtShort, + }) + } + } else if slow > max(fast, marketBase) { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + }) + } + }, + } +} + +func StDev(obj *ta.Series, period int) *ta.Series { + res := obj.To("stDev", period) + if res.Cached() { + return res + } + arr := ta.WrapFloatArr(res, period, obj.Get(0)) + if len(arr) < period { + return res.Append(math.NaN()) + } else { + var sum, ssum float64 + for _, v := range arr { + sum += v + ssum += v * v + } + var lenFlt = float64(period) + variance := (ssum*lenFlt - sum*sum) / (lenFlt * (lenFlt - 1)) + return res.Append(math.Sqrt(variance)) + } +} diff --git a/trend/fastio.go b/trend/fastio.go new file mode 100644 index 0000000..f2dd438 --- /dev/null +++ b/trend/fastio.go @@ -0,0 +1,53 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +func fastIO(p *config.RunPolicyConfig) *strat.TradeStrat { + lenAtr := 15 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + return &strat.TradeStrat{ + WarmupNum: 100, + EachMaxLong: 1, + EachMaxShort: 1, + DrawDownExit: true, + OnBar: func(s *strat.StratJob) { + e := s.Env + rsi4 := ta.RSI50(e.Close, 4).Get(0) + rsi14 := ta.RSI50(e.Close, 14).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + + if rsi4 > 30 && rsi14 > 25 && rsi14 < 40 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: atrBase, + TakeProfitVal: atrBase * 2.5, + }) + } else if rsi4 < -30 && rsi14 < -25 && rsi14 > -40 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + StopLossVal: atrBase, + TakeProfitVal: atrBase * 2.5, + }) + } + }, + GetDrawDownExitRate: func(s *strat.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + sl := od.GetStopLoss() + if sl == nil { + return 0 + } + slRate := sl.Range / od.Enter.Average + if maxChg > slRate { + return 0.63 + } + return 0 + }, + } +} diff --git a/trend/kdjBig.go b/trend/kdjBig.go new file mode 100644 index 0000000..8962270 --- /dev/null +++ b/trend/kdjBig.go @@ -0,0 +1,46 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +视频号:量化牛牛:改一个参数,如何轻松拿捏行情呢? + +KDJ的9,3,3改9,3,30(原文89),然后平滑D和50交叉时开平仓。 +最小4H +加入EMA做空后更稳定。 +*/ + +func KdjBig(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + _, kdjD, _ := ta.KDJ(e.High, e.Low, e.Close, 9, 3, 29) + jx50 := kdjD.Cross(50) + maaX := ta.EMA(e.Close, 9).Cross(ta.EMA(e.Close, 30)) + if len(s.LongOrders) > 0 { + if jx50 == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "exitL", Dirt: core.OdDirtLong}) + } + } else if len(s.ShortOrders) > 0 { + if maaX == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "exitS", Dirt: core.OdDirtShort}) + } + } else if jx50 == 1 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } else if maaX == -1 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + }) + } + }, + } +} diff --git a/trend/kdjX.go b/trend/kdjX.go new file mode 100644 index 0000000..dc9913e --- /dev/null +++ b/trend/kdjX.go @@ -0,0 +1,45 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +视频号:漫谈期市:日内高胜率交易系统 +kdj:18,3,3 +20下金叉开多,80上死叉开空 +效果差,无法盈利 +*/ + +func KdjX(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + k, d, _ := ta.KDJ(e.High, e.Low, e.Close, 18, 3, 3) + kdx := k.Cross(d) + //maaX := ta.EMA(e.Close.Cross(9), ta.EMA(e.Close, 30)) + if len(s.LongOrders) > 0 { + if kdx == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "exitL", Dirt: core.OdDirtLong}) + } + } else if len(s.ShortOrders) > 0 { + if kdx == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "exitS", Dirt: core.OdDirtShort}) + } + } else if kdx == 1 && d.Get(0) < 20 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } else if kdx == -1 && d.Get(0) > 80 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + }) + } + }, + } +} diff --git a/trend/ma1/common.go b/trend/ma1/common.go new file mode 100644 index 0000000..1007a7a --- /dev/null +++ b/trend/ma1/common.go @@ -0,0 +1,68 @@ +package ma1 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +func Main(pol *config.RunPolicyConfig) *strat.TradeStrat { + return MainV2_1(pol) +} + +var ( + // 这里是默认参数,请尽量在MainVX()中修改,不要在这里修改 + periodBig = 30 + periodSml = 10 + parentRate = 0.7 + periodParBig = int(float64(periodBig) * parentRate) + periodParSml = int(float64(periodSml) * parentRate) + sampleNum = 500 // 斜率采样数量 + sampleStep = 2 // 斜率计算间隔 + atrLen = 14 + openOffRate = float64(0) // 开单时,从均线偏移基准ATR的倍数 + goodRate = float64(10) // 收益超过此倍数的ATR时,将止损更新为均线 + badRate = float64(5) // 损失超过此倍数的ATR时,止损退出 +) + +type MoreType struct { + *com.Slop + hitGoods map[int64]bool // 触发goodRate的订单ID,允许每个bar更新止损 + open int // 1开多 -1开空 +} + +func onStartUp(s *strat.StratJob) { + s.More = &MoreType{ + Slop: com.NewSlop(sampleNum, 0.25), + hitGoods: make(map[int64]bool), + } +} + +func onInfoMaCross(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + smaSml := ta.SMA(e.Close, periodParSml) + smaBig := ta.SMA(e.Close, periodParBig) + x := smaSml.Cross(smaBig) + more, _ := s.More.(*MoreType) + if x == 1 || x == -1 { + more.Dirt = x + } +} + +func onInfoMaSlop(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + sma := ta.SMA(e.Close, periodParBig) + prevSma := sma.Get(sampleStep) + if e.BarNum%sampleStep > 0 || math.IsNaN(prevSma) { + return + } + more, _ := s.More.(*MoreType) + curSma := sma.Get(0) + slop := (curSma - prevSma) / curSma + //more.Slop.Update(e.BarNum, slop) + if slop > 0 { + more.Dirt = 1 + } else { + more.Dirt = -1 + } +} diff --git a/trend/ma1/v1.go b/trend/ma1/v1.go new file mode 100644 index 0000000..d799746 --- /dev/null +++ b/trend/ma1/v1.go @@ -0,0 +1,63 @@ +package ma1 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +func MainV1(pol *config.RunPolicyConfig) *strat.TradeStrat { + periodSml = 300 + periodBig = 1800 + parentRate = 0.5 + periodParBig = int(float64(periodBig) * parentRate) + periodParSml = int(float64(periodSml) * parentRate) + return &strat.TradeStrat{ + Version: 1, + WarmupNum: periodBig, + MinTfScore: 0.75, + //DrawDownExit: true, + OnStartUp: onStartUp, + OnPairInfos: com.MakePairInfos(sampleNum*sampleStep + periodParBig), + OnInfoBar: onInfoMaCross, + OnBar: func(s *strat.StratJob) { + e := s.Env + sml := ta.SMA(e.Close, 10) + big := ta.SMA(e.Close, 50) + x := sml.Cross(big) + + smaSml := ta.SMA(e.Close, periodSml) + smaBig := ta.SMA(e.Close, periodBig) + crx := smaSml.Cross(smaBig) + + more, _ := s.More.(*MoreType) + if crx == 1 || crx == -1 { + closeTag := "x_short" + if crx < 0 { + closeTag = "x_long" + } + _ = s.CloseOrders(&strat.ExitReq{ + Tag: closeTag, + Dirt: crx * -1, + }) + if crx == more.Dirt { + // 交叉信号和大环境一致 + more.open = crx + } + } + + if more.open != 0 && more.open == -x { + more.open = 0 + openTag := "long" + if x > 0 { + openTag = "short" + } + _ = s.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: x > 0, + }) + } + }, + } +} diff --git a/trend/ma1/v2.1.go b/trend/ma1/v2.1.go new file mode 100644 index 0000000..ed5d6f8 --- /dev/null +++ b/trend/ma1/v2.1.go @@ -0,0 +1,80 @@ +package ma1 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" +) + +/* +针对15m的参数 +*/ +func MainV2_1(pol *config.RunPolicyConfig) *strat.TradeStrat { + periodSml = 120 + periodBig = 800 + parentRate = 0.5 + periodParBig = int(float64(periodBig) * parentRate) + periodParSml = int(float64(periodSml) * parentRate) + return &strat.TradeStrat{ + Version: 1, + WarmupNum: sampleNum*sampleStep + periodBig, + MinTfScore: 0.75, + //DrawDownExit: true, + OnStartUp: onStartUp, + //OnPairInfos: onPairInfos, + //OnInfoBar: onInfoMaSlop, + OnBar: func(s *strat.StratJob) { + e := s.Env + sml := ta.SMA(e.Close, 30) + big := ta.SMA(e.Close, 70) + x := sml.Cross(big) + + smaSml := ta.SMA(e.Close, periodSml) + smaBig := ta.SMA(e.Close, periodBig) + crx := smaSml.Cross(smaBig) + + more, _ := s.More.(*MoreType) + if crx == 1 || crx == -1 { + closeTag := "x_short" + if crx < 0 { + closeTag = "x_long" + } + _ = s.CloseOrders(&strat.ExitReq{ + Tag: closeTag, + Dirt: crx * -1, + }) + more.open = crx + //if crx == more.Dirt { + // 交叉信号和大环境一致 + //openTag := "long" + //if crx < 0 { + // openTag = "short" + //} + //_ = s.OpenOrder(&strategy.EnterReq{ + // Tag: openTag, + // Short: crx < 0, + //}) + //} + } + prevSma := smaBig.Get(sampleStep) + if e.BarNum%sampleStep == 0 && !math.IsNaN(prevSma) { + curSma := smaBig.Get(0) + more.Slop.Update(e.BarNum, (curSma-prevSma)/curSma) + } + + x = -x + if more.open != 0 && more.open == x && more.open*more.Dirt >= 0 { + more.open = 0 + openTag := "long" + if x < 0 { + openTag = "short" + } + _ = s.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: x < 0, + }) + } + }, + } +} diff --git a/trend/ma1/v2.go b/trend/ma1/v2.go new file mode 100644 index 0000000..22eb008 --- /dev/null +++ b/trend/ma1/v2.go @@ -0,0 +1,84 @@ +package ma1 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +/* +针对15m的参数 +*/ +func MainV2(pol *config.RunPolicyConfig) *strat.TradeStrat { + periodSml = 120 + periodBig = 750 + parentRate = 0.5 + periodParBig = int(float64(periodBig) * parentRate) + periodParSml = int(float64(periodSml) * parentRate) + return &strat.TradeStrat{ + Version: 1, + WarmupNum: periodBig, + MinTfScore: 0.75, + //DrawDownExit: true, + OnStartUp: onStartUp, + OnPairInfos: com.MakePairInfos(sampleNum*sampleStep + periodParBig), + OnInfoBar: onInfoMaCross, + OnBar: func(s *strat.StratJob) { + e := s.Env + sml := ta.SMA(e.Close, 30) + big := ta.SMA(e.Close, 70) + x := sml.Cross(big) + + smaSml := ta.SMA(e.Close, periodSml) + smaBig := ta.SMA(e.Close, periodBig) + crx := smaSml.Cross(smaBig) + + more, _ := s.More.(*MoreType) + if crx == 1 || crx == -1 { + closeTag := "x_short" + if crx < 0 { + closeTag = "x_long" + } + _ = s.CloseOrders(&strat.ExitReq{ + Tag: closeTag, + Dirt: crx * -1, + }) + if crx == more.Dirt { + // 交叉信号和大环境一致 + more.open = crx + //openTag := "long" + //if crx < 0 { + // openTag = "short" + //} + //_ = s.OpenOrder(&strategy.EnterReq{ + // Tag: openTag, + // Short: crx < 0, + //}) + } + } + x = -x + if more.open != 0 && more.open == x { + more.open = 0 + openTag := "long" + if x < 0 { + openTag = "short" + } + _ = s.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: x < 0, + }) + } + }, + //GetDrawDownExitRate: func(s *strategy.StratJob, od *ormo.InOutOrder, maxChg float64) float64 { + // if maxChg > 3 { + // return 0.2 + // } else if maxChg > 1 { + // return 0.25 + // } else if maxChg > 0.5 { + // return 0.3 + // } + // return 0 + //}, + } +} diff --git a/trend/ma1/v3.go b/trend/ma1/v3.go new file mode 100644 index 0000000..e438501 --- /dev/null +++ b/trend/ma1/v3.go @@ -0,0 +1,74 @@ +package ma1 + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" +) + +/* +针对1h的参数 +*/ +func MainV3(pol *config.RunPolicyConfig) *strat.TradeStrat { + periodSml = 28 + periodBig = 160 + parentRate = 0.75 + periodParBig = int(float64(periodBig) * parentRate) + periodParSml = int(float64(periodSml) * parentRate) + return &strat.TradeStrat{ + Version: 1, + WarmupNum: periodBig, + MinTfScore: 0.75, + //DrawDownExit: true, + OnStartUp: onStartUp, + OnPairInfos: com.MakePairInfos(sampleNum*sampleStep + periodParBig), + OnInfoBar: onInfoMaCross, + OnBar: func(s *strat.StratJob) { + e := s.Env + sml := ta.SMA(e.Close, 7) + big := ta.SMA(e.Close, 24) + x := sml.Cross(big) + + smaSml := ta.SMA(e.Close, periodSml) + smaBig := ta.SMA(e.Close, periodBig) + crx := smaSml.Cross(smaBig) + + more, _ := s.More.(*MoreType) + if crx == 1 || crx == -1 { + closeTag := "x_short" + if crx < 0 { + closeTag = "x_long" + } + _ = s.CloseOrders(&strat.ExitReq{ + Tag: closeTag, + Dirt: crx * -1, + }) + if crx == more.Dirt { + // 交叉信号和大环境一致 + more.open = crx + //openTag := "long" + //if crx < 0 { + // openTag = "short" + //} + //_ = s.OpenOrder(&strategy.EnterReq{ + // Tag: openTag, + // Short: crx < 0, + //}) + } + } + x = -x + if more.open != 0 && more.open == x { + more.open = 0 + openTag := "long" + if x < 0 { + openTag = "short" + } + _ = s.OpenOrder(&strat.EnterReq{ + Tag: openTag, + Short: x < 0, + }) + } + }, + } +} diff --git a/trend/maMulti.go b/trend/maMulti.go new file mode 100644 index 0000000..7d8fc3d --- /dev/null +++ b/trend/maMulti.go @@ -0,0 +1,55 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +视频号:六叔大师兄:散户用一个战法,当天100%不亏 2025/01/25 +5日上穿10日买入;5日上穿20日加仓; +5日向下减仓,5日下穿10日平仓; +反之做空亦然; + +去掉5日向下减仓,更好一点。 +5,10,20适合8h;目前10,20,40适合4h + +固定50品种在23年持平 +滚动50品种在23年小亏 +*/ + +func MaMulti(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.SMA(e.Close, 10) + ma10 := ta.SMA(e.Close, 20) + ma20 := ta.SMA(e.Close, 40) + ma5X10 := ma5.Cross(ma10) + ma5x20 := ma5.Cross(ma20) + if len(s.LongOrders) == 1 && ma5x20 == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "l2", CostRate: 0.5}) + } + if len(s.LongOrders) > 0 { + if ma5X10 == -1 { + s.CloseOrders(&strat.ExitReq{Tag: "exit", Dirt: core.OdDirtLong}) + } + } else if ma5X10 == 1 { + s.OpenOrder(&strat.EnterReq{Tag: "l1", CostRate: 0.5}) + } + if len(s.ShortOrders) == 1 && ma5x20 == -1 { + s.OpenOrder(&strat.EnterReq{Tag: "s2", Short: true, CostRate: 0.5}) + } + if len(s.ShortOrders) > 0 { + if ma5X10 == 1 { + s.CloseOrders(&strat.ExitReq{Tag: "exitS", Dirt: core.OdDirtShort}) + } + } else if ma5X10 == -1 { + s.OpenOrder(&strat.EnterReq{Tag: "s1", Short: true, CostRate: 0.5}) + } + }, + } +} diff --git a/trend/ma_cross.go b/trend/ma_cross.go new file mode 100644 index 0000000..35a028e --- /dev/null +++ b/trend/ma_cross.go @@ -0,0 +1,58 @@ +package trend + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +适用4h +*/ + +func MaCross(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 130, + MinTfScore: 0.75, + OnBar: func(s *strat.StratJob) { + e := s.Env + ma5 := ta.SMA(e.Close, 14) + ma30 := ta.SMA(e.Close, 40) + xnum := ma5.Cross(ma30) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + //ma5Val := ma5.Get(0) + maBigVal := ma30.Get(0) + if xnum == 1 && len(s.LongOrders) == 0 { + err := s.OpenOrder(&strat.EnterReq{ + Tag: "enter", + Limit: maBigVal, + StopLoss: maBigVal - atr*2, + }) + if err != nil { + fmt.Printf("open fail: %s", err) + } + } else if xnum == -1 && len(s.LongOrders) > 0 { + //longOd := s.LongOrders[0] + //if maBigVal-longOd.Enter.Price > atr*2 { + err := s.CloseOrders(&strat.ExitReq{ + Tag: "exit", + //Limit: maBigVal, + }) + if err != nil { + fmt.Printf("close fail: %s", err) + } + //} + } + //else if len(s.LongOrders) > 0 { + // c := e.Close.Get(0) + // for _, od := range s.LongOrders { + // if od.Status >= ormo.InOutStatusFullEnter && (c-od.Enter.Price) > atr*2 { + // od.SetInfo(orm.OdInfoStopLoss, maBigVal) + // } + // } + //} + }, + } +} diff --git a/trend/macd.go b/trend/macd.go new file mode 100644 index 0000000..72ed31d --- /dev/null +++ b/trend/macd.go @@ -0,0 +1,47 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func MACDX(pol *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 220, + EachMaxLong: 1, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 29).Get(0) + macd, macdSig := ta.MACD(e.Close, 12, 26, 9) + macdX := macd.Cross(macdSig) + + maBig := ta.SMA(e.Close, 200).Get(0) + isBull := c > maBig + + kdjK, kdjD, _ := ta.KDJ(e.High, e.Low, e.Close, 30, 5, 5) + kdjX := kdjK.Cross(kdjD) + if kdjX < 0 { + kdjX = -kdjX + } + + rsi := ta.RSI(e.Close, 4).Get(0) - 50 + + if isBull && rsi < 30 && (macdX == 1 && kdjX <= 3 || macdX > 1 && kdjX == 1) && macd.Get(0) < 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + StopLossVal: atr * 0.8, + TakeProfitVal: atr * 3, + }) + } else if !isBull && rsi > -30 && (macdX == -1 && kdjX <= 3 || macdX < -1 && kdjX == 1) && macd.Get(0) > 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + StopLossVal: atr * 0.8, + TakeProfitVal: atr * 3, + }) + } + }, + } +} diff --git a/trend/pcmp.go b/trend/pcmp.go new file mode 100644 index 0000000..2e4df56 --- /dev/null +++ b/trend/pcmp.go @@ -0,0 +1,59 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" +) + +func pcmp(pol *config.RunPolicyConfig) *strat.TradeStrat { + // 定义可优化的超参数 + shortPeriod := int(pol.Def("short", 7, core.PNorm(5, 10))) + longPeriod := int(pol.Def("long", 14, core.PNorm(10, 20))) + + return &strat.TradeStrat{ + WarmupNum: longPeriod + 5, // 预热数据数量,确保有足够的历史数据 + EachMaxLong: 1, // 最多持有1个多单 + EachMaxShort: 1, // 最多持有1个空单 + OnBar: func(s *strat.StratJob) { + e := s.Env + curPrice := e.Close.Get(0) + + // 检查当前价格是否高于短周期和长周期前的价格 + shortPriceHigher := curPrice > e.Close.Get(shortPeriod) + longPriceHigher := curPrice > e.Close.Get(longPeriod) + + // 检查当前价格是否低于短周期和长周期前的价格 + shortPriceLower := curPrice < e.Close.Get(shortPeriod) + longPriceLower := curPrice < e.Close.Get(longPeriod) + + // 多头信号:当前价格比两个周期前都高 + goLong := shortPriceHigher && longPriceHigher + + // 空头信号:当前价格比两个周期前都低 + goShort := shortPriceLower && longPriceLower + + // 检查当前订单状态 + hasLongOrder := len(s.LongOrders) > 0 + hasShortOrder := len(s.ShortOrders) > 0 + + // 开多单逻辑:当前价格比两个周期前都高,且没有持仓 + if goLong && !hasLongOrder && !hasShortOrder { + s.OpenOrder(&strat.EnterReq{Tag: "long"}) + } else if goShort && !hasShortOrder && !hasLongOrder { + // 开空单逻辑:当前价格比两个周期前都低,且没有持仓 + s.OpenOrder(&strat.EnterReq{Tag: "short", Short: true}) + } + + // 平多单逻辑:不满足做多条件,且有多单持仓 + if !goLong && hasLongOrder { + s.CloseOrders(&strat.ExitReq{Tag: "close_long", Dirt: core.OdDirtLong}) + } + + // 平空单逻辑:不满足做空条件,且有空单持仓 + if !goShort && hasShortOrder { + s.CloseOrders(&strat.ExitReq{Tag: "close_short", Dirt: core.OdDirtShort}) + } + }, + } +} diff --git a/trend/randCut.go b/trend/randCut.go new file mode 100644 index 0000000..34b17d2 --- /dev/null +++ b/trend/randCut.go @@ -0,0 +1,54 @@ +package trend + +import ( + "fmt" + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" + "math" + "math/rand" +) + +func randCut(p *config.RunPolicyConfig) *strat.TradeStrat { + slRate := p.Def("slRate", 3, core.PNorm(1, 3)) + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 30) + slRange := atr.Get(0) * slRate + odList := s.GetOrders(0) + if len(odList) > 0 { + for _, od := range odList { + sl := od.GetStopLoss() + if od.Short { + if sl.Price > c+slRange { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: c + slRange, + }) + } + } else if sl.Price < c-slRange { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: c - slRange, + }) + } + } + } else { + openRate := 0.015 * c / atr.Get(0) + if openRate < 1 { + openRate = math.Sqrt(openRate) + } + isShort := rand.Float64() > 0.5 + s.OpenOrder(&strat.EnterReq{ + Tag: fmt.Sprintf("%v", isShort), + Short: isShort, + CostRate: openRate, + StopLossVal: slRange, + }) + } + }, + } +} diff --git a/trend/rsi_macd.go b/trend/rsi_macd.go new file mode 100644 index 0000000..9c63791 --- /dev/null +++ b/trend/rsi_macd.go @@ -0,0 +1,50 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +func rsi_macd(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 100, + OnBar: func(s *strat.StratJob) { + e := s.Env + rsi := ta.RSI(e.Close, 2).Get(0) + sml := ta.SMA(e.Close, 5).Get(0) + big := ta.SMA(e.Close, 20).Get(0) + if len(s.LongOrders) > 0 { + for _, od := range s.LongOrders { + barNum := e.BarCount(od.EnterAt) + if barNum > 5 { + s.CloseOrders(&strat.ExitReq{ + Tag: "exitL", + OrderID: od.ID, + }) + } + } + } else if rsi <= 10 && sml > big { + s.OpenOrder(&strat.EnterReq{ + Tag: "long", + }) + } + if len(s.ShortOrders) > 0 { + for _, od := range s.ShortOrders { + barNum := e.BarCount(od.EnterAt) + if barNum > 5 { + s.CloseOrders(&strat.ExitReq{ + Tag: "exitS", + OrderID: od.ID, + }) + } + } + } else if rsi >= 90 && sml < big { + s.OpenOrder(&strat.EnterReq{ + Tag: "short", + Short: true, + }) + } + }, + } +} diff --git a/trend/smp1.go b/trend/smp1.go new file mode 100644 index 0000000..24dce27 --- /dev/null +++ b/trend/smp1.go @@ -0,0 +1,119 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/orm/ormo" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +测试仅catr时开多,持有一日平仓 +*/ + +func smp1(p *config.RunPolicyConfig) *strat.TradeStrat { + allowOpen := false + return &strat.TradeStrat{ + WarmupNum: 10, + RunTimeFrames: []string{"4h"}, + OnPairInfos: func(s *strat.StratJob) []*strat.PairSub { + return []*strat.PairSub{ + {"_cur_", "1d", 30}, + } + }, + OnSymbols: func(items []string) []string { + hasBTC := false + for _, it := range items { + if it == "BTC/USDT:USDT" { + hasBTC = true + } + } + if !hasBTC { + return append([]string{"BTC/USDT:USDT"}, items...) + } + return items + }, + OnStartUp: func(s *strat.StratJob) { + s.More = 0 + }, + OnBar: func(s *strat.StratJob) { + e := s.Env + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + doOpen := s.More.(int) + c := e.Close.Get(0) + o := e.Open.Get(0) + if len(s.LongOrders) > 0 { + slPrice := c - atr*3 + for _, od := range s.LongOrders { + holdNum := e.BarCount(od.EnterAt) + if holdNum < 8 { + continue + } + sl := od.GetStopLoss() + if sl == nil && (c-od.InitPrice < atr*3 || o >= c-atr*0.6) { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit", + OrderID: od.ID, + }) + } else if sl == nil || slPrice > sl.Price { + od.SetStopLoss(&ormo.ExitTrigger{ + Price: slPrice, + }) + } + } + } else if doOpen == 1 { + s.More = 0 + s.OpenOrder(&strat.EnterReq{ + Tag: "long1", + }) + } + }, + OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) { + if pair == "BTC/USDT:USDT" { + ma30 := ta.SMA(e.Close, 45).Get(0) + c := e.Close.Get(0) + allowOpen = c > ma30 + } + c := e.Close.Get(0) + h1 := e.High.Get(1) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + tr := ta.TR(e.High, e.Low, e.Close).Get(0) + ma := ta.SMA(e.Close, 14).Get(0) + if allowOpen && len(s.LongOrders) == 0 && c < h1-atr*0.4 && c > ma { + if tr > atr { + s.More = 1 + } + } + }, + } +} + +func smp0(p *config.RunPolicyConfig) *strat.TradeStrat { + return &strat.TradeStrat{ + WarmupNum: 30, + RunTimeFrames: []string{"1d"}, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + h1 := e.High.Get(1) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + tr := ta.TR(e.High, e.Low, e.Close).Get(0) + ma := ta.SMA(e.Close, 14).Get(0) + if len(s.LongOrders) > 0 { + s.CloseOrders(&strat.ExitReq{ + Tag: "exit", + Dirt: core.OdDirtLong, + }) + } else if c < h1-atr*0.4 && c > ma { + if tr > atr { + s.OpenOrder(&strat.EnterReq{ + Tag: "long1", + TakeProfitVal: atr * 2, + }) + } + } + }, + } +} diff --git a/trend/swing.go b/trend/swing.go new file mode 100644 index 0000000..eef332e --- /dev/null +++ b/trend/swing.go @@ -0,0 +1,98 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/core" + "github.com/banbox/banbot/strat" + ta "github.com/banbox/banta" +) + +/* +初版:https://jesse.trade/strategies/trendswingtrader-v1 +去掉e1c后,更不稳定 +adx对部分条件应用会更好,直接全部过滤略差 +加入海龟资金管理&ER开仓系数后表现略差 +*/ + +func swing(p *config.RunPolicyConfig) *strat.TradeStrat { + slRate := p.Def("slRate", 4.5, core.PNorm(0.5, 5)) + tpRate := p.Def("tpRate", 8, core.PNorm(3, 13)) + return &strat.TradeStrat{ + WarmupNum: 200, + OnBar: func(s *strat.StratJob) { + e := s.Env + c := e.Close.Get(0) + adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0) + atr := ta.ATR(e.High, e.Low, e.Close, 14).Get(0) + e1 := ta.EMA(e.Close, 30).Get(0) + e2 := ta.EMA(e.Close, 50).Get(0) + e3 := ta.EMA(e.Close, 120).Get(0) + openRate := float64(1) + trend := float64(0) + tag := "" + if e2 < e1 && e1 < c { + if e3 < e2 { + tag = "long1" + } else { + tag = "long2" + if adx <= 25 { + // 表现较差 + openRate *= 0.5 + } + } + trend = 1 + } else if e2 > e1 && e1 > c { + if e3 > e2 { + tag = "short1" + } else { + tag = "short2" + if adx <= 25 { + // 表现较差 + openRate *= 0.5 + } + } + trend = -1 + } + if tag != "" { + if adx <= 25 { + tag += "_a" + } + } + if len(s.LongOrders) > 0 { + for _, od := range s.LongOrders { + sl := od.GetStopLoss() + curSL := c - atr*slRate + if curSL > sl.Price { + sl.Price = curSL + } + } + } else if trend == 1 && tag != "" && openRate > 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + CostRate: openRate, + StopLossVal: atr * slRate, + TakeProfitVal: atr * tpRate, + TakeProfitRate: 0.5, + }) + } + if len(s.ShortOrders) > 0 { + for _, od := range s.ShortOrders { + sl := od.GetStopLoss() + curSL := c + atr*slRate + if curSL < sl.Price { + sl.Price = curSL + } + } + } else if trend == -1 && tag != "" && openRate > 0 { + s.OpenOrder(&strat.EnterReq{ + Tag: tag, + Short: true, + CostRate: openRate, + StopLossVal: atr * slRate, + TakeProfitVal: atr * tpRate, + TakeProfitRate: 0.5, + }) + } + }, + } +} diff --git a/trend/t01v1.go b/trend/t01v1.go new file mode 100644 index 0000000..093f6f2 --- /dev/null +++ b/trend/t01v1.go @@ -0,0 +1,84 @@ +package trend + +import ( + "github.com/banbox/banbot/config" + "github.com/banbox/banbot/strat" + "github.com/banbox/banstagy/com" + ta "github.com/banbox/banta" + "math" +) + +func calcDirt(e *ta.BarEnv) float64 { + h, l := e.High.Get(0), e.Low.Get(0) + o, c := e.Open.Get(0), e.Close.Get(0) + h1, l1 := e.High.Get(1), e.Low.Get(1) + //h2, l2 := e.High.Get(2), e.Low.Get(2) + + if h == l { + return 0 + } + bodyRate := math.Abs(o-c) / (h - l) + if bodyRate < 0.5 { + return 0 + } + + subH1, subL1 := h-h1, l-l1 + //subH2, subL2 := h1-h2, l1-l2 + if subH1 > 0 && subL1 > 0 { + //if math.IsNaN(subH2) || subH2 < 0 || subL2 < 0 { + // return 0 + //} + //if subL2+subH2 < subH1+subL1 { + // return 1 + //} + return 1 + } + //else if subH1 < 0 && subL1 < 0 { + // if math.IsNaN(subH2) || subH2 > 0 || subL2 > 0 { + // return 0 + // } + // if subL2+subH2 > subH1+subL1 { + // return -1 + // } + // return 0 + //} + return 0 +} + +func T01V1(pol *config.RunPolicyConfig) *strat.TradeStrat { + + lenAtr := 15 + baseAtrLen := int(math.Round(float64(lenAtr) * 4.3)) + return &strat.TradeStrat{ + Version: 1, + WarmupNum: 30, + MinTfScore: 0.75, + DrawDownExit: true, + OnStartUp: func(s *strat.StratJob) { + s.More = float64(0) + }, + //OnPairInfos: common.MakePairInfos(10), + OnBar: func(s *strat.StratJob) { + e := s.Env + ma := ta.SMA(e.Close, 10).Get(0) + + atr := ta.ATR(e.High, e.Low, e.Close, lenAtr) + atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0) + atrRate := math.Sqrt(atr.Get(0) / atrBase) + + //bigDirt, _ := s.More.(float64) + + c := e.Close.Get(0) + dirt := calcDirt(e) + maSub := c - ma + if s.OrderNum == 0 && dirt != 0 && maSub*dirt > 0 { // && bigDirt == dirt { + slVal := atrBase * atrRate * 0.2 + slLong, slShort := c-slVal, c+slVal + com.FastOpen(s, dirt, slLong, slShort, 0) + } + }, + //OnInfoBar: func(s *strategy.StratJob, e *ta.BarEnv, pair, tf string) { + // s.More = calcDirt(e) + //}, + } +}