/// Parsed trading configuration (template + user override merged). class EffectiveTradingConfig { EffectiveTradingConfig({ required this.version, required this.enabled, required this.mode, required this.dataInputs, required this.rules, required this.guardrails, this.templateName, }); final int version; final bool enabled; final String mode; final List dataInputs; final List rules; final GuardrailsConfig guardrails; final String? templateName; factory EffectiveTradingConfig.fromJson( Map json, { String? templateName, bool? userEnabled, }) { final bool configEnabled = json['enabled'] as bool? ?? false; return EffectiveTradingConfig( version: (json['version'] as num?)?.toInt() ?? 1, enabled: userEnabled ?? configEnabled, mode: json['mode'] as String? ?? 'paper', dataInputs: _parseDataInputs(json['data_inputs']), rules: _parseRules(json['rules']), guardrails: GuardrailsConfig.fromJson( json['guardrails'] as Map? ?? {}, ), templateName: templateName, ); } static List _parseDataInputs(Object? raw) { if (raw is! List) { return []; } return raw .whereType() .map((Map m) => DataInputConfig.fromJson(Map.from(m))) .toList(); } static List _parseRules(Object? raw) { if (raw is! List) { return []; } return raw .whereType() .map((Map m) => TradingRuleConfig.fromJson(Map.from(m))) .toList(); } /// Deep-merge [override] onto [base]. Lists with `id` fields merge by id. static Map mergeJson( Map base, Map override, ) { final Map result = Map.from(base); for (final MapEntry entry in override.entries) { final Object? baseValue = base[entry.key]; final Object? overrideValue = entry.value; if (entry.key == 'data_inputs' || entry.key == 'rules') { result[entry.key] = _mergeListById( baseValue is List ? baseValue : [], overrideValue is List ? overrideValue : [], ); } else if (baseValue is Map && overrideValue is Map) { result[entry.key] = mergeJson(baseValue, overrideValue); } else { result[entry.key] = overrideValue; } } return result; } static List _mergeListById(List base, List override) { final Map> byId = >{}; for (final Object? item in base) { if (item is Map) { final Map map = Map.from(item); final String? id = map['id'] as String?; if (id != null) { byId[id] = map; } } } for (final Object? item in override) { if (item is Map) { final Map patch = Map.from(item); final String? id = patch['id'] as String?; if (id == null) { continue; } byId[id] = byId.containsKey(id) ? mergeJson(byId[id]!, patch) : patch; } } return byId.values.toList(); } } class DataInputConfig { DataInputConfig({ required this.id, required this.source, required this.assetClass, required this.symbols, required this.feed, required this.pollIntervalSeconds, required this.metrics, }); final String id; final String source; final String assetClass; final List symbols; final String feed; final int pollIntervalSeconds; final List metrics; factory DataInputConfig.fromJson(Map json) { return DataInputConfig( id: json['id']! as String, source: json['source'] as String? ?? 'alpaca', assetClass: json['asset_class'] as String? ?? 'us_equity', symbols: (json['symbols'] as List? ?? []) .map((dynamic s) => s as String) .toList(), feed: json['feed'] as String? ?? 'iex', pollIntervalSeconds: (json['poll_interval_seconds'] as num?)?.toInt() ?? 60, metrics: (json['metrics'] as List? ?? []) .map((dynamic m) => m as String) .toList(), ); } } class TradingRuleConfig { TradingRuleConfig({ required this.id, required this.type, required this.symbol, required this.refMetric, required this.thresholdPct, required this.questionTemplate, required this.maxStalenessSeconds, this.onAnswerMatch, }); final String id; final String type; final String symbol; final String refMetric; final num thresholdPct; final String questionTemplate; final int maxStalenessSeconds; final Map? onAnswerMatch; factory TradingRuleConfig.fromJson(Map json) { return TradingRuleConfig( id: json['id']! as String, type: json['type']! as String, symbol: json['symbol'] as String? ?? '*', refMetric: json['ref_metric'] as String? ?? 'prev_close', thresholdPct: json['threshold_pct'] as num? ?? 0, questionTemplate: json['question_template'] as String? ?? '', maxStalenessSeconds: (json['max_staleness_seconds'] as num?)?.toInt() ?? 900, onAnswerMatch: json['on_answer_match'] is Map ? Map.from(json['on_answer_match'] as Map) : null, ); } } class GuardrailsConfig { GuardrailsConfig({ required this.maxOrdersPerDay, required this.maxNotionalUsdPer4h, required this.requireQuestionBeforeOrder, required this.symbolsBlocklist, }); final int maxOrdersPerDay; /// Cap on total order notional placed in any rolling 4-hour window. /// /// Phase 1 enforces this against [trade_orders.submitted_at]; the same value /// is referenced via [Guardrails.windowDuration] when the caller computes the /// running total. final num maxNotionalUsdPer4h; final bool requireQuestionBeforeOrder; final List symbolsBlocklist; factory GuardrailsConfig.fromJson(Map json) { final num? per4h = json['max_notional_usd_per_4h'] as num?; // Back-compat: read legacy key if present, but new configs should use _per_4h. final num? legacyPerDay = json['max_notional_usd_per_day'] as num?; return GuardrailsConfig( maxOrdersPerDay: (json['max_orders_per_day'] as num?)?.toInt() ?? 3, maxNotionalUsdPer4h: per4h ?? legacyPerDay ?? 100, requireQuestionBeforeOrder: json['require_question_before_order'] as bool? ?? true, symbolsBlocklist: (json['symbols_blocklist'] as List? ?? []) .map((dynamic s) => s as String) .toList(), ); } }