-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
1668 lines (1497 loc) · 56.6 KB
/
Copy pathconfig.py
File metadata and controls
1668 lines (1497 loc) · 56.6 KB
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
# -*- coding: utf-8 -*-
import os
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterator, Optional, Tuple, Union
BASE_DIR = Path(__file__).resolve().parent
CONFIG_WARNINGS = []
_DOTENV_VARS: Dict[str, str] = {}
def _load_dotenv_if_present(path: Path, profile: str = "") -> None:
if not path.exists():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
if key:
clean_value = value.strip().strip("\"'")
if profile and not key.upper().startswith(
(f"{profile.upper()}_", "HTXBOT_")
):
formatted_key = f"{profile.upper()}_{key}"
if formatted_key not in _DOTENV_VARS:
_DOTENV_VARS[formatted_key] = clean_value
else:
if key not in _DOTENV_VARS:
_DOTENV_VARS[key] = clean_value
_load_dotenv_if_present(BASE_DIR / ".env")
def _env(name: str, profile: str = "") -> str:
candidates = []
if profile:
prefix = profile.upper()
candidates.extend((f"{prefix}_{name}", f"HTXBOT_{prefix}_{name}"))
candidates.append(f"HTXBOT_{name}")
candidates.append(name)
candidates.append(f"HTXBOT_{name}")
for candidate in candidates:
value = os.environ.get(candidate, _DOTENV_VARS.get(candidate, "")).strip()
if value:
return value
return ""
def _first_env(*names: str, profile: str = "") -> str:
for name in names:
value = _env(name, profile=profile)
if value:
return value
return ""
def _env_bool(name: str, default: bool, profile: str = "") -> bool:
value = _env(name, profile=profile).lower()
if value in {"1", "true", "yes", "y", "on"}:
return True
if value in {"0", "false", "no", "n", "off"}:
return False
return default
def _env_float(name: str, default: float, profile: str = "") -> float:
value = _env(name, profile=profile)
if not value:
return default
try:
return float(value)
except ValueError:
return default
def _env_int(name: str, default: int, profile: str = "") -> int:
value = _env(name, profile=profile)
if not value:
return default
try:
return int(value)
except ValueError:
return default
def _env_csv(name: str, default: Tuple[str, ...], profile: str = "") -> Tuple[str, ...]:
value = _env(name, profile=profile)
if not value:
return default
items = tuple(item.strip() for item in value.split(",") if item.strip())
return items or default
def _env_csv_optional(*names: str, profile: str = "") -> Optional[Tuple[str, ...]]:
for name in names:
value = _env(name, profile=profile)
if not value:
continue
return tuple(item.strip() for item in value.split(",") if item.strip())
return None
def _normalize_coin(coin: str) -> str:
return str(coin or "").strip().lower()
def _normalize_coins(coins: Tuple[str, ...]) -> Tuple[str, ...]:
seen = set()
normalized = []
for coin in coins or ():
item = _normalize_coin(coin)
if not item or item in seen:
continue
seen.add(item)
normalized.append(item)
return tuple(normalized)
def _account_coin_env_names(suffix: str = "") -> Tuple[str, ...]:
suffix = str(suffix or "").strip()
if not suffix:
return ("HTX_COINS", "COINS")
return (
f"HTX_COINS_{suffix}",
f"COINS_{suffix}",
f"HTX_API_{suffix}_COINS",
f"API_{suffix}_COINS",
f"HTX_{suffix}_COINS",
)
def _configured_account_coins(
profile: str, suffix: str = "", default: Tuple[str, ...] = ()
) -> Tuple[str, ...]:
configured = _env_csv_optional(*_account_coin_env_names(suffix), profile=profile)
if configured is None:
return _normalize_coins(default)
return _normalize_coins(configured)
def _configured_profile_coins(profile: str) -> Tuple[str, ...]:
return _normalize_coins(
_configured_account_coins(profile, "", ())
+ _configured_account_coins(profile, "2", ())
)
def _env_float_tuple(
name: str, default: Tuple[float, ...], profile: str = ""
) -> Tuple[float, ...]:
value = _env(name, profile=profile)
if not value:
return default
parsed = []
for item in value.split(","):
item = item.strip()
if not item:
continue
try:
parsed.append(float(item))
except ValueError:
return default
return tuple(parsed) or default
def _env_optional_float_tuple(
name: str, default: Tuple[Optional[float], ...], profile: str = ""
) -> Tuple[Optional[float], ...]:
value = _env(name, profile=profile)
if not value:
return default
parsed = []
for item in value.split(","):
item = item.strip().lower()
if not item:
continue
if item in {"runner", "none", "null"}:
parsed.append(None)
continue
try:
parsed.append(float(item))
except ValueError:
return default
return tuple(parsed) or default
def _add_config_warning(message: str) -> None:
if message not in CONFIG_WARNINGS:
CONFIG_WARNINGS.append(message)
LONG_COINS = _configured_profile_coins("long")
SHORT_COINS = _configured_profile_coins("short")
@dataclass(frozen=True)
class ApiCredentials:
api_key: str
api_secret: str
@dataclass(frozen=True)
class ApiAccountSettings:
name: str
api_credentials: ApiCredentials
coins: Tuple[str, ...]
@dataclass(frozen=True)
class ExchangeSettings:
quote_currency: str
enable_rate_limit: bool
timeout_ms: int
default_type: str
set_position_mode_on_start: bool
set_leverage_on_start: bool
contract_hostnames: Tuple[str, ...]
market_load_retries: int
markets_cache_max_age_sec: int
@dataclass(frozen=True)
class SignalSettings:
timeframe: str
rs_fast_window: int
rs_slow_window: int
@dataclass(frozen=True)
class BuySettings:
position_budget_fraction: float
ladder_fractions: Tuple[float, ...]
ladder_offsets: Tuple[float, ...]
@dataclass(frozen=True)
class SellSettings:
buy_fee_rate: float
sell_fee_rate: float
min_gross_profit_floor: float
@dataclass(frozen=True)
class RiskSettings:
min_quote_reserve: float
max_active_positions: int
max_position_notional_fraction: float
max_total_notional_fraction: float
active_position_min_notional_for_slot: float
dust_position_notional: float
dust_close_enabled: bool
tiny_entry_close_enabled: bool
tiny_entry_max_notional: float
tiny_entry_max_planned_fraction: float
leverage: int
account_leverage: int
margin_mode: str
position_mode: str
cooldown_minutes_after_close: float
post_win_cooldown_minutes_after_close: float
@dataclass(frozen=True)
class StrategySettings:
ema_strategy_enabled: bool
ema_macro_timeframe: str
ema_pullback_timeframe: str
ema_trigger_timeframe: str
ema_macro_fast_minutes: int
ema_macro_slow_minutes: int
ema_pullback_fast_minutes: int
ema_pullback_slow_minutes: int
ema_pullback_recovery_lookback_minutes: int
ema_pullback_recovery_max_cross_age_minutes: int
ema_pullback_recovery_gap: float
ema_entry_require_pullback_recovery: bool
ema_chop_filter_enabled: bool
ema_chop_period: int
ema_chop_max: float
ema_volume_confirmation_enabled: bool
ema_volume_short_window: int
ema_volume_long_window: int
ema_volume_min_ratio: float
ema_volume_min_directional_fraction: float
ema_volume_spike_filter_enabled: bool
ema_volume_spike_window: int
ema_volume_spike_min_ratio: float
ema_volume_adverse_spike_min_ratio: float
ema_volume_profile_filter_enabled: bool
ema_volume_profile_window: int
ema_volume_profile_bins: int
ema_volume_profile_value_area: float
ema_trigger_fast_minutes: int
ema_trigger_slow_minutes: int
ema_use_rs_confirmation: bool
ema_long_min_rs60: float
ema_short_max_rs60: float
ema_use_btc_risk_filter: bool
ema_btc_long_min_return_30m: float
ema_btc_short_max_return_30m: float
ema_take_profit_markup: float
ema_exit_ladder_fractions: Tuple[float, ...]
ema_adaptive_exit_enabled: bool
ema_exit_normal_ladder_fractions: Tuple[float, ...]
ema_exit_normal_ladder_markups: Tuple[float, ...]
ema_exit_medium_ladder_fractions: Tuple[float, ...]
ema_exit_medium_ladder_markups: Tuple[float, ...]
ema_exit_heavy_ladder_fractions: Tuple[float, ...]
ema_exit_heavy_ladder_markups: Tuple[float, ...]
ema_exit_medium_position_ratio: float
ema_exit_heavy_position_ratio: float
ema_exit_decay_first_markup_after_hours: float
ema_exit_decay_first_markup_cap: float
ema_exit_decay_max_markup_after_hours: float
ema_exit_decay_max_markup: float
ema_exit_runner_enabled: bool
ema_exit_runner_activation_markup: float
ema_exit_runner_trailing_pullback: float
ema_exit_runner_take_profit_markup: float
ema_exit_trailing_enabled: bool
ema_exit_trailing_fixed_fraction: float
ema_exit_trailing_activation_markup: float
ema_exit_trailing_pullback: float
ema_exit_trailing_atr_multiplier: float
ema_exit_trailing_min_pullback: float
ema_exit_trailing_max_pullback: float
ema_exit_trailing_take_profit_markup: float
ema_exit_runner_profit_lock_enabled: bool
ema_exit_runner_use_aggressive_limit: bool
ema_averaging_enabled: bool
ema_averaging_drawdown_step: float
ema_averaging_min_drawdown_step: float
ema_averaging_base_fraction: float
ema_averaging_power: float
ema_averaging_interval_hours: float
ema_averaging_atr_enabled: bool
ema_averaging_atr_period: int
ema_averaging_atr_multiplier: float
ema_averaging_min_atr_multiplier: float
ema_averaging_min_daily_volatility_fraction: float
ema_averaging_require_pullback_recovery: bool
ema_max_averaging_stages: int
account_pnl_enabled: bool
account_pnl_window_minutes: float
account_pnl_sample_interval_sec: float
account_profit_unload_enabled: bool
account_profit_unload_min_pnl_quote: float
account_profit_unload_min_pnl_rate: float
account_profit_unload_percentile: float
account_profit_unload_fraction: float
account_profit_unload_drawdown_fraction: float
account_profit_unload_peak_drawdown_fraction: float
account_profit_unload_full_pnl_quote: float
account_profit_unload_min_position_pnl_quote: float
account_profit_unload_min_position_pnl_rate: float
account_profit_unload_cooldown_sec: float
account_pnl_trailing_enabled: bool
account_pnl_trailing_activation_rate: float
account_pnl_trailing_stop_rate: float
account_pnl_trailing_min_pnl_quote: float
account_averaging_enabled: bool
account_averaging_min_samples: int
account_averaging_percentile: float
account_averaging_near_trough_quote: float
account_averaging_near_trough_fraction: float
account_averaging_bounce_quote: float
account_averaging_falling_guard_quote: float
account_averaging_falling_guard_fraction: float
account_averaging_budget_scale: float
ema_breakeven_enabled: bool
ema_breakeven_after_hours: float
ema_breakeven_reprice_minutes: float
ema_breakeven_fee_buffer: float
ema_breakeven_exit_fractions: Tuple[float, ...]
enable_signal_size_scaling: bool
signal_budget_min_multiplier: float
signal_budget_max_multiplier: float
signal_score_reference: float
signal_ema_gap_weight: float
entry_min_score: float
entry_min_rs60_abs: float
entry_min_rs30_abs: float
entry_macro_invalid_penalty: float
entry_pullback_invalid_penalty: float
entry_trigger_invalid_penalty: float
entry_btc_invalid_penalty: float
entry_btc_return_penalty_multiplier: float
entry_market_structure_invalid_penalty: float
entry_volume_invalid_penalty: float
entry_chop_invalid_penalty: float
entry_rs60_shortfall_penalty_multiplier: float
entry_rs30_shortfall_penalty_multiplier: float
entry_quality_budget_min_multiplier: float
entry_quality_budget_reference: float
entry_max_new_ladders_per_signal: int
entry_rate_limit_ladders: int
entry_rate_limit_window_minutes: float
entry_crowded_signal_fraction: float
entry_crowded_min_signals: int
entry_crowded_max_new_ladders_per_signal: int
entry_crowded_min_score: float
entry_crowded_min_rs60_abs: float
entry_crowded_min_rs30_abs: float
entry_spread_filter_enabled: bool
entry_spread_filter_max_bps: float
entry_spread_filter_block_if_unavailable: bool
max_buy_stages: int
averaging_drawdown_steps: Tuple[float, ...]
averaging_budget_fractions: Tuple[float, ...]
no_more_averaging_after_minutes: float
time_exit_after_minutes: float
urgent_time_exit_after_minutes: float
hard_time_exit_after_minutes: float
hard_time_exit_close_fraction: float
hard_time_exit_step_minutes: float
hard_time_exit_fraction_step: float
hard_time_exit_max_loss_on_notional: float
hard_time_exit_bypass_profit_bank: bool
hard_stop_loss_enabled: bool
hard_stop_loss_pct: float
hard_stop_loss_min_emergency_pct: float
hard_stop_loss_atr_enabled: bool
hard_stop_loss_atr_multiplier: float
hard_stop_loss_atr_max_pct: float
soft_defensive_exit_enabled: bool
soft_defensive_exit_min_drawdown: float
soft_defensive_exit_btc_against_return: float
soft_defensive_exit_confirmations: int
soft_defensive_exit_initial_fraction: float
soft_defensive_exit_step_fraction: float
soft_defensive_exit_max_fraction: float
soft_defensive_exit_reprice_minutes: float
enable_absolute_force_exit: bool
absolute_force_exit_after_minutes: float
enable_controlled_loss_exit: bool
controlled_loss_after_zombie_minutes: float
controlled_loss_min_drawdown: float
controlled_loss_max_loss_on_notional: float
controlled_loss_max_position_fraction: float
controlled_loss_profit_bank_today_fraction: float
controlled_loss_profit_bank_7d_fraction: float
controlled_loss_min_bank_usdt: float
controlled_loss_min_move_fraction: float
controlled_loss_ramp_minutes: float
controlled_loss_reprice_minutes: float
controlled_loss_macro_gap_reference: float
controlled_loss_macro_max_speed_multiplier: float
controlled_loss_volatility_speed_enabled: bool
controlled_loss_volatility_reference: float
controlled_loss_volatility_trigger_multiplier: float
controlled_loss_volatility_max_speed_multiplier: float
controlled_loss_volatility_exponent: float
controlled_loss_volatility_reprice_min_move_delta: float
max_unhealthy_positions_for_new_entries: int
cancel_unsafe_hidden_close_orders: bool
enable_volatility_adjusted_ladders: bool
volatility_window: int
volatility_reference: float
daily_volatility_window: int
daily_volatility_reference: float
enable_volatility_targeted_sizing: bool
min_volatility_budget_multiplier: float
max_volatility_budget_multiplier: float
enable_volatility_recovery_stages: bool
averaging_drawdown_daily_volatility_fraction: float
min_ladder_volatility_multiplier: float
max_ladder_volatility_multiplier: float
min_profit_fee_multiplier: float
enable_dynamic_profit_floor: bool
dynamic_profit_floor_volatility_multiplier_threshold: float
dynamic_profit_floor_high_vol_multiplier: float
dynamic_profit_floor_adverse_funding_multiplier: float
dynamic_profit_floor_urgent_multiplier: float
dynamic_profit_floor_min_rate: float
enable_btc_risk_multiplier: bool
btc_risk_return_window: int
btc_risk_drop_threshold: float
btc_risk_high_vol_threshold: float
btc_risk_drop_budget_multiplier: float
btc_risk_vol_budget_multiplier: float
btc_risk_min_budget_multiplier: float
btc_risk_max_ladder_multiplier: float
enable_funding_aware_exit: bool
funding_cache_ttl_sec: int
funding_positive_threshold: float
funding_negative_threshold: float
funding_positive_markup_multiplier: float
funding_negative_markup_multiplier: float
@dataclass(frozen=True)
class MacroSettings:
enable_gold_btc_rsi_overlay: bool
gold_coins: Tuple[str, ...]
gold_timeframe: str
gold_rsi_period: int
gold_min_candles: int
gold_cache_ttl_sec: int
use_direct_gold_btc_pair: bool
direct_gold_btc_symbol: str
gold_strong_rsi: float
gold_weak_rsi: float
btc_strong_rsi: float
btc_weak_rsi: float
rsi_spread_threshold: float
risk_off_long_budget_multiplier: float
risk_off_short_budget_multiplier: float
risk_off_ladder_multiplier: float
risk_off_disable_averaging: bool
risk_off_time_exit_multiplier: float
enable_gold_directional_bias: bool
gold_directional_bias_strength: float
gold_directional_bias_min_multiplier: float
gold_directional_bias_max_multiplier: float
gold_btc_ratio_return_reference: float
panic_disable_new_entries: bool
stale_macro_max_age_sec: int
@dataclass(frozen=True)
class ExternalPriceFeedSettings:
enabled: bool
primary_exchange: str
reference_exchanges: Tuple[str, ...]
rest_poll_interval_sec: float
rest_timeout_sec: float
max_price_age_ms: int
min_valid_bid_qty_usdt: float
min_valid_ask_qty_usdt: float
max_internal_spread_bps: float
entry_filter_enabled: bool
score_penalty_multiplier: float
max_htx_premium_for_long_bps: float
max_htx_discount_for_short_bps: float
block_if_exchange_divergence_1m_bps: float
block_duration_sec: int
directional_1m_gate_enabled: bool
directional_entry_1m_block_bps: float
directional_averaging_1m_block_bps: float
impulse_confirmation_enabled: bool
mexc_lead_threshold_bps_30s: float
impulse_score_bonus: float
require_same_direction: bool
exit_adjustment_enabled: bool
long_take_profit_tighten_if_htx_premium_bps: float
short_take_profit_tighten_if_htx_discount_bps: float
tightened_ladder_fractions: Tuple[float, ...]
tightened_ladder_markups: Tuple[Optional[float], ...]
disable_trading_if_reference_stale: bool
ignore_reference_if_stale: bool
stale_after_ms: int
@dataclass(frozen=True)
class HedgeSettings:
btc_hedge_enabled: bool
btc_hedge_coin: str
btc_hedge_ratio: float
btc_hedge_min_rebalance_notional: float
btc_hedge_max_notional: float
btc_hedge_max_spread_bps: float
btc_hedge_cooldown_sec: float
@dataclass(frozen=True)
class MonitoringSettings:
log_level: str
cycle_stats_csv_file: str
csv_log_file: str
macro_csv_file: str
external_price_csv_file: str
account_pnl_csv_file: str
signal_analytics_csv_file: str
signal_analytics_jsonl_file: str
diagnostics_csv_file: str
diagnostics_jsonl_file: str
csv_archive_dir: str
csv_rotate_max_bytes: int
@dataclass(frozen=True)
class RuntimeSettings:
dry_run: bool
dry_run_equity: float
order_timeout_sec: int
poll_interval_sec: int
market_data_max_workers: int
post_only_enabled: bool
reduce_only_enabled: bool
fetch_fill_details_on_sync: bool
fill_detail_lookback_sec: int
state_file: str
markets_cache_file: str
def _make_hedge_settings() -> HedgeSettings:
return HedgeSettings(
btc_hedge_enabled=False,
btc_hedge_coin=(_env("BTC_HEDGE_COIN") or "btc").strip().lower(),
btc_hedge_ratio=max(0.0, 1.0),
btc_hedge_min_rebalance_notional=max(
0.0, 10.0
),
btc_hedge_max_notional=max(0.0, 0.0),
btc_hedge_max_spread_bps=max(0.0, 30.0),
btc_hedge_cooldown_sec=max(0.0, 30.0),
)
HEDGE = _make_hedge_settings()
@dataclass(frozen=True)
class BotProfile:
name: str
coins: Tuple[str, ...]
trade_direction: str
position_side: str
opposite_position_side: str
entry_side: str
exit_side: str
api_credentials: ApiCredentials
api_accounts: Tuple[ApiAccountSettings, ...]
exchange: ExchangeSettings
signals: SignalSettings
buying: BuySettings
selling: SellSettings
risk: RiskSettings
strategy: StrategySettings
macro: MacroSettings
monitoring: MonitoringSettings
runtime: RuntimeSettings
external_price_feed: ExternalPriceFeedSettings
@property
def COINS(self) -> Tuple[str, ...]:
return self.coins
@property
def TRADE_DIRECTION(self) -> str:
return self.trade_direction
@property
def POSITION_SIDE(self) -> str:
return self.position_side
@property
def OPPOSITE_POSITION_SIDE(self) -> str:
return self.opposite_position_side
@property
def ENTRY_SIDE(self) -> str:
return self.entry_side
@property
def EXIT_SIDE(self) -> str:
return self.exit_side
@property
def API_CREDENTIALS(self) -> ApiCredentials:
return self.api_credentials
@property
def API_ACCOUNTS(self) -> Tuple[ApiAccountSettings, ...]:
return self.api_accounts
@property
def EXCHANGE(self) -> ExchangeSettings:
return self.exchange
@property
def SIGNALS(self) -> SignalSettings:
return self.signals
@property
def BUYING(self) -> BuySettings:
return self.buying
@property
def SELLING(self) -> SellSettings:
return self.selling
@property
def RISK(self) -> RiskSettings:
return self.risk
@property
def STRATEGY(self) -> StrategySettings:
return self.strategy
@property
def MACRO(self) -> MacroSettings:
return self.macro
@property
def MONITORING(self) -> MonitoringSettings:
return self.monitoring
@property
def RUNTIME(self) -> RuntimeSettings:
return self.runtime
@property
def EXTERNAL_PRICE_FEED(self) -> ExternalPriceFeedSettings:
return self.external_price_feed
@property
def BOT_NAME(self) -> str:
return self.name
def _path(profile: str, filename: str) -> str:
return str(BASE_DIR / profile / filename)
def _validate_fraction_tuple(
name: str, values: Tuple[float, ...], eps: float = 1e-9
) -> None:
if not values:
raise ValueError(f"{name} must not be empty")
if any(item < 0 for item in values):
raise ValueError(f"{name} must contain non-negative fractions")
total = sum(values)
if total > 1.0 + eps:
raise ValueError(f"{name} sum must be <= 1.0, got {total:.12f}")
def _validate_tuple_lengths(
name: str,
left_name: str,
left: Tuple[object, ...],
right_name: str,
right: Tuple[object, ...],
) -> None:
if len(left) != len(right):
raise ValueError(
f"{name} {left_name} and {right_name} must have the same length, "
f"got {len(left)} and {len(right)}"
)
def _validate_profile(profile: "BotProfile") -> None:
_validate_fraction_tuple(
f"{profile.name}.BUYING.ladder_fractions", profile.buying.ladder_fractions
)
_validate_fraction_tuple(
f"{profile.name}.STRATEGY.ema_exit_ladder_fractions",
profile.strategy.ema_exit_ladder_fractions,
)
_validate_fraction_tuple(
f"{profile.name}.STRATEGY.ema_exit_normal_ladder_fractions",
profile.strategy.ema_exit_normal_ladder_fractions,
)
_validate_fraction_tuple(
f"{profile.name}.STRATEGY.ema_exit_medium_ladder_fractions",
profile.strategy.ema_exit_medium_ladder_fractions,
)
_validate_fraction_tuple(
f"{profile.name}.STRATEGY.ema_exit_heavy_ladder_fractions",
profile.strategy.ema_exit_heavy_ladder_fractions,
)
_validate_fraction_tuple(
f"{profile.name}.STRATEGY.ema_breakeven_exit_fractions",
profile.strategy.ema_breakeven_exit_fractions,
)
_validate_tuple_lengths(
f"{profile.name}.BUYING",
"ladder_fractions",
profile.buying.ladder_fractions,
"ladder_offsets",
profile.buying.ladder_offsets,
)
_validate_tuple_lengths(
f"{profile.name}.STRATEGY.ema_exit_normal",
"ladder_fractions",
profile.strategy.ema_exit_normal_ladder_fractions,
"ladder_markups",
profile.strategy.ema_exit_normal_ladder_markups,
)
_validate_tuple_lengths(
f"{profile.name}.STRATEGY.ema_exit_medium",
"ladder_fractions",
profile.strategy.ema_exit_medium_ladder_fractions,
"ladder_markups",
profile.strategy.ema_exit_medium_ladder_markups,
)
_validate_tuple_lengths(
f"{profile.name}.STRATEGY.ema_exit_heavy",
"ladder_fractions",
profile.strategy.ema_exit_heavy_ladder_fractions,
"ladder_markups",
profile.strategy.ema_exit_heavy_ladder_markups,
)
_validate_tuple_lengths(
f"{profile.name}.EXTERNAL_PRICE_FEED.tightened_ladder",
"fractions",
profile.external_price_feed.tightened_ladder_fractions,
"markups",
profile.external_price_feed.tightened_ladder_markups,
)
_validate_fraction_tuple(
f"{profile.name}.EXTERNAL_PRICE_FEED.tightened_ladder_fractions",
profile.external_price_feed.tightened_ladder_fractions,
)
if not 0.0 <= profile.strategy.ema_exit_trailing_fixed_fraction <= 1.0:
raise ValueError(
f"{profile.name}.STRATEGY.ema_exit_trailing_fixed_fraction must be between 0 and 1"
)
for setting_name in (
"account_profit_unload_percentile",
"account_profit_unload_fraction",
"account_profit_unload_drawdown_fraction",
"account_profit_unload_peak_drawdown_fraction",
"account_pnl_trailing_activation_rate",
"account_pnl_trailing_stop_rate",
"account_averaging_percentile",
"account_averaging_near_trough_fraction",
"account_averaging_falling_guard_fraction",
"account_averaging_budget_scale",
"ema_exit_trailing_min_pullback",
"ema_exit_trailing_max_pullback",
"hard_stop_loss_pct",
"hard_stop_loss_min_emergency_pct",
"hard_stop_loss_atr_max_pct",
"soft_defensive_exit_min_drawdown",
"soft_defensive_exit_btc_against_return",
"soft_defensive_exit_initial_fraction",
"soft_defensive_exit_step_fraction",
"soft_defensive_exit_max_fraction",
"controlled_loss_max_position_fraction",
"controlled_loss_min_move_fraction",
"controlled_loss_volatility_reprice_min_move_delta",
):
value = getattr(profile.strategy, setting_name)
if value < 0.0 or value > 1.0:
raise ValueError(
f"{profile.name}.STRATEGY.{setting_name} must be between 0 and 1"
)
if profile.strategy.ema_exit_trailing_atr_multiplier < 0:
raise ValueError(
f"{profile.name}.STRATEGY.ema_exit_trailing_atr_multiplier must be non-negative"
)
if (
profile.strategy.ema_exit_trailing_max_pullback > 0.0
and profile.strategy.ema_exit_trailing_min_pullback
> profile.strategy.ema_exit_trailing_max_pullback
):
raise ValueError(
f"{profile.name}.STRATEGY.ema_exit_trailing_min_pullback "
"must be <= ema_exit_trailing_max_pullback"
)
if (
profile.strategy.account_pnl_trailing_enabled
and profile.strategy.account_pnl_trailing_stop_rate
> profile.strategy.account_pnl_trailing_activation_rate
):
_add_config_warning(
f"{profile.name}: account_pnl_trailing_stop_rate is above activation_rate; "
"global trailing may close immediately after activation"
)
if (
profile.strategy.hard_stop_loss_enabled
and profile.strategy.hard_stop_loss_pct <= 0
):
raise ValueError(
f"{profile.name}.STRATEGY.hard_stop_loss_pct must be positive when hard stop is enabled"
)
if profile.strategy.soft_defensive_exit_confirmations < 1:
raise ValueError(
f"{profile.name}.STRATEGY.soft_defensive_exit_confirmations must be at least 1"
)
if profile.strategy.hard_stop_loss_atr_multiplier < 0:
raise ValueError(
f"{profile.name}.STRATEGY.hard_stop_loss_atr_multiplier must be non-negative"
)
if profile.strategy.controlled_loss_volatility_reference < 0:
raise ValueError(
f"{profile.name}.STRATEGY.controlled_loss_volatility_reference must be non-negative"
)
if profile.strategy.controlled_loss_volatility_trigger_multiplier < 0:
raise ValueError(
f"{profile.name}.STRATEGY.controlled_loss_volatility_trigger_multiplier must be non-negative"
)
if profile.strategy.controlled_loss_volatility_max_speed_multiplier < 1:
raise ValueError(
f"{profile.name}.STRATEGY.controlled_loss_volatility_max_speed_multiplier must be at least 1"
)
if profile.strategy.controlled_loss_volatility_exponent < 1:
raise ValueError(
f"{profile.name}.STRATEGY.controlled_loss_volatility_exponent must be at least 1"
)
if profile.risk.max_position_notional_fraction > 0.03 + 1e-12:
_add_config_warning(
f"{profile.name}: live max_position_notional_fraction="
f"{profile.risk.max_position_notional_fraction:.4f} is above the conservative 0.0300 launch cap"
)
if profile.risk.max_total_notional_fraction > 0.50 + 1e-12:
_add_config_warning(
f"{profile.name}: live max_total_notional_fraction="
f"{profile.risk.max_total_notional_fraction:.4f} is above the conservative 0.5000 launch cap"
)
if profile.strategy.ema_max_averaging_stages > 2:
_add_config_warning(
f"{profile.name}: live ema_max_averaging_stages="
f"{profile.strategy.ema_max_averaging_stages} is above the conservative launch cap of 2"
)
def _api_credentials_for_account(profile: str, suffix: str = "") -> ApiCredentials:
suffix = str(suffix or "").strip()
if not suffix:
return ApiCredentials(
api_key=_first_env("HTX_API_KEY", "API_KEY", profile=profile),
api_secret=_first_env("HTX_API_SECRET", "API_SECRET", profile=profile),
)
return ApiCredentials(
api_key=_first_env(
f"HTX_API_KEY_{suffix}",
f"HTX_API{suffix}_KEY",
f"HTX_API_{suffix}_KEY",
f"HTX_{suffix}_API_KEY",
"HTX_SECONDARY_API_KEY",
f"API_KEY_{suffix}",
"SECONDARY_API_KEY",
profile=profile,
),
api_secret=_first_env(
f"HTX_API_SECRET_{suffix}",
f"HTX_API{suffix}_SECRET",
f"HTX_API_{suffix}_SECRET",
f"HTX_{suffix}_API_SECRET",
"HTX_SECONDARY_API_SECRET",
f"API_SECRET_{suffix}",
"SECONDARY_API_SECRET",
profile=profile,
),
)
def _validate_api_account_coins(
accounts: Tuple[ApiAccountSettings, ...], profile: str
) -> None:
owners = {}
for account in accounts:
for coin in account.coins:
previous = owners.get(coin)
if previous and previous != account.name:
raise ValueError(
f"{profile}: coin {coin!r} is assigned to multiple HTX API accounts "
f"({previous}, {account.name})"
)
owners[coin] = account.name
def _make_api_accounts(
profile: str, primary_credentials: ApiCredentials, fallback_coins: Tuple[str, ...]
) -> Tuple[ApiAccountSettings, ...]:
primary_coins = _configured_account_coins(profile, "", fallback_coins)
accounts = [
ApiAccountSettings(
name="primary",
api_credentials=primary_credentials,
coins=primary_coins,
)
]
secondary_credentials = _api_credentials_for_account(profile, "2")
secondary_coins = _configured_account_coins(profile, "2", ())
if (
secondary_coins
or secondary_credentials.api_key
or secondary_credentials.api_secret
):
accounts.append(
ApiAccountSettings(
name="secondary",
api_credentials=secondary_credentials,
coins=secondary_coins,
)
)
resolved = tuple(accounts)
_validate_api_account_coins(resolved, profile)
return resolved