Skip to main content

jflow_core/
config.rs

1//! Unified configuration for all JANUS modules
2//!
3//! Supports loading from TOML files with environment variable overrides.
4//! Configuration can be loaded from:
5//! 1. TOML file (specified by JANUS_CONFIG_PATH or default locations)
6//! 2. Environment variables (override file settings)
7//!
8//! Environment variable naming convention: JANUS_<SECTION>_<KEY>
9//! Example: JANUS_PORTS_HTTP=8080, JANUS_MODULES_FORWARD=true
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::Path;
14use tracing::{debug, info, warn};
15
16/// Default config file search paths
17const CONFIG_PATHS: &[&str] = &[
18    "config/janus.toml",
19    "/etc/janus/janus.toml",
20    "janus.toml",
21    "infrastructure/config/janus/janus.toml",
22];
23
24// ============================================================================
25// Main Configuration Structure
26// ============================================================================
27
28/// Main configuration for the unified JANUS service
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30#[serde(default, deny_unknown_fields)]
31pub struct Config {
32    /// Service metadata
33    pub service: ServiceConfig,
34
35    /// Network ports configuration
36    pub ports: PortsConfig,
37
38    /// Host/bind configuration
39    pub host: HostConfig,
40
41    /// Module toggles
42    pub modules: ModulesConfig,
43
44    /// Redis configuration
45    pub redis: RedisConfig,
46
47    /// Database configuration
48    pub database: DatabaseConfig,
49
50    /// QuestDB configuration
51    pub questdb: QuestDbConfig,
52
53    /// Forward module settings
54    pub forward: ForwardConfig,
55
56    /// Risk management settings
57    pub risk: RiskConfig,
58
59    /// Backward module settings
60    pub backward: BackwardConfig,
61
62    /// CNS module settings
63    pub cns: CnsConfig,
64
65    /// Market data configuration
66    pub market: MarketConfig,
67
68    /// Assets configuration
69    pub assets: AssetsConfig,
70
71    /// Trading configuration
72    pub trading: TradingConfig,
73
74    /// Logging configuration
75    pub logging: LoggingConfig,
76
77    /// Tracing configuration
78    pub tracing: TracingConfig,
79
80    /// Metrics configuration
81    pub metrics: MetricsConfig,
82
83    /// Alerting configuration
84    pub alerting: AlertingConfig,
85
86    /// Parameter hot-reload configuration
87    pub param_reload: ParamReloadConfig,
88
89    /// Feature engineering configuration
90    pub features: FeaturesConfig,
91
92    /// Security configuration
93    pub security: SecurityConfig,
94
95    /// Advanced settings
96    pub advanced: AdvancedConfig,
97}
98
99// ============================================================================
100// Configuration Sections
101// ============================================================================
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(default)]
105pub struct ServiceConfig {
106    /// Service name
107    pub name: String,
108    /// Service version
109    pub version: String,
110    /// Environment: development | staging | production
111    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    /// HTTP/REST API port
128    pub http: u16,
129    /// gRPC API port
130    pub grpc: u16,
131    /// WebSocket port
132    pub websocket: u16,
133    /// Prometheus metrics port
134    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    /// Bind address for all services
152    pub bind: String,
153    /// Public hostname
154    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    /// Enable forward module
170    pub forward: bool,
171    /// Enable backward module
172    pub backward: bool,
173    /// Enable CNS module
174    pub cns: bool,
175    /// Enable API module
176    pub api: bool,
177    /// Enable Data module
178    pub data: bool,
179    /// Enable WebSocket streaming
180    pub websocket: bool,
181    /// Enable gRPC API
182    pub grpc: bool,
183    /// Enable Prometheus metrics
184    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    /// Redis connection URL
206    pub url: String,
207    /// Maximum connections in pool
208    pub max_connections: u32,
209    /// Minimum connections in pool
210    pub min_connections: u32,
211    /// Connection timeout in seconds
212    pub connect_timeout_secs: u64,
213    /// Pub/Sub channel for parameters
214    pub param_channel: String,
215    /// Pub/Sub channel for signals
216    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    /// PostgreSQL connection URL
236    pub url: String,
237    /// Maximum connections in pool
238    pub max_connections: u32,
239    /// Minimum connections in pool
240    pub min_connections: u32,
241    /// Connection timeout in seconds
242    pub connect_timeout_secs: u64,
243    /// Idle timeout in seconds
244    pub idle_timeout_secs: u64,
245    /// Maximum connection lifetime in seconds
246    pub max_lifetime_secs: u64,
247    /// Enable SQL query logging
248    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    /// QuestDB host
269    pub host: String,
270    /// ILP port for line protocol
271    pub ilp_port: u16,
272    /// HTTP API port
273    pub http_port: u16,
274    /// PostgreSQL wire protocol port
275    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    /// Signal generation interval in seconds
293    pub signal_interval_secs: u64,
294    /// Path to ML models
295    pub ml_model_path: String,
296    /// Enable ML inference
297    pub enable_ml_inference: bool,
298    /// Signal configuration
299    pub signals: SignalConfig,
300    /// Execution configuration
301    pub execution: ExecutionConfig,
302    /// Indicators configuration
303    pub indicators: IndicatorsConfig,
304    /// Strategies configuration
305    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    /// Minimum confidence threshold
326    pub min_confidence: f64,
327    /// Minimum signal strength
328    pub min_strength: f64,
329    /// Maximum signal age in seconds
330    pub max_age_secs: u64,
331    /// Enable quality filtering
332    pub enable_quality_filter: bool,
333    /// Batch size for processing
334    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    /// Enable execution
353    pub enabled: bool,
354    /// Execution service endpoint
355    pub endpoint: String,
356    /// Connection timeout in seconds
357    pub connect_timeout_secs: u64,
358    /// Request timeout in seconds
359    pub request_timeout_secs: u64,
360    /// Enable TLS
361    pub enable_tls: bool,
362    /// Maximum retries
363    pub max_retries: u32,
364    /// Retry backoff in milliseconds
365    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    /// EMA periods
386    pub ema_periods: Vec<u32>,
387    /// RSI period
388    pub rsi_period: u32,
389    /// RSI overbought threshold
390    pub rsi_overbought: f64,
391    /// RSI oversold threshold
392    pub rsi_oversold: f64,
393    /// MACD fast period
394    pub macd_fast_period: u32,
395    /// MACD slow period
396    pub macd_slow_period: u32,
397    /// MACD signal period
398    pub macd_signal_period: u32,
399    /// Bollinger period
400    pub bollinger_period: u32,
401    /// Bollinger standard deviation
402    pub bollinger_std_dev: f64,
403    /// ATR period
404    pub atr_period: u32,
405    /// Volume MA period
406    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    /// Strategy weights
431    pub weights: StrategyWeights,
432    /// Consensus settings
433    pub consensus: ConsensusConfig,
434    /// EMA crossover strategy
435    pub ema_crossover: EmaCrossoverConfig,
436    /// RSI reversal strategy
437    pub rsi_reversal: RsiReversalConfig,
438    /// MACD momentum strategy
439    pub macd_momentum: MacdMomentumConfig,
440    /// Bollinger breakout strategy
441    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    /// Account balance for position sizing
568    pub account_balance: f64,
569    /// Maximum position size percentage
570    pub max_position_size_pct: f64,
571    /// Maximum portfolio risk percentage
572    pub max_portfolio_risk_pct: f64,
573    /// Maximum open positions
574    pub max_open_positions: u32,
575    /// Maximum daily loss
576    pub max_daily_loss: f64,
577    /// Maximum position hold time in hours
578    pub max_position_hold_hours: u32,
579    /// Stop loss configuration
580    pub stop_loss: StopLossConfig,
581    /// Take profit configuration
582    pub take_profit: TakeProfitConfig,
583    /// Position sizing configuration
584    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    /// Method: fixed | risk_based | kelly | volatility
647    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    /// Number of worker threads
668    pub worker_threads: usize,
669    /// Enable scheduled jobs
670    pub enable_scheduler: bool,
671    /// Persistence configuration
672    pub persistence: PersistenceConfig,
673    /// Analytics configuration
674    pub analytics: AnalyticsConfig,
675    /// Data retention configuration
676    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    /// Health check interval in seconds
749    pub health_check_interval_secs: u64,
750    /// Enable automatic recovery reflexes
751    pub enable_reflexes: bool,
752    /// Verbose logging
753    pub verbose_logging: bool,
754    /// Startup grace period in seconds
755    pub startup_grace_period_secs: u64,
756    /// Maximum concurrent probes
757    pub max_concurrent_probes: u32,
758    /// Probe retry attempts
759    pub probe_retry_attempts: u32,
760    /// Endpoints configuration
761    pub endpoints: CnsEndpointsConfig,
762    /// Circuit breakers configuration
763    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    /// Primary exchange
847    pub exchange: String,
848    /// Update interval in milliseconds
849    pub update_interval_ms: u64,
850    /// Enable order book
851    pub enable_orderbook: bool,
852    /// Order book depth
853    pub orderbook_depth: u32,
854    /// Timeframes configuration
855    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    /// List of enabled trading assets (base currency symbols)
874    pub enabled: Vec<String>,
875    /// Default quote currency
876    pub default_quote: String,
877    /// Assets to use for optimization runs
878    pub optimize_assets: Vec<String>,
879    /// High priority assets (receive more frequent updates)
880    pub priority_assets: Vec<String>,
881    /// Per-asset configurations
882    #[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    /// Get list of enabled trading symbols (e.g., "BTC/USD")
900    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    /// Get config for a specific asset
908    pub fn get(&self, asset: &str) -> Option<&AssetConfig> {
909        self.configs.get(asset)
910    }
911
912    /// Check if an asset is enabled
913    pub fn is_enabled(&self, asset: &str) -> bool {
914        self.enabled.contains(&asset.to_string())
915    }
916
917    /// Check if an asset is high priority
918    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    /// Full trading symbol (e.g., "BTC/USD")
927    pub symbol: String,
928    /// Whether this asset is enabled for trading
929    pub enabled: bool,
930    /// Maximum position size as percentage of account
931    pub max_position_size_pct: f64,
932    /// Maximum leverage allowed
933    pub max_leverage: f64,
934    /// Minimum order size in base currency
935    pub min_order_size: f64,
936    /// Maximum order size in base currency
937    pub max_order_size: f64,
938    /// ATR multiplier for stop loss calculation
939    pub atr_multiplier: f64,
940    /// RSI overbought threshold
941    pub rsi_overbought: f64,
942    /// RSI oversold threshold
943    pub rsi_oversold: f64,
944    /// Exchange-specific configurations
945    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    /// Exchange-specific trading pair symbol
969    pub pair: String,
970    /// Minimum order size on this exchange
971    pub min_order: f64,
972    /// Fee tier (e.g., "maker", "taker")
973    pub fee_tier: Option<String>,
974    /// Category for derivatives (e.g., "linear", "inverse")
975    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    /// Trading mode: simulation | paper | live
1016    pub mode: String,
1017    /// Enable real order execution
1018    pub real_orders_enabled: bool,
1019    /// Dry run mode
1020    pub dry_run: bool,
1021    /// Simulation settings
1022    pub simulation: SimulationConfig,
1023    /// Order settings
1024    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    /// Log level: trace | debug | info | warn | error
1087    pub level: String,
1088    /// Log format: json | pretty
1089    pub format: String,
1090    /// Enable console output
1091    pub console: bool,
1092    /// Enable file logging
1093    pub file_enabled: bool,
1094    /// Log file path
1095    pub file_path: String,
1096    /// Enable SQL query logging
1097    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    /// Enable distributed tracing
1117    pub enabled: bool,
1118    /// Jaeger endpoint
1119    pub jaeger_endpoint: String,
1120    /// Sampling rate
1121    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    /// Metrics endpoint path
1138    pub endpoint: String,
1139    /// Include detailed histograms
1140    pub detailed_histograms: bool,
1141    /// Custom labels
1142    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    /// Enable alerting
1162    pub enabled: bool,
1163    /// Discord configuration
1164    pub discord: DiscordConfig,
1165    /// Slack configuration
1166    pub slack: SlackConfig,
1167    /// Email configuration
1168    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    /// Enable parameter hot-reload
1224    pub enabled: bool,
1225    /// Instance ID for namespacing
1226    pub instance_id: String,
1227    /// Reconnection delay in milliseconds
1228    pub reconnect_delay_ms: u64,
1229    /// Maximum reconnection attempts (0 = unlimited)
1230    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    /// Enable GAF image generation
1248    pub enable_gaf: bool,
1249    /// GAF image size
1250    pub gaf_image_size: u32,
1251    /// GAF method: summation | difference
1252    pub gaf_method: String,
1253    /// Lookback windows
1254    pub lookback_windows: Vec<u32>,
1255    /// Enable normalization
1256    pub normalize: bool,
1257    /// Normalization method: minmax | zscore | robust
1258    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    /// CORS allowed origins
1278    pub cors_origins: String,
1279    /// Enable rate limiting
1280    pub enable_rate_limit: bool,
1281    /// Requests per second per IP
1282    pub rate_limit_rps: u32,
1283    /// API key header name
1284    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    /// Tokio worker thread count (0 = auto)
1302    pub tokio_worker_threads: usize,
1303    /// HTTP request timeout in seconds
1304    pub http_timeout_secs: u64,
1305    /// Database query timeout in seconds
1306    pub db_query_timeout_secs: u64,
1307    /// Signal buffer size
1308    pub signal_buffer_size: usize,
1309    /// Order buffer size
1310    pub order_buffer_size: usize,
1311    /// Enable experimental features
1312    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
1328// ============================================================================
1329// Config Implementation
1330// ============================================================================
1331
1332impl Config {
1333    /// Load configuration from TOML file with environment variable overrides
1334    ///
1335    /// Priority order (highest to lowest):
1336    /// 1. Redis overlay at `fks:janus:config` (when `redis` feature enabled and key present)
1337    /// 2. Environment variables
1338    /// 3. TOML config file
1339    /// 4. Default values
1340    pub fn load() -> anyhow::Result<Self> {
1341        // Try to load from TOML file
1342        let mut config = Self::load_from_file()?;
1343
1344        // Apply environment variable overrides
1345        config.apply_env_overrides();
1346
1347        // Apply Redis overlay (JanusAI session-driven config from Ruby).
1348        // Best-effort: failures are warned and ignored so cold boot still
1349        // works when Redis is unavailable.
1350        #[cfg(feature = "redis")]
1351        config.apply_redis_overlay_blocking();
1352
1353        Ok(config)
1354    }
1355
1356    /// Load configuration from environment variables only
1357    pub fn from_env() -> anyhow::Result<Self> {
1358        let mut config = Self::default();
1359        config.apply_env_overrides();
1360        Ok(config)
1361    }
1362
1363    /// Load configuration from a specific TOML file
1364    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    /// Load from default config file locations
1372    fn load_from_file() -> anyhow::Result<Self> {
1373        // Check JANUS_CONFIG_PATH environment variable first
1374        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        // Try default locations
1388        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    /// Apply environment variable overrides
1401    fn apply_env_overrides(&mut self) {
1402        // Service
1403        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        // Ports
1411        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        // Host
1433        if let Ok(v) = std::env::var("JANUS_HOST") {
1434            self.host.bind = v;
1435        }
1436
1437        // Modules
1438        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        // External services
1464        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        // Forward settings
1475        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        // Risk settings
1485        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        // Backward settings
1497        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        // CNS settings
1509        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        // Assets settings
1519        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            // Alias for ENABLED_ASSETS
1527            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        // Market/Exchange settings
1537        if let Ok(v) = std::env::var("PRIMARY_EXCHANGE") {
1538            self.market.exchange = v;
1539        }
1540
1541        // Trading mode
1542        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        // Security
1550        if let Ok(v) = std::env::var("JANUS_CORS_ORIGINS") {
1551            self.security.cors_origins = v;
1552        }
1553
1554        // Logging
1555        if let Ok(v) = std::env::var("RUST_LOG") {
1556            // Extract log level from RUST_LOG
1557            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    /// Apply an overlay sourced from the `fks:janus:config` Redis key.
1573    ///
1574    /// The overlay is a JSON object whose keys mirror the sections produced
1575    /// by `Config::to_toml()`; only fields that are present in the JSON
1576    /// document are touched (everything else keeps its prior value).
1577    ///
1578    /// This is the JFLOW-B handshake — Ruby writes session-specific config
1579    /// (currently: which assets to trade) into Redis when a JanusAI session
1580    /// starts; Janus picks it up on the next cold boot.
1581    ///
1582    /// Best-effort: connection or parse failures are logged and ignored so
1583    /// the process can still boot when Redis is unavailable or the key is
1584    /// malformed.
1585    #[cfg(feature = "redis")]
1586    fn apply_redis_overlay_blocking(&mut self) {
1587        // Skip entirely when explicitly disabled so unit tests can opt out.
1588        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        // Run the async Redis fetch on a tiny single-threaded runtime so
1600        // `Config::load()` stays synchronous (it's called before tokio's
1601        // main runtime exists).
1602        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        // Merge overlay into a JSON view of self, then deserialize back.
1636        // Using JSON as the intermediate keeps the merge logic ignorant of
1637        // the (large) Config schema — anything serde can round-trip works.
1638        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    /// Check if running in production
1659    pub fn is_production(&self) -> bool {
1660        self.service.environment == "production"
1661    }
1662
1663    /// Get CORS origins as a list
1664    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    /// Validate configuration
1673    pub fn validate(&self) -> anyhow::Result<()> {
1674        // Ensure at least one module is enabled
1675        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        // Validate ports are unique
1685        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        // Validate trading mode
1697        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        // Warn about dangerous configurations
1707        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    /// Export configuration to TOML string
1715    pub fn to_toml(&self) -> anyhow::Result<String> {
1716        Ok(toml::to_string_pretty(self)?)
1717    }
1718
1719    /// Save configuration to file
1720    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
1727// ============================================================================
1728// Helper Functions
1729// ============================================================================
1730
1731fn parse_bool(s: &str) -> bool {
1732    matches!(s.to_lowercase().as_str(), "true" | "1" | "yes" | "on")
1733}
1734
1735/// Deep-merge `overlay` into `base`. Objects are merged recursively; any
1736/// other value type in `overlay` (including arrays and null) replaces the
1737/// corresponding entry in `base`. Used by the Redis config overlay so a
1738/// partial JSON document like `{"assets": {"enabled": ["BTC"]}}` only
1739/// touches `assets.enabled`.
1740#[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// ============================================================================
1755// Tests
1756// ============================================================================
1757
1758#[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; // Duplicate!
1827        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        // Untouched fields survive.
1850        assert_eq!(base["ports"]["http"], 8080);
1851        assert_eq!(base["assets"]["default_quote"], "USD");
1852        // Overlay field is replaced (array replacement, not concat).
1853        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        // Old configs sometimes set ports at the top level (e.g. `http_port = 8080`)
1869        // instead of inside `[ports]`. `deny_unknown_fields` should make this fail
1870        // loudly rather than silently dropping the value.
1871        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}