cyberhybridhub/server/lib/trading/trading_config.dart
2026-05-31 11:17:12 -05:00

222 lines
7.0 KiB
Dart

/// 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<DataInputConfig> dataInputs;
final List<TradingRuleConfig> rules;
final GuardrailsConfig guardrails;
final String? templateName;
factory EffectiveTradingConfig.fromJson(
Map<String, dynamic> 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<String, dynamic>? ?? <String, dynamic>{},
),
templateName: templateName,
);
}
static List<DataInputConfig> _parseDataInputs(Object? raw) {
if (raw is! List) {
return <DataInputConfig>[];
}
return raw
.whereType<Map>()
.map((Map<dynamic, dynamic> m) =>
DataInputConfig.fromJson(Map<String, dynamic>.from(m)))
.toList();
}
static List<TradingRuleConfig> _parseRules(Object? raw) {
if (raw is! List) {
return <TradingRuleConfig>[];
}
return raw
.whereType<Map>()
.map((Map<dynamic, dynamic> m) =>
TradingRuleConfig.fromJson(Map<String, dynamic>.from(m)))
.toList();
}
/// Deep-merge [override] onto [base]. Lists with `id` fields merge by id.
static Map<String, dynamic> mergeJson(
Map<String, dynamic> base,
Map<String, dynamic> override,
) {
final Map<String, dynamic> result = Map<String, dynamic>.from(base);
for (final MapEntry<String, dynamic> 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 : <Object?>[],
overrideValue is List ? overrideValue : <Object?>[],
);
} else if (baseValue is Map<String, dynamic> &&
overrideValue is Map<String, dynamic>) {
result[entry.key] = mergeJson(baseValue, overrideValue);
} else {
result[entry.key] = overrideValue;
}
}
return result;
}
static List<Object?> _mergeListById(List<Object?> base, List<Object?> override) {
final Map<String, Map<String, dynamic>> byId = <String, Map<String, dynamic>>{};
for (final Object? item in base) {
if (item is Map) {
final Map<String, dynamic> map = Map<String, dynamic>.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<String, dynamic> patch = Map<String, dynamic>.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<String> symbols;
final String feed;
final int pollIntervalSeconds;
final List<String> metrics;
factory DataInputConfig.fromJson(Map<String, dynamic> 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<dynamic>? ?? <dynamic>[])
.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<dynamic>? ?? <dynamic>[])
.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<String, dynamic>? onAnswerMatch;
factory TradingRuleConfig.fromJson(Map<String, dynamic> 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<String, dynamic>.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<String> symbolsBlocklist;
factory GuardrailsConfig.fromJson(Map<String, dynamic> 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<dynamic>? ??
<dynamic>[])
.map((dynamic s) => s as String)
.toList(),
);
}
}