cyberhybridhub/server/lib/trading/trading_orchestrator.dart

149 lines
4.3 KiB
Dart

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<void> runMaintenanceCycle() async {
final List<String> 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<TradingTickResult> 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,
);
}
}