1use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::Path;
14use tracing::{debug, info, warn};
15
16const CONFIG_PATHS: &[&str] = &[
18 "config/janus.toml",
19 "/etc/janus/janus.toml",
20 "janus.toml",
21 "infrastructure/config/janus/janus.toml",
22];
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30#[serde(default, deny_unknown_fields)]
31pub struct Config {
32 pub service: ServiceConfig,
34
35 pub ports: PortsConfig,
37
38 pub host: HostConfig,
40
41 pub modules: ModulesConfig,
43
44 pub redis: RedisConfig,
46
47 pub database: DatabaseConfig,
49
50 pub questdb: QuestDbConfig,
52
53 pub forward: ForwardConfig,
55
56 pub risk: RiskConfig,
58
59 pub backward: BackwardConfig,
61
62 pub cns: CnsConfig,
64
65 pub market: MarketConfig,
67
68 pub assets: AssetsConfig,
70
71 pub trading: TradingConfig,
73
74 pub logging: LoggingConfig,
76
77 pub tracing: TracingConfig,
79
80 pub metrics: MetricsConfig,
82
83 pub alerting: AlertingConfig,
85
86 pub param_reload: ParamReloadConfig,
88
89 pub features: FeaturesConfig,
91
92 pub security: SecurityConfig,
94
95 pub advanced: AdvancedConfig,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(default)]
105pub struct ServiceConfig {
106 pub name: String,
108 pub version: String,
110 pub environment: String,
112}
113
114impl Default for ServiceConfig {
115 fn default() -> Self {
116 Self {
117 name: "janus".to_string(),
118 version: env!("CARGO_PKG_VERSION").to_string(),
119 environment: "development".to_string(),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(default)]
126pub struct PortsConfig {
127 pub http: u16,
129 pub grpc: u16,
131 pub websocket: u16,
133 pub metrics: u16,
135}
136
137impl Default for PortsConfig {
138 fn default() -> Self {
139 Self {
140 http: 8080,
141 grpc: 50051,
142 websocket: 8081,
143 metrics: 9090,
144 }
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(default)]
150pub struct HostConfig {
151 pub bind: String,
153 pub public: String,
155}
156
157impl Default for HostConfig {
158 fn default() -> Self {
159 Self {
160 bind: "0.0.0.0".to_string(),
161 public: "localhost".to_string(),
162 }
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(default)]
168pub struct ModulesConfig {
169 pub forward: bool,
171 pub backward: bool,
173 pub cns: bool,
175 pub api: bool,
177 pub data: bool,
179 pub websocket: bool,
181 pub grpc: bool,
183 pub metrics: bool,
185}
186
187impl Default for ModulesConfig {
188 fn default() -> Self {
189 Self {
190 forward: true,
191 backward: true,
192 cns: true,
193 api: true,
194 data: true,
195 websocket: true,
196 grpc: true,
197 metrics: true,
198 }
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(default)]
204pub struct RedisConfig {
205 pub url: String,
207 pub max_connections: u32,
209 pub min_connections: u32,
211 pub connect_timeout_secs: u64,
213 pub param_channel: String,
215 pub signal_channel: String,
217}
218
219impl Default for RedisConfig {
220 fn default() -> Self {
221 Self {
222 url: "redis://localhost:6379/0".to_string(),
223 max_connections: 10,
224 min_connections: 2,
225 connect_timeout_secs: 10,
226 param_channel: "fks:params".to_string(),
227 signal_channel: "fks:signals".to_string(),
228 }
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(default)]
234pub struct DatabaseConfig {
235 pub url: String,
237 pub max_connections: u32,
239 pub min_connections: u32,
241 pub connect_timeout_secs: u64,
243 pub idle_timeout_secs: u64,
245 pub max_lifetime_secs: u64,
247 pub enable_logging: bool,
249}
250
251impl Default for DatabaseConfig {
252 fn default() -> Self {
253 Self {
254 url: "postgresql://postgres:postgres@localhost:5432/janus".to_string(),
255 max_connections: 10,
256 min_connections: 2,
257 connect_timeout_secs: 30,
258 idle_timeout_secs: 600,
259 max_lifetime_secs: 1800,
260 enable_logging: false,
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[serde(default)]
267pub struct QuestDbConfig {
268 pub host: String,
270 pub ilp_port: u16,
272 pub http_port: u16,
274 pub pg_port: u16,
276}
277
278impl Default for QuestDbConfig {
279 fn default() -> Self {
280 Self {
281 host: "localhost".to_string(),
282 ilp_port: 9009,
283 http_port: 9000,
284 pg_port: 8812,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(default)]
291pub struct ForwardConfig {
292 pub signal_interval_secs: u64,
294 pub ml_model_path: String,
296 pub enable_ml_inference: bool,
298 pub signals: SignalConfig,
300 pub execution: ExecutionConfig,
302 pub indicators: IndicatorsConfig,
304 pub strategies: StrategiesConfig,
306}
307
308impl Default for ForwardConfig {
309 fn default() -> Self {
310 Self {
311 signal_interval_secs: 5,
312 ml_model_path: "/models".to_string(),
313 enable_ml_inference: false,
314 signals: SignalConfig::default(),
315 execution: ExecutionConfig::default(),
316 indicators: IndicatorsConfig::default(),
317 strategies: StrategiesConfig::default(),
318 }
319 }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(default)]
324pub struct SignalConfig {
325 pub min_confidence: f64,
327 pub min_strength: f64,
329 pub max_age_secs: u64,
331 pub enable_quality_filter: bool,
333 pub batch_size: usize,
335}
336
337impl Default for SignalConfig {
338 fn default() -> Self {
339 Self {
340 min_confidence: 0.6,
341 min_strength: 0.5,
342 max_age_secs: 300,
343 enable_quality_filter: true,
344 batch_size: 100,
345 }
346 }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(default)]
351pub struct ExecutionConfig {
352 pub enabled: bool,
354 pub endpoint: String,
356 pub connect_timeout_secs: u64,
358 pub request_timeout_secs: u64,
360 pub enable_tls: bool,
362 pub max_retries: u32,
364 pub retry_backoff_ms: u64,
366}
367
368impl Default for ExecutionConfig {
369 fn default() -> Self {
370 Self {
371 enabled: false,
372 endpoint: "http://execution:50052".to_string(),
373 connect_timeout_secs: 10,
374 request_timeout_secs: 30,
375 enable_tls: false,
376 max_retries: 3,
377 retry_backoff_ms: 100,
378 }
379 }
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
383#[serde(default)]
384pub struct IndicatorsConfig {
385 pub ema_periods: Vec<u32>,
387 pub rsi_period: u32,
389 pub rsi_overbought: f64,
391 pub rsi_oversold: f64,
393 pub macd_fast_period: u32,
395 pub macd_slow_period: u32,
397 pub macd_signal_period: u32,
399 pub bollinger_period: u32,
401 pub bollinger_std_dev: f64,
403 pub atr_period: u32,
405 pub volume_ma_period: u32,
407}
408
409impl Default for IndicatorsConfig {
410 fn default() -> Self {
411 Self {
412 ema_periods: vec![9, 21, 50, 200],
413 rsi_period: 14,
414 rsi_overbought: 70.0,
415 rsi_oversold: 30.0,
416 macd_fast_period: 12,
417 macd_slow_period: 26,
418 macd_signal_period: 9,
419 bollinger_period: 20,
420 bollinger_std_dev: 2.0,
421 atr_period: 14,
422 volume_ma_period: 20,
423 }
424 }
425}
426
427#[derive(Debug, Clone, Default, Serialize, Deserialize)]
428#[serde(default)]
429pub struct StrategiesConfig {
430 pub weights: StrategyWeights,
432 pub consensus: ConsensusConfig,
434 pub ema_crossover: EmaCrossoverConfig,
436 pub rsi_reversal: RsiReversalConfig,
438 pub macd_momentum: MacdMomentumConfig,
440 pub bollinger_breakout: BollingerBreakoutConfig,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
445#[serde(default)]
446pub struct StrategyWeights {
447 pub ema_crossover: f64,
448 pub rsi_reversal: f64,
449 pub macd_momentum: f64,
450 pub bollinger_breakout: f64,
451}
452
453impl Default for StrategyWeights {
454 fn default() -> Self {
455 Self {
456 ema_crossover: 1.0,
457 rsi_reversal: 1.0,
458 macd_momentum: 1.0,
459 bollinger_breakout: 1.0,
460 }
461 }
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
465#[serde(default)]
466pub struct ConsensusConfig {
467 pub min_agreement: f64,
468 pub min_strategies: u32,
469}
470
471impl Default for ConsensusConfig {
472 fn default() -> Self {
473 Self {
474 min_agreement: 0.6,
475 min_strategies: 2,
476 }
477 }
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
481#[serde(default)]
482pub struct EmaCrossoverConfig {
483 pub enabled: bool,
484 pub fast_period: u32,
485 pub slow_period: u32,
486 pub min_spread_pct: f64,
487}
488
489impl Default for EmaCrossoverConfig {
490 fn default() -> Self {
491 Self {
492 enabled: true,
493 fast_period: 9,
494 slow_period: 21,
495 min_spread_pct: 0.1,
496 }
497 }
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
501#[serde(default)]
502pub struct RsiReversalConfig {
503 pub enabled: bool,
504 pub period: u32,
505 pub overbought_threshold: f64,
506 pub oversold_threshold: f64,
507 pub confirmation_candles: u32,
508}
509
510impl Default for RsiReversalConfig {
511 fn default() -> Self {
512 Self {
513 enabled: true,
514 period: 14,
515 overbought_threshold: 70.0,
516 oversold_threshold: 30.0,
517 confirmation_candles: 1,
518 }
519 }
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(default)]
524pub struct MacdMomentumConfig {
525 pub enabled: bool,
526 pub fast_period: u32,
527 pub slow_period: u32,
528 pub signal_period: u32,
529 pub histogram_threshold: f64,
530}
531
532impl Default for MacdMomentumConfig {
533 fn default() -> Self {
534 Self {
535 enabled: true,
536 fast_period: 12,
537 slow_period: 26,
538 signal_period: 9,
539 histogram_threshold: 0.0,
540 }
541 }
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
545#[serde(default)]
546pub struct BollingerBreakoutConfig {
547 pub enabled: bool,
548 pub period: u32,
549 pub std_dev: f64,
550 pub require_close_outside: bool,
551}
552
553impl Default for BollingerBreakoutConfig {
554 fn default() -> Self {
555 Self {
556 enabled: true,
557 period: 20,
558 std_dev: 2.0,
559 require_close_outside: true,
560 }
561 }
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
565#[serde(default)]
566pub struct RiskConfig {
567 pub account_balance: f64,
569 pub max_position_size_pct: f64,
571 pub max_portfolio_risk_pct: f64,
573 pub max_open_positions: u32,
575 pub max_daily_loss: f64,
577 pub max_position_hold_hours: u32,
579 pub stop_loss: StopLossConfig,
581 pub take_profit: TakeProfitConfig,
583 pub position_sizing: PositionSizingConfig,
585}
586
587impl Default for RiskConfig {
588 fn default() -> Self {
589 Self {
590 account_balance: 100000.0,
591 max_position_size_pct: 0.02,
592 max_portfolio_risk_pct: 0.10,
593 max_open_positions: 10,
594 max_daily_loss: 1000.0,
595 max_position_hold_hours: 24,
596 stop_loss: StopLossConfig::default(),
597 take_profit: TakeProfitConfig::default(),
598 position_sizing: PositionSizingConfig::default(),
599 }
600 }
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
604#[serde(default)]
605pub struct StopLossConfig {
606 pub default_pct: f64,
607 pub use_atr: bool,
608 pub atr_multiplier: f64,
609 pub min_distance_pct: f64,
610 pub max_distance_pct: f64,
611}
612
613impl Default for StopLossConfig {
614 fn default() -> Self {
615 Self {
616 default_pct: 0.02,
617 use_atr: true,
618 atr_multiplier: 2.0,
619 min_distance_pct: 0.005,
620 max_distance_pct: 0.10,
621 }
622 }
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize)]
626#[serde(default)]
627pub struct TakeProfitConfig {
628 pub risk_reward_ratio: f64,
629 pub enable_trailing: bool,
630 pub trailing_distance_pct: f64,
631}
632
633impl Default for TakeProfitConfig {
634 fn default() -> Self {
635 Self {
636 risk_reward_ratio: 2.0,
637 enable_trailing: false,
638 trailing_distance_pct: 0.01,
639 }
640 }
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
644#[serde(default)]
645pub struct PositionSizingConfig {
646 pub method: String,
648 pub fixed_size_usd: f64,
649 pub risk_per_trade_pct: f64,
650 pub kelly_fraction: f64,
651}
652
653impl Default for PositionSizingConfig {
654 fn default() -> Self {
655 Self {
656 method: "risk_based".to_string(),
657 fixed_size_usd: 1000.0,
658 risk_per_trade_pct: 0.01,
659 kelly_fraction: 0.25,
660 }
661 }
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
665#[serde(default)]
666pub struct BackwardConfig {
667 pub worker_threads: usize,
669 pub enable_scheduler: bool,
671 pub persistence: PersistenceConfig,
673 pub analytics: AnalyticsConfig,
675 pub retention: RetentionConfig,
677}
678
679impl Default for BackwardConfig {
680 fn default() -> Self {
681 Self {
682 worker_threads: 4,
683 enable_scheduler: true,
684 persistence: PersistenceConfig::default(),
685 analytics: AnalyticsConfig::default(),
686 retention: RetentionConfig::default(),
687 }
688 }
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
692#[serde(default)]
693pub struct PersistenceConfig {
694 pub batch_size: usize,
695 pub flush_interval_secs: u64,
696 pub enable_wal: bool,
697}
698
699impl Default for PersistenceConfig {
700 fn default() -> Self {
701 Self {
702 batch_size: 100,
703 flush_interval_secs: 5,
704 enable_wal: true,
705 }
706 }
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
710#[serde(default)]
711pub struct AnalyticsConfig {
712 pub update_interval_secs: u64,
713 pub performance_window_hours: u32,
714 pub enable_trade_analysis: bool,
715}
716
717impl Default for AnalyticsConfig {
718 fn default() -> Self {
719 Self {
720 update_interval_secs: 60,
721 performance_window_hours: 24,
722 enable_trade_analysis: true,
723 }
724 }
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize)]
728#[serde(default)]
729pub struct RetentionConfig {
730 pub signals_days: u32,
731 pub trades_days: u32,
732 pub metrics_days: u32,
733}
734
735impl Default for RetentionConfig {
736 fn default() -> Self {
737 Self {
738 signals_days: 90,
739 trades_days: 365,
740 metrics_days: 30,
741 }
742 }
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize)]
746#[serde(default)]
747pub struct CnsConfig {
748 pub health_check_interval_secs: u64,
750 pub enable_reflexes: bool,
752 pub verbose_logging: bool,
754 pub startup_grace_period_secs: u64,
756 pub max_concurrent_probes: u32,
758 pub probe_retry_attempts: u32,
760 pub endpoints: CnsEndpointsConfig,
762 pub circuit_breakers: HashMap<String, CircuitBreakerConfig>,
764}
765
766impl Default for CnsConfig {
767 fn default() -> Self {
768 Self {
769 health_check_interval_secs: 10,
770 enable_reflexes: true,
771 verbose_logging: false,
772 startup_grace_period_secs: 30,
773 max_concurrent_probes: 10,
774 probe_retry_attempts: 2,
775 endpoints: CnsEndpointsConfig::default(),
776 circuit_breakers: HashMap::new(),
777 }
778 }
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
782#[serde(default)]
783pub struct CnsEndpointsConfig {
784 pub forward_service: String,
785 pub backward_service: String,
786 pub gateway_service: String,
787 pub redis: String,
788 pub qdrant: String,
789 pub shared_memory_path: String,
790 pub neuromorphic: NeuromorphicConfig,
791}
792
793impl Default for CnsEndpointsConfig {
794 fn default() -> Self {
795 Self {
796 forward_service: "http://localhost:8080/api/v1".to_string(),
797 backward_service: "http://localhost:8082".to_string(),
798 gateway_service: "http://localhost:8000".to_string(),
799 redis: "redis://localhost:6379".to_string(),
800 qdrant: String::new(),
801 shared_memory_path: "/dev/shm/janus_forward_backward".to_string(),
802 neuromorphic: NeuromorphicConfig::default(),
803 }
804 }
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize)]
808#[serde(default)]
809pub struct NeuromorphicConfig {
810 pub enabled: bool,
811 pub base_url: String,
812}
813
814impl Default for NeuromorphicConfig {
815 fn default() -> Self {
816 Self {
817 enabled: false,
818 base_url: "http://localhost:8090".to_string(),
819 }
820 }
821}
822
823#[derive(Debug, Clone, Serialize, Deserialize)]
824#[serde(default)]
825pub struct CircuitBreakerConfig {
826 pub failure_threshold: u32,
827 pub failure_window_secs: u64,
828 pub recovery_timeout_secs: u64,
829 pub success_threshold: u32,
830}
831
832impl Default for CircuitBreakerConfig {
833 fn default() -> Self {
834 Self {
835 failure_threshold: 5,
836 failure_window_secs: 60,
837 recovery_timeout_secs: 30,
838 success_threshold: 3,
839 }
840 }
841}
842
843#[derive(Debug, Clone, Serialize, Deserialize)]
844#[serde(default)]
845pub struct MarketConfig {
846 pub exchange: String,
848 pub update_interval_ms: u64,
850 pub enable_orderbook: bool,
852 pub orderbook_depth: u32,
854 pub timeframes: TimeframesConfig,
856}
857
858impl Default for MarketConfig {
859 fn default() -> Self {
860 Self {
861 exchange: "kraken".to_string(),
862 update_interval_ms: 1000,
863 enable_orderbook: true,
864 orderbook_depth: 10,
865 timeframes: TimeframesConfig::default(),
866 }
867 }
868}
869
870#[derive(Debug, Clone, Serialize, Deserialize)]
871#[serde(default)]
872pub struct AssetsConfig {
873 pub enabled: Vec<String>,
875 pub default_quote: String,
877 pub optimize_assets: Vec<String>,
879 pub priority_assets: Vec<String>,
881 #[serde(flatten)]
883 pub configs: HashMap<String, AssetConfig>,
884}
885
886impl Default for AssetsConfig {
887 fn default() -> Self {
888 Self {
889 enabled: vec!["BTC".to_string(), "ETH".to_string(), "SOL".to_string()],
890 default_quote: "USD".to_string(),
891 optimize_assets: vec!["BTC".to_string(), "ETH".to_string(), "SOL".to_string()],
892 priority_assets: vec!["BTC".to_string(), "ETH".to_string()],
893 configs: HashMap::new(),
894 }
895 }
896}
897
898impl AssetsConfig {
899 pub fn enabled_symbols(&self) -> Vec<String> {
901 self.enabled
902 .iter()
903 .map(|asset| format!("{}/{}", asset, self.default_quote))
904 .collect()
905 }
906
907 pub fn get(&self, asset: &str) -> Option<&AssetConfig> {
909 self.configs.get(asset)
910 }
911
912 pub fn is_enabled(&self, asset: &str) -> bool {
914 self.enabled.contains(&asset.to_string())
915 }
916
917 pub fn is_priority(&self, asset: &str) -> bool {
919 self.priority_assets.contains(&asset.to_string())
920 }
921}
922
923#[derive(Debug, Clone, Serialize, Deserialize)]
924#[serde(default)]
925pub struct AssetConfig {
926 pub symbol: String,
928 pub enabled: bool,
930 pub max_position_size_pct: f64,
932 pub max_leverage: f64,
934 pub min_order_size: f64,
936 pub max_order_size: f64,
938 pub atr_multiplier: f64,
940 pub rsi_overbought: f64,
942 pub rsi_oversold: f64,
944 pub exchanges: HashMap<String, ExchangeAssetConfig>,
946}
947
948impl Default for AssetConfig {
949 fn default() -> Self {
950 Self {
951 symbol: String::new(),
952 enabled: true,
953 max_position_size_pct: 0.02,
954 max_leverage: 2.0,
955 min_order_size: 0.001,
956 max_order_size: 100.0,
957 atr_multiplier: 2.0,
958 rsi_overbought: 70.0,
959 rsi_oversold: 30.0,
960 exchanges: HashMap::new(),
961 }
962 }
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize)]
966#[serde(default)]
967pub struct ExchangeAssetConfig {
968 pub pair: String,
970 pub min_order: f64,
972 pub fee_tier: Option<String>,
974 pub category: Option<String>,
976}
977
978impl Default for ExchangeAssetConfig {
979 fn default() -> Self {
980 Self {
981 pair: String::new(),
982 min_order: 0.001,
983 fee_tier: None,
984 category: None,
985 }
986 }
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
990#[serde(default)]
991pub struct TimeframesConfig {
992 pub enabled: Vec<String>,
993 pub primary: String,
994}
995
996impl Default for TimeframesConfig {
997 fn default() -> Self {
998 Self {
999 enabled: vec![
1000 "1m".to_string(),
1001 "5m".to_string(),
1002 "15m".to_string(),
1003 "1h".to_string(),
1004 "4h".to_string(),
1005 "1d".to_string(),
1006 ],
1007 primary: "5m".to_string(),
1008 }
1009 }
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013#[serde(default)]
1014pub struct TradingConfig {
1015 pub mode: String,
1017 pub real_orders_enabled: bool,
1019 pub dry_run: bool,
1021 pub simulation: SimulationConfig,
1023 pub orders: OrdersConfig,
1025}
1026
1027impl Default for TradingConfig {
1028 fn default() -> Self {
1029 Self {
1030 mode: "paper".to_string(),
1031 real_orders_enabled: false,
1032 dry_run: true,
1033 simulation: SimulationConfig::default(),
1034 orders: OrdersConfig::default(),
1035 }
1036 }
1037}
1038
1039#[derive(Debug, Clone, Serialize, Deserialize)]
1040#[serde(default)]
1041pub struct SimulationConfig {
1042 pub initial_balance: f64,
1043 pub slippage_bps: u32,
1044 pub fee_bps: u32,
1045 pub fill_delay_ms: u64,
1046 pub enable_slippage: bool,
1047}
1048
1049impl Default for SimulationConfig {
1050 fn default() -> Self {
1051 Self {
1052 initial_balance: 100000.0,
1053 slippage_bps: 5,
1054 fee_bps: 10,
1055 fill_delay_ms: 200,
1056 enable_slippage: true,
1057 }
1058 }
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize)]
1062#[serde(default)]
1063pub struct OrdersConfig {
1064 pub default_type: String,
1065 pub default_tif: String,
1066 pub min_size_usd: f64,
1067 pub max_size_usd: f64,
1068 pub max_slippage_bps: u32,
1069}
1070
1071impl Default for OrdersConfig {
1072 fn default() -> Self {
1073 Self {
1074 default_type: "limit".to_string(),
1075 default_tif: "gtc".to_string(),
1076 min_size_usd: 10.0,
1077 max_size_usd: 100000.0,
1078 max_slippage_bps: 50,
1079 }
1080 }
1081}
1082
1083#[derive(Debug, Clone, Serialize, Deserialize)]
1084#[serde(default)]
1085pub struct LoggingConfig {
1086 pub level: String,
1088 pub format: String,
1090 pub console: bool,
1092 pub file_enabled: bool,
1094 pub file_path: String,
1096 pub sql_logging: bool,
1098}
1099
1100impl Default for LoggingConfig {
1101 fn default() -> Self {
1102 Self {
1103 level: "info".to_string(),
1104 format: "json".to_string(),
1105 console: true,
1106 file_enabled: false,
1107 file_path: "/var/log/janus/janus.log".to_string(),
1108 sql_logging: false,
1109 }
1110 }
1111}
1112
1113#[derive(Debug, Clone, Serialize, Deserialize)]
1114#[serde(default)]
1115pub struct TracingConfig {
1116 pub enabled: bool,
1118 pub jaeger_endpoint: String,
1120 pub sampling_rate: f64,
1122}
1123
1124impl Default for TracingConfig {
1125 fn default() -> Self {
1126 Self {
1127 enabled: false,
1128 jaeger_endpoint: "http://jaeger:14268/api/traces".to_string(),
1129 sampling_rate: 0.1,
1130 }
1131 }
1132}
1133
1134#[derive(Debug, Clone, Serialize, Deserialize)]
1135#[serde(default)]
1136pub struct MetricsConfig {
1137 pub endpoint: String,
1139 pub detailed_histograms: bool,
1141 pub labels: HashMap<String, String>,
1143}
1144
1145impl Default for MetricsConfig {
1146 fn default() -> Self {
1147 let mut labels = HashMap::new();
1148 labels.insert("service".to_string(), "janus".to_string());
1149 labels.insert("environment".to_string(), "development".to_string());
1150 Self {
1151 endpoint: "/metrics".to_string(),
1152 detailed_histograms: true,
1153 labels,
1154 }
1155 }
1156}
1157
1158#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1159#[serde(default)]
1160pub struct AlertingConfig {
1161 pub enabled: bool,
1163 pub discord: DiscordConfig,
1165 pub slack: SlackConfig,
1167 pub email: EmailConfig,
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize)]
1172#[serde(default)]
1173pub struct DiscordConfig {
1174 pub webhook_url: String,
1175 pub enabled: bool,
1176 pub notify_on_signal: bool,
1177 pub notify_on_fill: bool,
1178 pub notify_on_error: bool,
1179}
1180
1181impl Default for DiscordConfig {
1182 fn default() -> Self {
1183 Self {
1184 webhook_url: String::new(),
1185 enabled: false,
1186 notify_on_signal: true,
1187 notify_on_fill: true,
1188 notify_on_error: true,
1189 }
1190 }
1191}
1192
1193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1194#[serde(default)]
1195pub struct SlackConfig {
1196 pub webhook_url: String,
1197 pub enabled: bool,
1198}
1199
1200#[derive(Debug, Clone, Serialize, Deserialize)]
1201#[serde(default)]
1202pub struct EmailConfig {
1203 pub enabled: bool,
1204 pub smtp_host: String,
1205 pub smtp_port: u16,
1206 pub recipients: Vec<String>,
1207}
1208
1209impl Default for EmailConfig {
1210 fn default() -> Self {
1211 Self {
1212 enabled: false,
1213 smtp_host: "localhost".to_string(),
1214 smtp_port: 587,
1215 recipients: Vec::new(),
1216 }
1217 }
1218}
1219
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1221#[serde(default)]
1222pub struct ParamReloadConfig {
1223 pub enabled: bool,
1225 pub instance_id: String,
1227 pub reconnect_delay_ms: u64,
1229 pub max_retries: u32,
1231}
1232
1233impl Default for ParamReloadConfig {
1234 fn default() -> Self {
1235 Self {
1236 enabled: true,
1237 instance_id: "default".to_string(),
1238 reconnect_delay_ms: 5000,
1239 max_retries: 0,
1240 }
1241 }
1242}
1243
1244#[derive(Debug, Clone, Serialize, Deserialize)]
1245#[serde(default)]
1246pub struct FeaturesConfig {
1247 pub enable_gaf: bool,
1249 pub gaf_image_size: u32,
1251 pub gaf_method: String,
1253 pub lookback_windows: Vec<u32>,
1255 pub normalize: bool,
1257 pub normalization_method: String,
1259}
1260
1261impl Default for FeaturesConfig {
1262 fn default() -> Self {
1263 Self {
1264 enable_gaf: false,
1265 gaf_image_size: 32,
1266 gaf_method: "summation".to_string(),
1267 lookback_windows: vec![5, 10, 20, 50, 100],
1268 normalize: true,
1269 normalization_method: "zscore".to_string(),
1270 }
1271 }
1272}
1273
1274#[derive(Debug, Clone, Serialize, Deserialize)]
1275#[serde(default)]
1276pub struct SecurityConfig {
1277 pub cors_origins: String,
1279 pub enable_rate_limit: bool,
1281 pub rate_limit_rps: u32,
1283 pub api_key_header: String,
1285}
1286
1287impl Default for SecurityConfig {
1288 fn default() -> Self {
1289 Self {
1290 cors_origins: "*".to_string(),
1291 enable_rate_limit: true,
1292 rate_limit_rps: 100,
1293 api_key_header: "X-API-Key".to_string(),
1294 }
1295 }
1296}
1297
1298#[derive(Debug, Clone, Serialize, Deserialize)]
1299#[serde(default)]
1300pub struct AdvancedConfig {
1301 pub tokio_worker_threads: usize,
1303 pub http_timeout_secs: u64,
1305 pub db_query_timeout_secs: u64,
1307 pub signal_buffer_size: usize,
1309 pub order_buffer_size: usize,
1311 pub experimental_features: bool,
1313}
1314
1315impl Default for AdvancedConfig {
1316 fn default() -> Self {
1317 Self {
1318 tokio_worker_threads: 0,
1319 http_timeout_secs: 30,
1320 db_query_timeout_secs: 30,
1321 signal_buffer_size: 1000,
1322 order_buffer_size: 500,
1323 experimental_features: false,
1324 }
1325 }
1326}
1327
1328impl Config {
1333 pub fn load() -> anyhow::Result<Self> {
1341 let mut config = Self::load_from_file()?;
1343
1344 config.apply_env_overrides();
1346
1347 #[cfg(feature = "redis")]
1351 config.apply_redis_overlay_blocking();
1352
1353 Ok(config)
1354 }
1355
1356 pub fn from_env() -> anyhow::Result<Self> {
1358 let mut config = Self::default();
1359 config.apply_env_overrides();
1360 Ok(config)
1361 }
1362
1363 pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
1365 let contents = std::fs::read_to_string(path.as_ref())?;
1366 let mut config: Config = toml::from_str(&contents)?;
1367 config.apply_env_overrides();
1368 Ok(config)
1369 }
1370
1371 fn load_from_file() -> anyhow::Result<Self> {
1373 if let Ok(config_path) = std::env::var("JANUS_CONFIG_PATH") {
1375 if Path::new(&config_path).exists() {
1376 info!(
1377 "Loading configuration from JANUS_CONFIG_PATH: {}",
1378 config_path
1379 );
1380 let contents = std::fs::read_to_string(&config_path)?;
1381 return Ok(toml::from_str(&contents)?);
1382 } else {
1383 warn!("JANUS_CONFIG_PATH set but file not found: {}", config_path);
1384 }
1385 }
1386
1387 for path in CONFIG_PATHS {
1389 if Path::new(path).exists() {
1390 info!("Loading configuration from: {}", path);
1391 let contents = std::fs::read_to_string(path)?;
1392 return Ok(toml::from_str(&contents)?);
1393 }
1394 }
1395
1396 debug!("No config file found, using defaults");
1397 Ok(Self::default())
1398 }
1399
1400 fn apply_env_overrides(&mut self) {
1402 if let Ok(v) = std::env::var("JANUS_SERVICE_NAME") {
1404 self.service.name = v;
1405 }
1406 if let Ok(v) = std::env::var("JANUS_ENVIRONMENT") {
1407 self.service.environment = v;
1408 }
1409
1410 if let Ok(v) = std::env::var("JANUS_HTTP_PORT")
1412 && let Ok(port) = v.parse()
1413 {
1414 self.ports.http = port;
1415 }
1416 if let Ok(v) = std::env::var("JANUS_GRPC_PORT")
1417 && let Ok(port) = v.parse()
1418 {
1419 self.ports.grpc = port;
1420 }
1421 if let Ok(v) = std::env::var("JANUS_WS_PORT")
1422 && let Ok(port) = v.parse()
1423 {
1424 self.ports.websocket = port;
1425 }
1426 if let Ok(v) = std::env::var("JANUS_METRICS_PORT")
1427 && let Ok(port) = v.parse()
1428 {
1429 self.ports.metrics = port;
1430 }
1431
1432 if let Ok(v) = std::env::var("JANUS_HOST") {
1434 self.host.bind = v;
1435 }
1436
1437 if let Ok(v) = std::env::var("JANUS_ENABLE_FORWARD") {
1439 self.modules.forward = parse_bool(&v);
1440 }
1441 if let Ok(v) = std::env::var("JANUS_ENABLE_BACKWARD") {
1442 self.modules.backward = parse_bool(&v);
1443 }
1444 if let Ok(v) = std::env::var("JANUS_ENABLE_CNS") {
1445 self.modules.cns = parse_bool(&v);
1446 }
1447 if let Ok(v) = std::env::var("JANUS_ENABLE_API") {
1448 self.modules.api = parse_bool(&v);
1449 }
1450 if let Ok(v) = std::env::var("JANUS_ENABLE_DATA") {
1451 self.modules.data = parse_bool(&v);
1452 }
1453 if let Ok(v) = std::env::var("JANUS_ENABLE_WEBSOCKET") {
1454 self.modules.websocket = parse_bool(&v);
1455 }
1456 if let Ok(v) = std::env::var("JANUS_ENABLE_GRPC") {
1457 self.modules.grpc = parse_bool(&v);
1458 }
1459 if let Ok(v) = std::env::var("JANUS_ENABLE_METRICS") {
1460 self.modules.metrics = parse_bool(&v);
1461 }
1462
1463 if let Ok(v) = std::env::var("REDIS_URL") {
1465 self.redis.url = v;
1466 }
1467 if let Ok(v) = std::env::var("DATABASE_URL") {
1468 self.database.url = v;
1469 }
1470 if let Ok(v) = std::env::var("QUESTDB_HOST") {
1471 self.questdb.host = v;
1472 }
1473
1474 if let Ok(v) = std::env::var("JANUS_FORWARD_SIGNAL_INTERVAL")
1476 && let Ok(interval) = v.parse()
1477 {
1478 self.forward.signal_interval_secs = interval;
1479 }
1480 if let Ok(v) = std::env::var("JANUS_FORWARD_ML_MODEL_PATH") {
1481 self.forward.ml_model_path = v;
1482 }
1483
1484 if let Ok(v) = std::env::var("RISK_ACCOUNT_BALANCE")
1486 && let Ok(balance) = v.parse()
1487 {
1488 self.risk.account_balance = balance;
1489 }
1490 if let Ok(v) = std::env::var("RISK_MAX_POSITION_SIZE_PCT")
1491 && let Ok(pct) = v.parse()
1492 {
1493 self.risk.max_position_size_pct = pct;
1494 }
1495
1496 if let Ok(v) = std::env::var("JANUS_BACKWARD_PERSIST_BATCH_SIZE")
1498 && let Ok(size) = v.parse()
1499 {
1500 self.backward.persistence.batch_size = size;
1501 }
1502 if let Ok(v) = std::env::var("JANUS_BACKWARD_ANALYTICS_INTERVAL")
1503 && let Ok(interval) = v.parse()
1504 {
1505 self.backward.analytics.update_interval_secs = interval;
1506 }
1507
1508 if let Ok(v) = std::env::var("JANUS_CNS_HEALTH_INTERVAL")
1510 && let Ok(interval) = v.parse()
1511 {
1512 self.cns.health_check_interval_secs = interval;
1513 }
1514 if let Ok(v) = std::env::var("JANUS_CNS_AUTO_RECOVERY") {
1515 self.cns.enable_reflexes = parse_bool(&v);
1516 }
1517
1518 if let Ok(v) = std::env::var("OPTIMIZE_ASSETS") {
1520 self.assets.optimize_assets = v.split(',').map(|s| s.trim().to_string()).collect();
1521 }
1522 if let Ok(v) = std::env::var("ENABLED_ASSETS") {
1523 self.assets.enabled = v.split(',').map(|s| s.trim().to_string()).collect();
1524 }
1525 if let Ok(v) = std::env::var("TRADING_ASSETS") {
1526 self.assets.enabled = v.split(',').map(|s| s.trim().to_string()).collect();
1528 }
1529 if let Ok(v) = std::env::var("PRIORITY_ASSETS") {
1530 self.assets.priority_assets = v.split(',').map(|s| s.trim().to_string()).collect();
1531 }
1532 if let Ok(v) = std::env::var("DEFAULT_QUOTE_CURRENCY") {
1533 self.assets.default_quote = v;
1534 }
1535
1536 if let Ok(v) = std::env::var("PRIMARY_EXCHANGE") {
1538 self.market.exchange = v;
1539 }
1540
1541 if let Ok(v) = std::env::var("TRADING_MODE") {
1543 self.trading.mode = v;
1544 }
1545 if let Ok(v) = std::env::var("REAL_ORDERS_ENABLED") {
1546 self.trading.real_orders_enabled = parse_bool(&v);
1547 }
1548
1549 if let Ok(v) = std::env::var("JANUS_CORS_ORIGINS") {
1551 self.security.cors_origins = v;
1552 }
1553
1554 if let Ok(v) = std::env::var("RUST_LOG") {
1556 if v.contains("debug") {
1558 self.logging.level = "debug".to_string();
1559 } else if v.contains("trace") {
1560 self.logging.level = "trace".to_string();
1561 } else if v.contains("warn") {
1562 self.logging.level = "warn".to_string();
1563 } else if v.contains("error") {
1564 self.logging.level = "error".to_string();
1565 }
1566 }
1567 if let Ok(v) = std::env::var("LOG_FORMAT") {
1568 self.logging.format = v;
1569 }
1570 }
1571
1572 #[cfg(feature = "redis")]
1586 fn apply_redis_overlay_blocking(&mut self) {
1587 if matches!(
1589 std::env::var("JANUS_REDIS_OVERLAY").as_deref(),
1590 Ok("0" | "false" | "off" | "no")
1591 ) {
1592 return;
1593 }
1594
1595 let url = self.redis.url.clone();
1596 let key = std::env::var("JANUS_REDIS_CONFIG_KEY")
1597 .unwrap_or_else(|_| "fks:janus:config".to_string());
1598
1599 let fetched = std::thread::spawn(move || -> Option<String> {
1603 let rt = tokio::runtime::Builder::new_current_thread()
1604 .enable_all()
1605 .build()
1606 .ok()?;
1607 rt.block_on(async move {
1608 let client = redis::Client::open(url.as_str()).ok()?;
1609 let mut conn = client.get_multiplexed_async_connection().await.ok()?;
1610 redis::cmd("GET")
1611 .arg(&key)
1612 .query_async::<Option<String>>(&mut conn)
1613 .await
1614 .ok()
1615 .flatten()
1616 })
1617 })
1618 .join()
1619 .ok()
1620 .flatten();
1621
1622 let Some(json) = fetched else {
1623 debug!("Redis config overlay: key not present, using env/file config");
1624 return;
1625 };
1626
1627 let overlay: serde_json::Value = match serde_json::from_str(&json) {
1628 Ok(v) => v,
1629 Err(e) => {
1630 warn!("Redis config overlay: malformed JSON at {key} — {e}", key = "fks:janus:config");
1631 return;
1632 }
1633 };
1634
1635 let mut base = match serde_json::to_value(&*self) {
1639 Ok(v) => v,
1640 Err(e) => {
1641 warn!("Redis config overlay: failed to serialize current config — {e}");
1642 return;
1643 }
1644 };
1645 merge_json(&mut base, overlay);
1646
1647 match serde_json::from_value::<Config>(base) {
1648 Ok(merged) => {
1649 info!("Redis config overlay applied from key fks:janus:config");
1650 *self = merged;
1651 }
1652 Err(e) => {
1653 warn!("Redis config overlay: merged document failed to deserialize — {e}");
1654 }
1655 }
1656 }
1657
1658 pub fn is_production(&self) -> bool {
1660 self.service.environment == "production"
1661 }
1662
1663 pub fn cors_origins_list(&self) -> Vec<String> {
1665 self.security
1666 .cors_origins
1667 .split(',')
1668 .map(|s| s.trim().to_string())
1669 .collect()
1670 }
1671
1672 pub fn validate(&self) -> anyhow::Result<()> {
1674 if !self.modules.forward
1676 && !self.modules.backward
1677 && !self.modules.cns
1678 && !self.modules.api
1679 && !self.modules.data
1680 {
1681 anyhow::bail!("At least one module must be enabled");
1682 }
1683
1684 let ports = [
1686 self.ports.http,
1687 self.ports.grpc,
1688 self.ports.websocket,
1689 self.ports.metrics,
1690 ];
1691 let unique: std::collections::HashSet<_> = ports.iter().collect();
1692 if unique.len() != ports.len() {
1693 anyhow::bail!("All ports must be unique");
1694 }
1695
1696 let valid_modes = ["simulation", "paper", "live"];
1698 if !valid_modes.contains(&self.trading.mode.as_str()) {
1699 anyhow::bail!(
1700 "Invalid trading mode '{}'. Must be one of: {:?}",
1701 self.trading.mode,
1702 valid_modes
1703 );
1704 }
1705
1706 if self.trading.mode == "live" && self.trading.real_orders_enabled {
1708 warn!("⚠️ LIVE TRADING ENABLED - Real orders will be executed!");
1709 }
1710
1711 Ok(())
1712 }
1713
1714 pub fn to_toml(&self) -> anyhow::Result<String> {
1716 Ok(toml::to_string_pretty(self)?)
1717 }
1718
1719 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
1721 let contents = self.to_toml()?;
1722 std::fs::write(path, contents)?;
1723 Ok(())
1724 }
1725}
1726
1727fn parse_bool(s: &str) -> bool {
1732 matches!(s.to_lowercase().as_str(), "true" | "1" | "yes" | "on")
1733}
1734
1735#[cfg(feature = "redis")]
1741fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) {
1742 match (base, overlay) {
1743 (serde_json::Value::Object(base_map), serde_json::Value::Object(overlay_map)) => {
1744 for (k, v) in overlay_map {
1745 merge_json(base_map.entry(k).or_insert(serde_json::Value::Null), v);
1746 }
1747 }
1748 (slot, value) => {
1749 *slot = value;
1750 }
1751 }
1752}
1753
1754#[cfg(test)]
1759mod tests {
1760 use super::*;
1761
1762 #[test]
1763 fn test_default_config() {
1764 let config = Config::default();
1765 assert_eq!(config.ports.http, 8080);
1766 assert!(config.modules.forward);
1767 }
1768
1769 #[test]
1770 fn test_validate_config() {
1771 let config = Config::default();
1772 assert!(config.validate().is_ok());
1773 }
1774
1775 #[test]
1776 fn test_cors_origins_list() {
1777 let mut config = Config::default();
1778 config.security.cors_origins = "http://localhost:3000, http://localhost:8080".to_string();
1779 let origins = config.cors_origins_list();
1780 assert_eq!(origins.len(), 2);
1781 }
1782
1783 #[test]
1784 fn test_toml_serialization() {
1785 let config = Config::default();
1786 let toml_str = config.to_toml().unwrap();
1787 assert!(toml_str.contains("[service]"));
1788 assert!(toml_str.contains("[ports]"));
1789 }
1790
1791 #[test]
1792 fn test_toml_deserialization() {
1793 let toml_str = r#"
1794 [service]
1795 name = "test-janus"
1796 environment = "testing"
1797
1798 [ports]
1799 http = 9000
1800 grpc = 50052
1801 "#;
1802
1803 let config: Config = toml::from_str(toml_str).unwrap();
1804 assert_eq!(config.service.name, "test-janus");
1805 assert_eq!(config.service.environment, "testing");
1806 assert_eq!(config.ports.http, 9000);
1807 assert_eq!(config.ports.grpc, 50052);
1808 }
1809
1810 #[test]
1811 fn test_parse_bool() {
1812 assert!(parse_bool("true"));
1813 assert!(parse_bool("True"));
1814 assert!(parse_bool("1"));
1815 assert!(parse_bool("yes"));
1816 assert!(parse_bool("on"));
1817 assert!(!parse_bool("false"));
1818 assert!(!parse_bool("0"));
1819 assert!(!parse_bool("no"));
1820 }
1821
1822 #[test]
1823 fn test_validate_unique_ports() {
1824 let mut config = Config::default();
1825 config.ports.http = 8080;
1826 config.ports.grpc = 8080; assert!(config.validate().is_err());
1828 }
1829
1830 #[test]
1831 fn test_validate_trading_mode() {
1832 let mut config = Config::default();
1833 config.trading.mode = "invalid".to_string();
1834 assert!(config.validate().is_err());
1835 }
1836
1837 #[cfg(feature = "redis")]
1838 #[test]
1839 fn test_merge_json_partial_overlay_only_touches_named_fields() {
1840 let mut base = serde_json::json!({
1841 "ports": { "http": 8080, "grpc": 50051 },
1842 "assets": { "enabled": ["BTC", "ETH"], "default_quote": "USD" }
1843 });
1844 let overlay = serde_json::json!({
1845 "assets": { "enabled": ["SOL"] }
1846 });
1847 merge_json(&mut base, overlay);
1848
1849 assert_eq!(base["ports"]["http"], 8080);
1851 assert_eq!(base["assets"]["default_quote"], "USD");
1852 assert_eq!(base["assets"]["enabled"], serde_json::json!(["SOL"]));
1854 }
1855
1856 #[cfg(feature = "redis")]
1857 #[test]
1858 fn test_merge_json_replaces_scalars_and_handles_null() {
1859 let mut base = serde_json::json!({ "service": { "name": "old" } });
1860 let overlay = serde_json::json!({ "service": { "name": "new", "extra": null } });
1861 merge_json(&mut base, overlay);
1862 assert_eq!(base["service"]["name"], "new");
1863 assert!(base["service"]["extra"].is_null());
1864 }
1865
1866 #[test]
1867 fn test_legacy_top_level_field_rejected() {
1868 let toml_str = r#"
1872 http_port = 9000
1873
1874 [ports]
1875 http = 8080
1876 "#;
1877 let result: Result<Config, _> = toml::from_str(toml_str);
1878 assert!(result.is_err(), "legacy top-level http_port should be rejected");
1879 }
1880}