scematica-core 1.11.4

Core types, traits, and shared utilities for Scematica — config, RPC, wallet, metrics, token helpers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;

/// Rate mode profile: defines entry size, TP%, SL% for a specific risk posture
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateMode {
    /// Human-readable name: "Micro", "Bearish", "Safe", "Balanced", "Aggressive", "Degen", "Moon"
    pub name: String,
    /// Menu order (1–7)
    pub order: u32,
    /// Entry amount in quote token (e.g. 0.01 SOL)
    pub quote_amount: f64,
    /// Take profit % for this mode
    pub take_profit_pct: f64,
    /// Stop loss % for this mode
    pub stop_loss_pct: f64,
    /// Max TP escalations allowed in momentum mode
    pub momentum_max_escalations: u32,
    /// Is this mode enabled?
    pub enabled: bool,
    /// Wallet % to use per trade (0.0 = use quote_amount instead)
    #[serde(default)]
    pub wallet_pct: f64,
}

/// Top-level bot configuration loaded from .env / config file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BotConfig {
    pub rpc: RpcConfig,
    pub wallet: WalletConfig,
    pub sniper: SniperConfig,
    pub arb: ArbConfig,
    pub execution: ExecutionConfig,
    #[serde(default)]
    pub alerts: AlertsConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcConfig {
    /// HTTPS RPC endpoint
    pub endpoint: String,
    /// WebSocket RPC endpoint
    pub ws_endpoint: String,
    /// Commitment level: "processed" | "confirmed" | "finalized"
    pub commitment: String,
}

impl Default for RpcConfig {
    fn default() -> Self {
        Self {
            endpoint: "https://api.mainnet-beta.solana.com".into(),
            ws_endpoint: "wss://api.mainnet-beta.solana.com".into(),
            commitment: "confirmed".into(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalletConfig {
    /// Path to keypair JSON file, or base58-encoded private key
    pub keypair_path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SniperConfig {
    pub enabled: bool,
    /// Quote token: "WSOL" or "USDC"
    pub quote_mint: String,
    /// Amount to spend per snipe (in quote token UI units, e.g. 0.1 SOL)
    pub quote_amount: f64,
    /// Delay in ms before buying after pool detection
    pub buy_delay_ms: u64,
    /// Max buy retries
    pub max_buy_retries: u32,
    /// Max sell retries
    pub max_sell_retries: u32,
    /// Auto-sell after buy
    pub auto_sell: bool,
    /// Delay in ms before auto-selling
    pub auto_sell_delay_ms: u64,
    /// Take profit % (e.g. 50.0 = 50%)
    pub take_profit_pct: f64,
    /// Stop loss % (e.g. 20.0 = 20%)
    pub stop_loss_pct: f64,
    /// Buy slippage %
    pub buy_slippage_pct: f64,
    /// Sell slippage %
    pub sell_slippage_pct: f64,
    /// Price check interval ms
    pub price_check_interval_ms: u64,
    /// Price check duration ms (how long to monitor before force-sell)
    pub price_check_duration_ms: u64,
    /// Only process one token at a time
    pub one_token_at_a_time: bool,
    /// Use snipe list (whitelist mode)
    pub use_snipe_list: bool,
    /// Path to snipe list file
    pub snipe_list_path: String,
    /// Auto-activate sell mode after this many successful buys (0 = unlimited)
    pub max_buys: u32,
    /// Trailing stop loss % from peak price (0 = disabled, uses fixed stop_loss_pct instead)
    pub trailing_stop_loss_pct: f64,
    /// Sell this % of position at partial_tp_trigger (0 = disabled)
    pub partial_tp_pct: f64,
    /// Price gain % that triggers partial take profit
    pub partial_tp_trigger: f64,
    /// Max open positions at once (0 = unlimited)
    pub max_concurrent_positions: u32,
    /// Activate cooldown after N consecutive losses (0 = disabled)
    pub cooldown_after_losses: u32,
    /// Cooldown duration in minutes
    pub cooldown_minutes: u32,
    /// Halt buying if daily losses exceed this SOL amount (0.0 = disabled)
    pub daily_loss_limit_sol: f64,
    /// Activate sell mode if wallet drops below this % of session starting balance (0.0 = disabled)
    pub max_drawdown_pct: f64,
    /// Path to dev wallet blacklist file (one pubkey per line)
    pub blacklist_path: String,
    /// Wallet addresses to copy-trade (buy same tokens they buy)
    pub copy_wallets: Vec<String>,
    pub filters: FilterConfig,

    // ── Rate Modes: 7 trading profiles with individual entry sizes and TP/SL ──
    /// 7 rate mode profiles: Micro, Bearish, Safe, Balanced, Aggressive, Degen, Moon
    pub rate_modes: Vec<RateMode>,
    /// Currently active rate mode name (e.g., "Balanced")
    pub active_mode_name: String,

    // ── Kelly position sizing ─────────────────────────────────────────────────
    /// Enable Kelly Criterion position sizing
    pub kelly_sizing: bool,
    /// Fraction of Kelly formula to apply (0.25 = quarter-Kelly)
    pub kelly_fraction: f64,
    /// Number of recent trades to use for Kelly estimate
    pub kelly_lookback: usize,

    // ── Gas war mode ─────────────────────────────────────────────────────────
    /// Escalate compute unit price when multiple pools are detected rapidly
    pub gas_war_mode: bool,
    /// Maximum compute unit price in micro-lamports during gas wars
    pub gas_war_max_cu_price: u64,

    // ── ATH drawdown watermark ────────────────────────────────────────────────
    /// Pause buying if wallet drops this % below all-time-high balance (0.0 = disabled)
    pub ath_drawdown_pct: f64,

    // ── Grief-loss circuit breaker ────────────────────────────────────────────
    /// Sliding window size in seconds for grief-loss calculation
    pub grief_loss_window_secs: u64,
    /// Halt buying if losses in the window exceed this SOL amount (0.0 = disabled)
    pub grief_loss_limit_sol: f64,

    // ── Pump.fun trending monitor ─────────────────────────────────────────────
    /// Enable the PumpPortal WebSocket trending monitor.
    /// Tracks per-token buy/sell velocity and emits graduating tokens ahead of
    /// the standard Raydium AMM V4 listener when they show strong buy momentum.
    #[serde(default)]
    pub pumpfun_trending_enabled: bool,
    /// Minimum trending score (0–100) for a token to be considered "trending".
    /// Score = buy_pressure(40) + volume_velocity(30) + curve_fill(30).
    /// Default: 55.0
    #[serde(default = "default_pumpfun_trending_score")]
    pub pumpfun_trending_score: f64,
    /// Minimum bonding curve fill % before a token is flagged as trending.
    /// Prevents entering tokens with very little traction (< 28 SOL raised).
    /// Default: 40.0
    #[serde(default = "default_pumpfun_min_curve_pct")]
    pub pumpfun_min_curve_pct: f64,
    /// Sliding window in seconds for the trending score calculation. Default: 120
    #[serde(default = "default_pumpfun_window_secs")]
    pub pumpfun_window_secs: u64,
    /// Maximum age from first Pump.fun observation to migration before the early
    /// migration path skips it. The normal Raydium listener can still evaluate it.
    #[serde(default = "default_pumpfun_max_migration_age_secs")]
    pub pumpfun_max_migration_age_secs: u64,
    /// Require at least this many recent buys in the Pump.fun sliding window.
    #[serde(default = "default_pumpfun_min_recent_buys")]
    pub pumpfun_min_recent_buys: u32,
    /// Require recent buy volume minus sell volume to exceed this SOL amount.
    #[serde(default = "default_pumpfun_min_net_buy_sol")]
    pub pumpfun_min_net_buy_sol: f64,
    /// Last buy must be this recent at migration time.
    #[serde(default = "default_pumpfun_max_last_buy_age_secs")]
    pub pumpfun_max_last_buy_age_secs: u64,

    // ── Time-of-day weighting ─────────────────────────────────────────────────
    /// Scale position size based on UTC trading hour activity
    pub time_of_day_weighting: bool,
    /// UTC hours (0–23) where buys are completely blocked — skip the pool entirely.
    /// Distinct from time_of_day_weighting (which just scales size).
    /// Calibrated from live data: hours 1 and 21 UTC show 0% win rate.
    #[serde(default)]
    pub blocked_hours_utc: Vec<u8>,
    /// Rate mode to auto-switch to on Saturday and Sunday UTC (empty = no auto-switch).
    /// Live data shows 0% WR on Saturday. E.g., "Bearish" or "Micro".
    #[serde(default)]
    pub weekend_mode: String,
    /// Rate mode to restore Monday–Friday when weekend_mode is active (empty = "Balanced").
    #[serde(default)]
    pub weekday_mode: String,

    // ── Profit extraction ─────────────────────────────────────────────────────
    /// Automatically extract profits when session PnL exceeds this SOL (0.0 = disabled)
    pub profit_extraction_threshold_sol: f64,
    /// Percentage of profit to extract on each sweep (0.0 = disabled)
    pub profit_extraction_pct: f64,
    /// Cold wallet address to receive extracted profits
    pub profit_extraction_wallet: String,

    // ── Pool predictive scoring ────────────────────────────────────────────────
    /// Minimum pool score (0–100) required to proceed with a buy (0.0 = disabled)
    pub min_pool_score: f64,
    /// Require a pool to show moonshot confirmation before buying.
    /// Confirmation means live vault inflow, strong pump.fun pre-graduation momentum,
    /// or real historical AMM velocity. This intentionally lowers trade frequency.
    #[serde(default)]
    pub require_moonshot_confirmation: bool,
    /// Minimum final pool score when moonshot confirmation is required.
    #[serde(default = "default_moonshot_min_score")]
    pub moonshot_min_score: f64,
    /// Minimum measured quote-vault inflow rate in SOL/s for a live moonshot signal.
    #[serde(default = "default_moonshot_min_inflow_sol_per_sec")]
    pub moonshot_min_inflow_sol_per_sec: f64,
    /// Minimum quote-vault growth percentage between reserve snapshots.
    #[serde(default = "default_moonshot_min_growth_pct")]
    pub moonshot_min_growth_pct: f64,
    /// Minimum absolute quote-vault growth in SOL between reserve snapshots.
    #[serde(default = "default_moonshot_min_growth_sol")]
    pub moonshot_min_growth_sol: f64,
    /// Minimum pump.fun pre-graduation score that qualifies as moonshot momentum.
    #[serde(default = "default_moonshot_min_pumpfun_score")]
    pub moonshot_min_pumpfun_score: f64,
    /// Minimum historical AMM velocity in SOL/s when pool open_time is trustworthy.
    #[serde(default = "default_moonshot_min_historical_velocity_sol_per_sec")]
    pub moonshot_min_historical_velocity_sol_per_sec: f64,

    /// Final fail-closed entry gate for "best of best" pool selection.
    /// When enabled, a pool must clear a high score floor and show multiple
    /// independent runner signals before buy execution can start.
    #[serde(default)]
    pub elite_pool_mode: bool,
    /// Minimum final pool score required by elite mode.
    #[serde(default = "default_elite_min_score")]
    pub elite_min_score: f64,
    /// Minimum number of independent runner signals required by elite mode.
    #[serde(default = "default_elite_min_signal_count")]
    pub elite_min_signal_count: u8,
    /// Minimum quote-vault size in SOL required by elite mode.
    #[serde(default = "default_elite_min_pool_size_sol")]
    pub elite_min_pool_size_sol: f64,
    /// Maximum quote-vault size in SOL allowed by elite mode. 0 disables max.
    #[serde(default = "default_elite_max_pool_size_sol")]
    pub elite_max_pool_size_sol: f64,
    /// Maximum trustworthy pool age in seconds. Unknown ages can pass only when
    /// live or pre-graduation momentum supplies the freshness signal.
    #[serde(default = "default_elite_max_age_secs")]
    pub elite_max_age_secs: u64,
    /// Live quote-vault inflow in SOL/s required to count as a runner signal.
    #[serde(default = "default_elite_min_inflow_sol_per_sec")]
    pub elite_min_inflow_sol_per_sec: f64,
    /// Pump.fun pre-graduation score required to count as a runner signal.
    #[serde(default = "default_elite_min_pumpfun_score")]
    pub elite_min_pumpfun_score: f64,
    /// Historical AMM velocity in SOL/s required to count as a runner signal.
    #[serde(default = "default_elite_min_historical_velocity_sol_per_sec")]
    pub elite_min_historical_velocity_sol_per_sec: f64,
    /// AMM quote/base vault skew required to count as a buy-pressure signal.
    #[serde(default = "default_elite_min_buy_pressure_ratio")]
    pub elite_min_buy_pressure_ratio: f64,
    /// Require either live inflow or pre-graduation momentum. This prevents
    /// historical-only or paid-boost-only pools from passing as elite runners.
    #[serde(default = "default_elite_require_live_or_pregrad")]
    pub elite_require_live_or_pregrad: bool,

    // ── Multi-RPC endpoints ────────────────────────────────────────────────────
    /// Additional RPC endpoints for automatic failover
    pub extra_rpc_endpoints: Vec<String>,

    // ── Adaptive slippage ─────────────────────────────────────────────────────
    /// Automatically adjust slippage based on recent sell success rates
    pub adaptive_slippage: bool,

    // ── Sandwich shield ───────────────────────────────────────────────────────
    /// Switch to Jito bundle routing when front-running is detected
    pub sandwich_shield: bool,

    // ── Momentum-aware long-term sniping ──────────────────────────────────────
    /// Enable momentum-driven TP escalation + pullback-from-peak exit. When ON,
    /// the sell monitor tracks the velocity of recent price checks: positions
    /// that hit their TP target with strong upward momentum get their TP raised
    /// instead of exiting, and positions in significant profit exit on a sharp
    /// pullback from peak rather than waiting for the SL trigger. This biases
    /// the bot toward "let winners run" — small wins get bumped into big ones
    /// when the market cooperates. Default ON.
    pub momentum_hold: bool,
    /// Number of recent price-check deltas averaged into the velocity signal.
    /// Default 5 — small enough that recent momentum dominates, large enough
    /// that single jittery checks don't flip the decision.
    pub momentum_window_checks: u32,
    /// Minimum average velocity (% of entry per check) required to trigger a TP
    /// escalation. Higher = more conservative (only the strongest movers ride).
    /// Default 5.0 — works out to roughly +5 % per ~250 ms tick during a real pump.
    pub momentum_escalation_threshold_pct: f64,
    /// Multiplier applied to the current TP target each time momentum escalates.
    /// 1.5 means TP grows by 50 % per escalation: 100 % → 150 % → 225 % → 337 %.
    /// Default 1.5.
    pub momentum_escalation_factor: f64,
    /// Maximum number of TP escalations allowed for a single position — caps the
    /// "let it ride" greed so the bot still books eventually. Default 4 (= TP
    /// can grow up to 1.5^4 = 5.06× the configured target).
    pub momentum_max_escalations: u32,
    /// Position only exits via pullback-from-peak when realised peak gain has
    /// exceeded this percentage of entry. Below this, normal TP/SL logic runs.
    /// Default 20.0 — avoids triggering on noise before the trade has run.
    pub momentum_min_peak_pct: f64,
    /// Pullback from peak that fires the lock-in exit. 15 % = "you were up 200 %,
    /// now you're up 170 %, get out before this round-trips." Default 15.0.
    pub momentum_pullback_exit_pct: f64,

    // ── Perfect-exit timing (v0.9.6) ──────────────────────────────────────────
    /// Adaptive pullback threshold scales with peak height — bigger winners
    /// get more room to breathe before the pullback exit fires. Formula:
    ///   pullback_θ(peak) = base × sqrt(1 + peak/100)
    /// At peak=20% → 1.10×.  At peak=100% → 1.41×.  At peak=500% → 2.45×.
    /// Default ON. Disable to use the flat threshold for all positions.
    pub adaptive_pullback: bool,

    /// Velocity-decay exit catches the *inflection point* — the moment momentum
    /// starts dying — before price actually reverses. Compares the average
    /// velocity over the most recent N checks vs the previous N checks; if the
    /// signal has flipped from positive-accelerating to positive-decelerating
    /// AND we're already in profit, exit. This is the "perfect exit" trigger.
    /// Default ON.
    pub velocity_decay_exit: bool,
    /// Number of checks per half-window for the decay comparison. Total memory
    /// = 2N. Default 3 (compares last 3 vs previous 3 = 6 checks of history,
    /// ~1.5 s of price action at 250 ms polling).
    pub velocity_decay_window: u32,
    /// Velocity decay only fires when current pnl is above this %. Avoids
    /// exiting on noise during the early "filling out" phase of a position.
    /// Default 10.0 — by the time we're up 10 %, momentum signal is meaningful.
    pub velocity_decay_min_pnl_pct: f64,
    /// Minimum velocity-drop magnitude (in % of entry per check) required to
    /// trigger the decay exit. Filters out micro-flutters. Default 2.0 = the
    /// velocity must have fallen by at least 2 percentage points across the
    /// window for the exit to fire.
    pub velocity_decay_drop_threshold: f64,

    /// Tiered partial-TP ladder: instead of one partial sell at
    /// `partial_tp_trigger`, sell incrementally at multiple gain levels. Locks
    /// the median win automatically while leaving capital for the escalator.
    /// Default ON; disable to fall back to the single partial_tp_pct.
    pub tiered_partial_tp: bool,
    /// Ladder of (trigger_pct, sell_pct) tuples. Sells `sell_pct` of REMAINING
    /// position when current_pnl_pct first crosses `trigger_pct`. Default:
    ///   +30 % → sell 25 %    (capture median win)
    ///   +75 % → sell 25 % more   (lock first multiple)
    ///   +150 % → sell 25 % more  (lock 2x return)
    ///   remainder rides to escalator / pullback exit
    pub tiered_partial_tp_levels: Vec<(f64, f64)>,

    // ── Profit-first growth mode ──────────────────────────────────────────────
    /// While the wallet is below `wallet_target_sol`, refuse stop-loss exits
    /// (using `profit_first_floor_pct` as a rug-only safety net) and only
    /// close positions in profit. Once the wallet reaches the target, normal
    /// SL behavior resumes. This is the "establish profits first" doctrine —
    /// it accepts longer drawdowns in individual positions to bias toward
    /// realising wins before tolerating any losses.
    pub profit_first_mode: bool,
    /// Wallet size in SOL we're trying to build to. Below this, profit-first
    /// is active; at/above, normal SL applies. Default 0.2 SOL.
    pub wallet_target_sol: f64,
    /// Rug-only safety net in profit-first mode: still exit if value drops
    /// this far below entry, even though regular SL is gated. Default 50%.
    pub profit_first_floor_pct: f64,

    // ── Flash-crash protection ────────────────────────────────────────────────
    /// If the position value drops more than this % from its peak in a SINGLE
    /// price-check interval (≥75 ms), exit immediately without waiting for the
    /// 3-consecutive-decline counter. Catches vertical dumps that the streak
    /// detector would miss during the slow normal-interval phase.
    /// Default 22.0.  Set 0.0 to disable.
    pub flash_crash_pct: f64,

    // ── Profit lock ───────────────────────────────────────────────────────────
    /// After the position stays above breakeven for this many CONSECUTIVE price
    /// checks, raise the stop-loss floor to near-breakeven (entry × 0.98) to
    /// ensure a winning position never turns into a significant loss. Activates
    /// independently of partial-TP — useful for slow steady movers that never
    /// hit the first tier quickly but stay green for many checks. Default 8.
    pub profit_lock_checks: u32,

    // ── ATA cleanup ───────────────────────────────────────────────────────────
    /// Close the base-token ATA after a full sell to reclaim the 0.002 SOL rent.
    /// Sent as a separate fire-and-forget transaction after the sell confirms.
    /// Default true.
    pub close_ata_on_sell: bool,

    // ── Hard position time cap ────────────────────────────────────────────────
    /// Force-sell any position that has been open longer than this many minutes,
    /// regardless of profit-first extension logic. Prevents capital from being
    /// permanently locked in a dead pool. 0 = disabled. Default 90.
    pub max_position_hold_mins: u32,

    // ── Absolute SOL floor ────────────────────────────────────────────────────
    /// Never enter a trade if the wallet's SOL balance would drop below this
    /// after the buy. Reserves gas money so sells never fail due to empty wallet.
    /// Default 0.02 SOL.
    pub min_sol_reserve: f64,

    // ── Confirmation window ───────────────────────────────────────────────────
    /// After detecting a pool, wait this many ms and re-check vault balance.
    /// If price has already moved >15% from creation price, skip the buy — we
    /// are entering after the initial pump. 0 = disabled. Default 0.
    pub confirmation_window_ms: u64,

    // ── Session heat cooldown ─────────────────────────────────────────────────
    /// Pause buying for `session_heat_cooldown_mins` after this many losses in
    /// `session_heat_window_secs`. Distinct from grief_breaker (which is based on
    /// SOL magnitude); this fires on *frequency*. 0 = disabled. Default 3.
    pub session_heat_losses: u32,
    /// Rolling window in seconds for session heat tracking. Default 3600.
    pub session_heat_window_secs: u64,
    /// Pause duration in minutes when session heat is tripped. Default 15.
    pub session_heat_cooldown_mins: u32,

    // ── Volume exhaustion exit ────────────────────────────────────────────────
    /// Exit the position when observed volume drops to this % of the entry-time
    /// volume. The pump is over when volume dries up. 0.0 = disabled.
    pub volume_exhaustion_pct: f64,

    // ── Buy/sell ratio inversion exit ─────────────────────────────────────────
    /// Exit immediately when the buy/sell ratio drops below this threshold after
    /// having been above it at entry. Net sellers dominating = distribution phase.
    /// 0.0 = disabled. Default 0.0.
    pub buy_sell_ratio_exit: f64,

    // ── Check interval acceleration ───────────────────────────────────────────
    /// When 3 consecutive price checks are all down, halve the check interval
    /// (minimum 25 ms). Re-expands to normal when price stabilises. Default true.
    pub check_interval_acceleration: bool,

    // ── Whale exit detector ───────────────────────────────────────────────────
    /// If the pool's quote vault drops by more than this % in a single price-check
    /// cycle, trigger an immediate sell — a large holder exited. 0.0 = disabled.
    pub whale_exit_vault_drop_pct: f64,

    // ── Pool-quality buy sizing ───────────────────────────────────────────────
    /// Scale buy size by pool_score/100. Score 95 → full size, score 55 → 55%.
    /// Requires min_pool_score > 0 to be meaningful. Default false.
    pub pool_quality_sizing: bool,

    // ── Kelly warm-up ─────────────────────────────────────────────────────────
    /// Minimum number of historical trades before Kelly sizing activates. Below
    /// this the multiplier is 0.5× (half base) to avoid wild early sizing.
    /// Default 10.
    pub kelly_min_trades: usize,

    // ── Deployer wallet age filter ────────────────────────────────────────────
    /// Reject pools from deployer wallets younger than this many hours.
    /// Fresh wallets almost always belong to rug setups. 0 = disabled.
    pub deployer_wallet_age_min_hours: u64,

    // ── Filter TTL cache ──────────────────────────────────────────────────────
    /// Cache filter pipeline results per pool pubkey for this many seconds to
    /// avoid repeated RPC calls when the same pool fires multiple events.
    /// Default 30.
    pub filter_cache_ttl_secs: u64,

    // ── Dead-zone early exit ──────────────────────────────────────────────────
    /// Exit a position if no meaningful upward momentum has been observed within
    /// this many seconds of entry. Live data: winning pump.fun trades exit within
    /// 6 s; positions still flat after 45 s are statistically dead capital.
    /// 0 = disabled. Default 45.
    pub no_pump_timeout_secs: u64,
    /// Minimum peak gain % that must have been seen to suppress the dead-zone
    /// exit. If the position's best price ever seen is below this threshold after
    /// `no_pump_timeout_secs`, exit at market to recycle capital.
    /// Default 3.0.
    pub no_pump_min_gain_pct: f64,

    // ── Dump-mode fresh-position protection ──────────────────────────────────
    /// If dump_mode fires but NOT sell_mode, protect positions younger than this
    /// many seconds — let them run through normal TP/SL instead of force-selling
    /// at min_out=0. Prevents dump mode from destroying a freshly-entered position
    /// mid-pump. 0 = no protection (dump all immediately). Default 0.
    pub min_dump_hold_secs: u64,

    // ── Mint re-entry cooldown ────────────────────────────────────────────────
    /// After buying a mint, skip it for this many seconds on any subsequent pool
    /// event. Persisted to `scematica-mint-cooldown.json` so it survives restarts.
    /// Live data: same losing pools were re-entered 3× in 26 min after restarts.
    /// Default 1800 (30 min).
    pub mint_cooldown_secs: u64,

    // ── AI chain pool selection ────────────────────────────────────────────────
    /// 3-layer LLM chain for pool screening (Groq → OpenRouter → Cerebras).
    /// When enabled, pools must pass all 3 layers to proceed to buy.
    /// Falls back to pool_scorer if any API key is missing or times out.
    #[serde(default)]
    pub ai_chain: AiChainConfig,

    // ── Peak stagnation exit ──────────────────────────────────────────────────
    /// Exit if peak gain hasn't improved in this many seconds AND current pnl
    /// exceeds `peak_stagnation_min_pnl_pct`. Catches flat pools that pumped once
    /// then stopped — they bleed slowly back while capital sits idle.
    /// 0 = disabled. Default 90.
    pub peak_stagnation_secs: u64,
    /// Minimum current pnl % required to arm the stagnation exit.
    /// Prevents exiting breakeven positions too early during normal chop.
    /// Default 20.0.
    pub peak_stagnation_min_pnl_pct: f64,

    // ── Runner detection ──────────────────────────────────────────────────────
    /// When true, pools scoring ≥ 98 (ultra-fresh ≤7 s AND sweet-spot 6.5–28 SOL)
    /// get a position sized at `runner_scale_in_sol` instead of `quote_amount`.
    /// Score=98 is the highest-conviction pool profile in live data — the bot should
    /// bet more on these than on borderline-qualifying pools (score 95–97). Default false.
    pub runner_mode: bool,
    /// Buy size in SOL for max-conviction (score ≥ 98) pools when runner_mode is on.
    /// Typically 2–4× quote_amount. Default 0.02.
    pub runner_scale_in_sol: f64,
    /// If the pool's quote vault falls below this % of its balance at position entry,
    /// exit immediately — the pool is actively draining (rug in progress).
    /// 0.0 = disabled. Default 15.0 (pool retains < 15 % of entry liquidity = drain).
    pub pool_drain_exit_pct: f64,
}

fn default_pumpfun_trending_score() -> f64 {
    55.0
}
fn default_pumpfun_min_curve_pct() -> f64 {
    40.0
}
fn default_pumpfun_window_secs() -> u64 {
    120
}
fn default_pumpfun_max_migration_age_secs() -> u64 {
    120
}
fn default_pumpfun_min_recent_buys() -> u32 {
    3
}
fn default_pumpfun_min_net_buy_sol() -> f64 {
    0.25
}
fn default_pumpfun_max_last_buy_age_secs() -> u64 {
    20
}
fn default_moonshot_min_score() -> f64 {
    85.0
}
fn default_moonshot_min_inflow_sol_per_sec() -> f64 {
    0.50
}
fn default_moonshot_min_growth_pct() -> f64 {
    2.0
}
fn default_moonshot_min_growth_sol() -> f64 {
    0.01
}
fn default_moonshot_min_pumpfun_score() -> f64 {
    85.0
}
fn default_moonshot_min_historical_velocity_sol_per_sec() -> f64 {
    2.0
}
fn default_elite_min_score() -> f64 {
    92.0
}
fn default_elite_min_signal_count() -> u8 {
    2
}
fn default_elite_min_pool_size_sol() -> f64 {
    20.0
}
fn default_elite_max_pool_size_sol() -> f64 {
    100.0
}
fn default_elite_max_age_secs() -> u64 {
    30
}
fn default_elite_min_inflow_sol_per_sec() -> f64 {
    0.75
}
fn default_elite_min_pumpfun_score() -> f64 {
    90.0
}
fn default_elite_min_historical_velocity_sol_per_sec() -> f64 {
    2.618
}
fn default_elite_min_buy_pressure_ratio() -> f64 {
    // Calibrated for pump.fun meme tokens: quote_lamports/base_lamports ≈ 4e-5 at launch.
    // Old value (0.02) was never reachable for any meme token — all pools were treated
    // as having no buy pressure. New value triggers for pools showing 2× launch ratio.
    0.0001
}
fn default_elite_require_live_or_pregrad() -> bool {
    true
}
fn default_compute_unit_price_hard_cap() -> u64 {
    2_000_000
}

impl Default for SniperConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            quote_mint: "WSOL".into(),
            quote_amount: 0.1,
            buy_delay_ms: 0,
            max_buy_retries: 3,
            // 3 inner retries × 4 outer rounds = 12 total tx attempts per position.
            // Was 5 (= 20 total, up to 30s per round on timeouts).
            max_sell_retries: 3,
            auto_sell: true,
            auto_sell_delay_ms: 0,
            // v1.4.0: raised 80→175 — at 80% the TP fired on the FIRST price check
            // (~75ms) before the 5-sample momentum window filled, so the escalation
            // system never ran. At 175% the base TP is a fallback; the pullback and
            // velocity-decay exits are the primary signals. Data: most wins at 99-198%
            // were first-check exits, not momentum-held exits. Higher base = escalation
            // runs = bot rides parabolic moves instead of capping at initial pump.
            take_profit_pct: 175.0,
            // v1.4.0: widened 15→18 — profit_first_floor (25%) already handles rugs;
            // the SL here catches legitimate reversals without stopping out on the
            // normal initial volatility of new pools.
            stop_loss_pct: 18.0,
            // v1.1.1: widened 1.5→3.0 — thin memecoin pools move 3-5% between pool
            // detection and tx landing. 1.5% caused frequent buy rejections with 0x26
            // errors on the first attempt, burning all 3 retries on pools that were
            // still buyable. 3.0% accepts the same buy in one shot.
            buy_slippage_pct: 3.0,
            // v1.0.0: widened 2.0→2.5 — memecoin pools are thin; a 2% slippage
            // floor causes frequent sell rejections during high-volatility moves.
            sell_slippage_pct: 2.5,
            // Tighter price polling — the sell monitor now fans out the 3 balance reads
            // concurrently, so 250 ms is achievable without overloading a paid RPC.
            price_check_interval_ms: 250,
            // v0.9.0: extended from 120s → 900s (15 min) so the momentum-hold
            // escalator has room to let strong winners run. The profit-first hold
            // extension can stretch this up to 6× if the position recovers from a
            // shallow loss.
            price_check_duration_ms: 900_000,
            one_token_at_a_time: true,
            use_snipe_list: false,
            snipe_list_path: "snipe-list.txt".into(),
            max_buys: 0,
            // v1.4.0: kept at 12 — trailing stop is the backstop for tokens that
            // keep climbing past the pullback threshold. Tightening below 12 would
            // conflict with the adaptive pullback formula at modest peak heights.
            trailing_stop_loss_pct: 12.0,
            partial_tp_pct: 50.0,
            // Legacy single partial — unused when tiered_partial_tp is on.
            partial_tp_trigger: 100.0,
            max_concurrent_positions: 0,
            cooldown_after_losses: 3,
            cooldown_minutes: 15,
            daily_loss_limit_sol: 0.0,
            max_drawdown_pct: 0.0,
            blacklist_path: "blacklist.txt".into(),
            copy_wallets: vec![],
            filters: FilterConfig::default(),
            // v1.6.0: Rate modes — 7 profiles from Micro (0.001 SOL) to Moon (0.1 SOL)
            // Default to Balanced (0.01 SOL, 40% win rate from Phase 1 analysis)
            rate_modes: vec![
                RateMode {
                    name: "Micro".into(),
                    order: 1,
                    quote_amount: 0.001,
                    wallet_pct: 0.3,
                    take_profit_pct: 50.0,
                    stop_loss_pct: 8.0,
                    momentum_max_escalations: 3,
                    enabled: true,
                },
                RateMode {
                    name: "Bearish".into(),
                    order: 2,
                    quote_amount: 0.003,
                    wallet_pct: 0.5,
                    take_profit_pct: 75.0,
                    stop_loss_pct: 10.0,
                    momentum_max_escalations: 4,
                    enabled: true,
                },
                RateMode {
                    name: "Safe".into(),
                    order: 3,
                    quote_amount: 0.005,
                    wallet_pct: 0.8,
                    take_profit_pct: 100.0,
                    stop_loss_pct: 12.0,
                    momentum_max_escalations: 5,
                    enabled: true,
                },
                RateMode {
                    name: "Balanced".into(),
                    order: 4,
                    quote_amount: 0.01,
                    wallet_pct: 1.5,
                    take_profit_pct: 175.0,
                    stop_loss_pct: 12.0,
                    momentum_max_escalations: 7,
                    enabled: true,
                },
                RateMode {
                    name: "Aggressive".into(),
                    order: 5,
                    quote_amount: 0.02,
                    wallet_pct: 3.0,
                    take_profit_pct: 300.0,
                    stop_loss_pct: 15.0,
                    momentum_max_escalations: 7,
                    enabled: true,
                },
                RateMode {
                    name: "Degen".into(),
                    order: 6,
                    quote_amount: 0.04,
                    wallet_pct: 6.0,
                    take_profit_pct: 450.0,
                    stop_loss_pct: 25.0,
                    momentum_max_escalations: 7,
                    enabled: true,
                },
                RateMode {
                    name: "Moon".into(),
                    order: 7,
                    quote_amount: 0.1,
                    wallet_pct: 12.0,
                    take_profit_pct: 100000.0,
                    stop_loss_pct: 60.0,
                    momentum_max_escalations: 12,
                    enabled: true,
                },
            ],
            active_mode_name: "Balanced".to_string(),
            kelly_sizing: false,
            kelly_fraction: 0.25,
            kelly_lookback: 20,
            gas_war_mode: false,
            gas_war_max_cu_price: 2_000_000,
            ath_drawdown_pct: 0.0,
            grief_loss_window_secs: 300,
            grief_loss_limit_sol: 0.0,
            time_of_day_weighting: false,
            profit_extraction_threshold_sol: 0.0,
            profit_extraction_pct: 0.0,
            profit_extraction_wallet: String::new(),
            // v1.2.0: raised 25→35. The 2 SOL min_pool_size and check_name filter
            // already eliminated the worst pools; raising the score gate here
            // focuses on pools with both fresh age AND sufficient liquidity.
            // Moderate: operators willing to take more micro-cap risk can lower
            // to 25 in config.toml.
            min_pool_score: 35.0,
            require_moonshot_confirmation: false,
            moonshot_min_score: default_moonshot_min_score(),
            moonshot_min_inflow_sol_per_sec: default_moonshot_min_inflow_sol_per_sec(),
            moonshot_min_growth_pct: default_moonshot_min_growth_pct(),
            moonshot_min_growth_sol: default_moonshot_min_growth_sol(),
            moonshot_min_pumpfun_score: default_moonshot_min_pumpfun_score(),
            moonshot_min_historical_velocity_sol_per_sec:
                default_moonshot_min_historical_velocity_sol_per_sec(),
            elite_pool_mode: false,
            elite_min_score: default_elite_min_score(),
            elite_min_signal_count: default_elite_min_signal_count(),
            elite_min_pool_size_sol: default_elite_min_pool_size_sol(),
            elite_max_pool_size_sol: default_elite_max_pool_size_sol(),
            elite_max_age_secs: default_elite_max_age_secs(),
            elite_min_inflow_sol_per_sec: default_elite_min_inflow_sol_per_sec(),
            elite_min_pumpfun_score: default_elite_min_pumpfun_score(),
            elite_min_historical_velocity_sol_per_sec:
                default_elite_min_historical_velocity_sol_per_sec(),
            elite_min_buy_pressure_ratio: default_elite_min_buy_pressure_ratio(),
            elite_require_live_or_pregrad: default_elite_require_live_or_pregrad(),
            extra_rpc_endpoints: vec![],
            adaptive_slippage: false,
            sandwich_shield: false,
            // Profit-first defaults: ON, build toward 0.15 SOL, rug-only floor at -25%.
            // Operators who want classic SL behavior can set `profit_first_mode = false`.
            // v1.3.0: lowered target 0.2→0.15 so the mode disengages faster and
            // normal SL resumes sooner, protecting the accumulated gains.
            // v1.3.1: tightened floor 50→25% — exit rugs faster instead of holding
            // a near-zero token hoping for a recovery that statistically won't come.
            profit_first_mode: true,
            wallet_target_sol: 0.15,
            profit_first_floor_pct: 25.0,
            // Momentum-aware long-term sniping: ON by default. The escalation +
            // pullback exit replaces fixed-TP greed with "ride strong winners,
            // lock when they cool". See the field comments above for tuning.
            momentum_hold: true,
            momentum_window_checks: 5,
            // v1.4.0: lowered 5→3%/check — easier to trigger escalation so the
            // bot rides moderate momentum, not just parabolic outliers.
            momentum_escalation_threshold_pct: 3.0,
            // v1.4.0: raised 1.6→1.8 — more aggressive TP target per escalation.
            // Ladder: 175 → 315 → 567 → 1020 → 1836 → 3305 → 5949% (7 rounds max).
            momentum_escalation_factor: 1.8,
            // v1.4.0: raised 5→7 rounds — lets truly parabolic movers run further
            // without a forced TP exit; the pullback exit locks gains on reversal.
            momentum_max_escalations: 7,
            // v1.4.0: raised 25→60 — require a real peak before the pullback exit
            // fires. At 25% the pullback was triggering on normal early-position
            // volatility before the position had time to develop into a winner.
            momentum_min_peak_pct: 60.0,
            // v1.4.0: tightened 18→8 (base). Despite the lower base, the adaptive
            // formula θ_eff = 8 × √(1 + peak/100) scales up with peak height:
            //   peak=60%  → 10.1 PnL pts → exits at 49.9%  (was 20.1 → exits at 4.9%)
            //   peak=100% → 11.3 PnL pts → exits at 88.7%  (was 25.5 → exits at 74.5%)
            //   peak=200% → 13.9 PnL pts → exits at 186.1% (was 31.2 → exits at 168.8%)
            //   peak=500% → 19.6 PnL pts → exits at 480.4% (was 49.2 → exits at 450.8%)
            // Net effect: locks in gains at HIGHER levels than before on every peak size.
            momentum_pullback_exit_pct: 8.0,
            // v0.9.6 perfect-exit defaults
            adaptive_pullback: true,
            velocity_decay_exit: true,
            velocity_decay_window: 3,
            // v1.4.0: raised 7→25 — at 7% the velocity-decay exit was triggering
            // on tokens that barely covered fees. Requires a genuine 25% gain before
            // the momentum-inflection signal can fire an exit.
            velocity_decay_min_pnl_pct: 25.0,
            // v1.4.0: loosened 1.2→1.5 — 1.2 was too sensitive on thin pools where
            // normal price chop looks like velocity decay. 1.5 requires a sharper
            // inflection before exiting.
            velocity_decay_drop_threshold: 1.5,
            tiered_partial_tp: true,
            // v1.4.0: All levels shifted up; fractions reduced to stay invested longer.
            // Old: (45%,20%) (100%,25%) (200%,25%) — first partial too early at 45%.
            // Data shows tokens commonly pump 100-300% on launch; selling 20% at 45%
            // was giving up upside on every strong winner.
            tiered_partial_tp_levels: vec![
                (100.0, 15.0), // first partial at +100% — sell 15% (was 45%→20%)
                (300.0, 20.0), // second at +300% — sell 20% (was 100%→25%)
                (600.0, 25.0), // third at +600% — sell 25% (was 200%→25%)
            ],
            // v1.0.0 reliability defaults
            flash_crash_pct: 22.0,
            // v1.2.0: lowered 8→6 — lock profits sooner once we're consistently
            // above entry (prevents round-trip losses on sustained winners)
            profit_lock_checks: 6,
            close_ata_on_sell: true,
            // v1.3.0: lowered 90→60 — free capital faster; profit-first extension
            // logic still holds genuinely recovering positions beyond this limit.
            max_position_hold_mins: 60,
            // v1.1.0 new risk + sizing defaults
            min_sol_reserve: 0.02,
            // v1.1.1: enabled 0→200ms — waits 200ms then re-checks the quote vault.
            // If the vault drained >15% (someone already front-ran the pump), skip.
            // This costs one extra RPC call per pool but eliminates the class of trades
            // where we buy into a pool that was already >15% pumped by the time our
            // tx builds — those trades are near-guaranteed immediate losses.
            confirmation_window_ms: 200,
            // v1.3.1: disabled session heat (0 = off) — the drawdown guard is a
            // better circuit breaker. Heat was miscounting forced sell-mode exits
            // (-0.499% AMM spread) as "losses", triggering 15-min buy pauses
            // after normal operation and causing the bot to miss profitable pools.
            session_heat_losses: 0,
            session_heat_window_secs: 3600,
            session_heat_cooldown_mins: 15,
            // v1.2.0: exit when in profit and volume drops >65% from entry vault —
            // signals the pump is exhausted and smart money has already exited.
            volume_exhaustion_pct: 65.0,
            buy_sell_ratio_exit: 0.0,
            check_interval_acceleration: true,
            // v1.2.0: exit instantly when a single check shows the vault drops >22%
            // in one tick — strong rug/whale-exit signal; acts faster than the
            // 3-consecutive-decline detector.
            whale_exit_vault_drop_pct: 22.0,
            // v1.3.0: ON — scales buy size by pool_score/100 so the best pools
            // get full capital while marginal ones get partial exposure.
            pool_quality_sizing: true,
            kelly_min_trades: 10,
            deployer_wallet_age_min_hours: 0,
            filter_cache_ttl_secs: 30,
            // v1.5.0: live data shows all profitable pump.fun trades exit within 6 s.
            // Positions flat beyond 45 s are dead — exit to redeploy capital.
            no_pump_timeout_secs: 45,
            no_pump_min_gain_pct: 3.0,
            min_dump_hold_secs: 0,
            peak_stagnation_secs: 90,
            peak_stagnation_min_pnl_pct: 20.0,
            runner_mode: false,
            runner_scale_in_sol: 0.02,
            pool_drain_exit_pct: 15.0,
            mint_cooldown_secs: 1800,
            ai_chain: AiChainConfig::default(),
            blocked_hours_utc: Vec::new(),
            weekend_mode: String::new(),
            weekday_mode: String::new(),
            pumpfun_trending_enabled: false,
            pumpfun_trending_score: default_pumpfun_trending_score(),
            pumpfun_min_curve_pct: default_pumpfun_min_curve_pct(),
            pumpfun_window_secs: default_pumpfun_window_secs(),
            pumpfun_max_migration_age_secs: default_pumpfun_max_migration_age_secs(),
            pumpfun_min_recent_buys: default_pumpfun_min_recent_buys(),
            pumpfun_min_net_buy_sol: default_pumpfun_min_net_buy_sol(),
            pumpfun_max_last_buy_age_secs: default_pumpfun_max_last_buy_age_secs(),
        }
    }
}

/// Configuration for the 3-layer LLM pool-selection chain.
///
/// Layer assignment:
///   L1 = Groq (llama-3.1-8b-instant) — fast binary screener, GROQ_API_KEY
///   L2 = OpenRouter (gemma-2-9b-it:free) — deep analyst, OPENROUTER_API_KEY
///   L3 = Cerebras (llama-3.1-8b) — risk judge, CEREBRAS_API_KEY
///
/// All three providers have free tiers. Missing keys = chain disabled, falls
/// back to pool_scorer gating only.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AiChainConfig {
    /// Enable the 3-layer LLM chain. Requires all three API keys.
    pub enabled: bool,
    /// Groq model for Layer 1 screener. Default: llama-3.1-8b-instant
    pub l1_model: String,
    /// OpenRouter model for Layer 2 analyst. Default: google/gemma-2-9b-it:free
    pub l2_model: String,
    /// Cerebras model for Layer 3 risk judge. Default: llama-3.1-8b
    pub l3_model: String,
    /// L1 timeout in ms. Default 600.
    pub l1_timeout_ms: u64,
    /// L2+L3 parallel timeout in ms. Default 1800.
    pub l2l3_timeout_ms: u64,
    /// Minimum L2 analyst score (0-100) required for a BUY. Default 65.
    pub min_l2_score: u8,
}

impl Default for AiChainConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            l1_model: "llama-3.1-8b-instant".into(),
            l2_model: "google/gemma-2-9b-it:free".into(),
            l3_model: "llama-3.1-8b".into(),
            l1_timeout_ms: 600,
            l2l3_timeout_ms: 1800,
            min_l2_score: 65,
        }
    }
}

impl SniperConfig {
    /// Retrieve the active rate mode by name, or None if not found
    pub fn get_active_rate_mode(&self) -> Option<&RateMode> {
        self.rate_modes
            .iter()
            .find(|m| m.name == self.active_mode_name && m.enabled)
    }

    /// List all enabled rate modes sorted by order
    pub fn enabled_rate_modes(&self) -> Vec<&RateMode> {
        let mut modes: Vec<_> = self.rate_modes.iter().filter(|m| m.enabled).collect();
        modes.sort_by_key(|m| m.order);
        modes
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FilterConfig {
    /// Interval ms between filter checks
    pub check_interval_ms: u64,
    /// Total duration ms to wait for filters to pass
    pub check_duration_ms: u64,
    /// How many consecutive passes required
    pub consecutive_matches: u32,
    pub check_mint_renounced: bool,
    pub check_freezable: bool,
    pub check_burned: bool,
    pub check_mutable: bool,
    pub check_socials: bool,
    /// Min pool size in quote token (0 = disabled)
    pub min_pool_size: f64,
    /// Max pool size in quote token (0 = disabled)
    pub max_pool_size: f64,
    /// Reject tokens whose name/symbol contains known scam words
    pub check_name: bool,
    /// Require minimum recent transaction activity before buying
    pub check_volume: bool,
    /// Minimum number of transactions on the pool in the last 60s
    pub min_volume_txns: u32,
    /// Check if our position size causes >X% price impact
    pub check_liquidity_depth: bool,
    /// Max acceptable price impact % for our buy size (0 = disabled)
    pub max_price_impact_pct: f64,
    /// Reject tokens where top-10 holders own more than this % of supply
    pub check_holder_concentration: bool,
    /// Maximum allowed top-10 holder concentration % (e.g. 70.0)
    pub max_top10_holder_pct: f64,

    // ── Liquidity momentum ────────────────────────────────────────────────────
    /// Reject pools whose quote vault hasn't grown enough between two checks
    pub check_liquidity_momentum: bool,
    /// Minimum required liquidity growth % in the momentum check window
    pub liquidity_momentum_pct: f64,

    // ── Cross-pool correlation rug guard ──────────────────────────────────────
    /// Reject tokens from deployers with too many recent rugs
    pub check_cross_pool_correlation: bool,
    /// Maximum allowable rug count in the last 24 h for a deployer
    pub max_deployer_rugs_24h: u32,

    // ── Jupiter price discrepancy ─────────────────────────────────────────────
    /// Only buy when Jupiter price is higher than AMM price by at least this %
    pub check_jupiter_discrepancy: bool,
    /// Minimum Jupiter premium % to treat as a buy signal
    pub jupiter_min_premium_pct: f64,

    // ── Deployer wallet age ───────────────────────────────────────────────────
    /// Reject pools from wallets younger than `deployer_min_age_hours`. Fresh
    /// wallets are a strong rug signal. 0 = disabled.
    pub check_deployer_wallet_age: bool,
    pub deployer_min_age_hours: u64,
    /// Cache filter results per pool pubkey for this many seconds.
    /// Avoids redundant RPC calls when duplicate pool events arrive. Default 30.
    pub filter_cache_ttl_secs: u64,
}

impl Default for FilterConfig {
    fn default() -> Self {
        // v0.9.2 sweet-spot tuning: v0.8.0 tightened filters 30% but the new
        // ON-by-default gates (liquidity_momentum, holder_concentration via the
        // 67% cap) rejected essentially all fresh pump.fun graduates because
        // memecoin reality is: top-heavy at launch, vault non-monotonic during
        // first few seconds, dev holds 30–60 % until first wave of buys.
        //
        // The new doctrine: cheap dumb filters loose, smart AI gates (Grok risk
        // scorer + dQ* observer) carry the discrimination weight. The dQ* agent
        // learns the pool-quality signal from realised PnL, so as it trains it
        // gets sharper — better than fixed thresholds.
        //
        // Kept ON because they're truly cheap and sharp:
        //   - check_freezable (real honeypot signal)
        //   - check_burned (empty vault is a real rug)
        //   - check_cross_pool_correlation (rejects known-rug deployers)
        Self {
            check_interval_ms: 500,
            check_duration_ms: 8_000,
            consecutive_matches: 1,
            check_mint_renounced: false,
            check_freezable: true,
            check_burned: true,
            check_mutable: true,
            check_socials: false,
            // v1.2.0: raised 1.0→2.0 SOL. The 1 SOL floor reduced the worst rug
            // rate but 1-2 SOL pools still drain within the first 500ms. 2 SOL gives
            // the pool enough liquidity cushion to survive initial bot traffic.
            // Ultra-low-cap operators can lower to 1.0 in config.toml.
            min_pool_size: 2.0,
            max_pool_size: 0.0,
            // v1.2.0: enabled — cheap name/symbol check filters the most obvious
            // scam tokens (test, fake, honeypot etc.) with zero RPC cost.
            check_name: true,
            check_volume: false,
            min_volume_txns: 3,
            check_liquidity_depth: true,
            max_price_impact_pct: 5.0,
            check_holder_concentration: false,
            // Soft guard if check_holder_concentration is enabled — 90 % means
            // we reject only the most extreme top-heavy pools. Fresh memecoins
            // are inherently top-heavy until early buyers diversify.
            max_top10_holder_pct: 90.0,
            // OFF by default — vault doesn't always grow during the first few
            // ticks of a pump.fun launch (early sells, fee burn). Enable
            // explicitly in config.toml only after backtesting against your
            // pool stream.
            check_liquidity_momentum: false,
            liquidity_momentum_pct: 5.0,
            // KEEP ON — sharp, cheap, and the dQ* agent's reward signal also
            // depends on the deployer ledger.
            check_cross_pool_correlation: true,
            // v1.2.0: tightened 3→2 — a deployer with 2+ rugs in 24h is a repeat
            // offender; blocking at 2 reduces exposure without excluding genuinely
            // new deployers (who have 0 rug history).
            max_deployer_rugs_24h: 2,
            check_jupiter_discrepancy: false,
            jupiter_min_premium_pct: 5.0,
            // v1.3.0: ON — reject deployers whose wallet is younger than 24h.
            // Fresh wallets are near-universally rug setups. 24h is enough
            // to eliminate day-1 throwaway wallets without being too strict.
            check_deployer_wallet_age: true,
            deployer_min_age_hours: 24,
            filter_cache_ttl_secs: 30,
        }
    }
}

/// Notification / alert configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AlertsConfig {
    /// Telegram bot token (leave empty to disable)
    pub telegram_bot_token: String,
    /// Telegram chat ID (user or group)
    pub telegram_chat_id: String,
    /// Discord webhook URL (leave empty to disable)
    pub discord_webhook_url: String,
    /// Fire Windows desktop toast notifications
    pub desktop_notifications: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArbConfig {
    pub enabled: bool,
    /// Starting token mint (default: USDC)
    pub start_mint: String,
    /// Starting capital in UI units
    pub start_amount: f64,
    /// Minimum profit in lamports to execute
    pub min_profit_lamports: u64,
    /// Max hops in arb path (2 or 3)
    pub max_hops: usize,
    /// DEXes to include in graph
    pub dexes: Vec<String>,
    /// Pool metadata directory
    pub pool_dir: String,
    /// How many parallel amount sizes to try (halving strategy)
    pub amount_levels: u32,
}

impl Default for ArbConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            start_mint: "USDC".into(),
            start_amount: 100.0,
            min_profit_lamports: 10_000,
            max_hops: 3,
            dexes: vec!["Raydium".into(), "Orca".into(), "Meteora".into()],
            pool_dir: "pools".into(),
            amount_levels: 4,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionConfig {
    /// "default" | "jito" | "warp"
    pub executor: String,
    /// Custom fee in SOL (used by jito/warp)
    pub custom_fee_sol: f64,
    /// Compute unit limit (default executor)
    pub compute_unit_limit: u32,
    /// Compute unit price in micro-lamports (default executor)
    pub compute_unit_price: u64,
    /// Hard cap for computed priority fee in micro-lamports (0 = disabled)
    #[serde(default = "default_compute_unit_price_hard_cap")]
    pub compute_unit_price_hard_cap: u64,
    /// Optional SetLoadedAccountsDataSizeLimit byte limit (0 = disabled)
    #[serde(default)]
    pub loaded_accounts_data_size_limit: u32,
    /// Skip preflight simulation
    pub skip_preflight: bool,
    /// Jito block engine URL
    pub jito_url: String,
}

impl Default for ExecutionConfig {
    fn default() -> Self {
        Self {
            executor: "default".into(),
            custom_fee_sol: 0.006,
            compute_unit_limit: 200_000,
            compute_unit_price: 100_000,
            compute_unit_price_hard_cap: default_compute_unit_price_hard_cap(),
            loaded_accounts_data_size_limit: 0,
            skip_preflight: true,
            jito_url: "https://mainnet.block-engine.jito.wtf".into(),
        }
    }
}

impl BotConfig {
    /// Load config from a TOML file
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
        let content = std::fs::read_to_string(path)?;
        let config: BotConfig = toml::from_str(&content)?;
        Ok(config)
    }

    /// Load config from environment variables (dotenv) and overrides from config file
    pub fn from_env() -> Result<Self> {
        dotenv::dotenv().ok();

        // Try loading from config.toml first, then fall back to env
        let config_path = std::env::var("CONFIG_PATH").unwrap_or_else(|_| "config.toml".into());
        if std::path::Path::new(&config_path).exists() {
            return Self::from_file(config_path);
        }

        Ok(Self {
            rpc: RpcConfig::default(),
            wallet: WalletConfig {
                keypair_path: std::env::var("KEYPAIR_PATH")
                    .unwrap_or_else(|_| "~/.config/solana/id.json".into()),
            },
            sniper: SniperConfig::default(),
            arb: ArbConfig::default(),
            execution: ExecutionConfig::default(),
            alerts: AlertsConfig::default(),
        })
    }
}