import 'dart:async'; import 'dart:io'; import '../questions_db.dart'; import 'market_data_ingest.dart'; import 'trade_actuator.dart'; import 'trading_config.dart'; import 'trading_config_db.dart'; import 'trading_pipeline.dart'; /// Per-user outcome of one orchestrator tick. class TradingTickResult { TradingTickResult({ required this.firebaseUid, required this.skippedReason, this.ingest, this.evaluation, this.actuator, }); final String firebaseUid; /// Non-null when the tick was a no-op (config disabled, missing, etc.). final String? skippedReason; final MarketDataIngestResult? ingest; final TradingEvaluationResult? evaluation; final TradeActuatorResult? actuator; bool get skipped => skippedReason != null; } /// Coordinates the per-tick trading work for a user. /// /// One tick: /// 1. Resolve effective config (skip if missing or disabled). /// 2. Ingest market data for each [DataInputConfig] (respecting poll /// intervals via `user_trading_state.context.ingest_last_fetch`). /// 3. Evaluate rules → create `pipeline_key=trading` questions. /// 4. Drain `pending_orders` via [TradeActuator] (test mode or Alpaca). /// /// All three stages are best-effort: a failure in one logs and continues so /// a transient Alpaca outage during ingest doesn't block actuation of /// already-staged orders. class TradingOrchestrator { TradingOrchestrator({ required QuestionsDb questionsDb, required TradingConfigDb tradingConfigDb, required TradingPipeline pipeline, required TradeActuator actuator, MarketDataIngest? ingest, bool ingestEnabled = true, bool evalEnabled = true, DateTime Function()? clock, }) : _questionsDb = questionsDb, _tradingConfigDb = tradingConfigDb, _pipeline = pipeline, _actuator = actuator, _ingest = ingest, _ingestEnabled = ingestEnabled, _evalEnabled = evalEnabled, _clock = clock ?? DateTime.now; final QuestionsDb _questionsDb; final TradingConfigDb _tradingConfigDb; final TradingPipeline _pipeline; final TradeActuator _actuator; final MarketDataIngest? _ingest; final bool _ingestEnabled; final bool _evalEnabled; final DateTime Function() _clock; bool get hasIngest => _ingest != null && _ingestEnabled; /// Runs [tickUser] for every Firebase UID known to [QuestionsDb]. /// /// Per-user errors are logged to stderr but do not abort the cycle. Future runMaintenanceCycle() async { final List uids = await _questionsDb.listAllUserFirebaseUids(); for (final String uid in uids) { try { await tickUser(uid); } catch (e, st) { stderr.writeln('TradingOrchestrator tick failed for $uid: $e\n$st'); } } } Future tickUser(String firebaseUid) async { final EffectiveTradingConfig? config = await _tradingConfigDb.resolveEffectiveConfig(firebaseUid); if (config == null) { return TradingTickResult( firebaseUid: firebaseUid, skippedReason: 'no_config', ); } if (!config.enabled) { return TradingTickResult( firebaseUid: firebaseUid, skippedReason: 'disabled', ); } MarketDataIngestResult? ingestResult; if (hasIngest) { try { ingestResult = await _ingest!.runIfDue( firebaseUid: firebaseUid, config: config, now: _clock(), ); } catch (e, st) { stderr.writeln( 'TradingOrchestrator ingest failed for $firebaseUid: $e\n$st', ); } } TradingEvaluationResult? evaluationResult; if (_evalEnabled) { try { evaluationResult = await _pipeline.evaluate(firebaseUid); } catch (e, st) { stderr.writeln( 'TradingOrchestrator evaluate failed for $firebaseUid: $e\n$st', ); } } TradeActuatorResult? actuatorResult; try { actuatorResult = await _actuator.processPendingOrders(firebaseUid); } catch (e, st) { stderr.writeln( 'TradingOrchestrator actuate failed for $firebaseUid: $e\n$st', ); } return TradingTickResult( firebaseUid: firebaseUid, skippedReason: null, ingest: ingestResult, evaluation: evaluationResult, actuator: actuatorResult, ); } }