/// Market-history pipeline settings loaded from env (§7). class MarketHistoryEnv { MarketHistoryEnv({ required this.syncEnabled, required this.windowDays, required this.retentionDays, required this.archiveEnabled, required this.universeRefreshHours, required this.historySyncHours, required this.cleanupHours, required this.syncHourUtc, required this.historySyncBatchSize, required this.historySyncMaxSymbols, required this.minBarsForGuess, required this.guessCooldownHours, required this.apiRequestsPerMinute, required this.staleSyncRunMinutes, }); final bool syncEnabled; final int windowDays; final int retentionDays; final bool archiveEnabled; final int universeRefreshHours; final int historySyncHours; final int cleanupHours; /// UTC hour (0–23) for optional daily alignment, or `null` when unset. final int? syncHourUtc; final int historySyncBatchSize; final int historySyncMaxSymbols; final int minBarsForGuess; final int guessCooldownHours; final int apiRequestsPerMinute; final int staleSyncRunMinutes; static MarketHistoryEnv fromMap(Map env) { final bool syncEnabled = (env['MARKET_HISTORY_SYNC_ENABLED'] ?? 'false').toLowerCase() == 'true'; final int windowDays = _positiveInt(env['MARKET_HISTORY_WINDOW_DAYS'], defaultValue: 5, name: 'MARKET_HISTORY_WINDOW_DAYS'); final int retentionDays = _positiveInt( env['MARKET_HISTORY_RETENTION_DAYS'], defaultValue: 5, name: 'MARKET_HISTORY_RETENTION_DAYS', ); final bool archiveEnabled = (env['MARKET_HISTORY_ARCHIVE_ENABLED'] ?? 'false').toLowerCase() == 'true'; final int universeRefreshHours = _positiveInt( env['MARKET_UNIVERSE_REFRESH_HOURS'], defaultValue: 24, name: 'MARKET_UNIVERSE_REFRESH_HOURS', ); final int historySyncHours = _positiveInt( env['MARKET_HISTORY_SYNC_HOURS'], defaultValue: 24, name: 'MARKET_HISTORY_SYNC_HOURS', ); final int cleanupHours = _positiveInt( env['MARKET_HISTORY_CLEANUP_HOURS'], defaultValue: 24, name: 'MARKET_HISTORY_CLEANUP_HOURS', ); final int? syncHourUtc = _optionalSyncHourUtc(env['MARKET_HISTORY_SYNC_HOUR_UTC']); final int historySyncBatchSize = _positiveInt( env['HISTORY_SYNC_BATCH_SIZE'], defaultValue: 50, name: 'HISTORY_SYNC_BATCH_SIZE', ); final int historySyncMaxSymbols = _positiveInt( env['HISTORY_SYNC_MAX_SYMBOLS'], defaultValue: 2000, name: 'HISTORY_SYNC_MAX_SYMBOLS', ); final int minBarsForGuess = _positiveInt( env['MIN_BARS_FOR_GUESS'], defaultValue: 5, name: 'MIN_BARS_FOR_GUESS', ); final int guessCooldownHours = _positiveInt( env['GUESS_COOLDOWN_HOURS'], defaultValue: 24, name: 'GUESS_COOLDOWN_HOURS', ); final int apiRequestsPerMinute = _positiveInt( env['MARKET_HISTORY_API_REQUESTS_PER_MINUTE'], defaultValue: 200, name: 'MARKET_HISTORY_API_REQUESTS_PER_MINUTE', ); final int staleSyncRunMinutes = _positiveInt( env['MARKET_HISTORY_SYNC_STALE_MINUTES'], defaultValue: 30, name: 'MARKET_HISTORY_SYNC_STALE_MINUTES', ); return MarketHistoryEnv( syncEnabled: syncEnabled, windowDays: windowDays, retentionDays: retentionDays, archiveEnabled: archiveEnabled, universeRefreshHours: universeRefreshHours, historySyncHours: historySyncHours, cleanupHours: cleanupHours, syncHourUtc: syncHourUtc, historySyncBatchSize: historySyncBatchSize, historySyncMaxSymbols: historySyncMaxSymbols, minBarsForGuess: minBarsForGuess, guessCooldownHours: guessCooldownHours, apiRequestsPerMinute: apiRequestsPerMinute, staleSyncRunMinutes: staleSyncRunMinutes, ); } /// Fails fast when market-history flags conflict with trading gates. void assertConsistent({required bool tradingEnabled}) { if (syncEnabled && !tradingEnabled) { throw StateError( 'MARKET_HISTORY_SYNC_ENABLED=true requires TRADING_ENABLED=true', ); } } static int _positiveInt( String? raw, { required int defaultValue, required String name, }) { if (raw == null || raw.trim().isEmpty) { return defaultValue; } final int? parsed = int.tryParse(raw.trim()); if (parsed == null || parsed <= 0) { throw ArgumentError.value(raw, name, 'must be a positive integer'); } return parsed; } static int? _optionalSyncHourUtc(String? raw) { if (raw == null || raw.trim().isEmpty) { return null; } final int? parsed = int.tryParse(raw.trim()); if (parsed == null || parsed < 0 || parsed > 23) { throw ArgumentError.value( raw, 'MARKET_HISTORY_SYNC_HOUR_UTC', 'must be an integer from 0 to 23', ); } return parsed; } }