149 lines
4.3 KiB
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,
|
|
);
|
|
}
|
|
}
|