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

139 lines
4.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:math';
import 'package:postgres/postgres.dart';
import 'market_data_db.dart' show MarketDataDb;
import 'market_history_bar_placeholder.dart';
import 'market_history_config.dart';
import 'tradable_assets_db.dart';
/// One symbol's rolling-window 4-hour bar summary for the guessing game.
class WeeklyMover {
WeeklyMover({
required this.symbol,
required this.openClose,
required this.currentClose,
required this.days,
});
final String symbol;
final num openClose;
final num currentClose;
/// Number of 4-hour bars in the window (≥ [MarketHistoryConfig.minBarsForGuess]).
final int days;
}
/// Read-side queries over 4-hour history bars for question rules.
class MarketHistoryQuery {
MarketHistoryQuery({
required Connection connection,
TradableAssetsDb? tradableAssetsDb,
}) : _connection = connection,
_tradableAssetsDb = tradableAssetsDb ?? TradableAssetsDb(connection);
final Connection _connection;
final TradableAssetsDb _tradableAssetsDb;
/// Symbols with ≥ [minBars] 4-hour closes in [`asOf` window, `asOf`), whose
/// newest bar is not older than [maxStalenessDays] before [asOf].
///
/// When [random] is set, eligible rows are sorted by symbol then shuffled for
/// a stable pick order across runs with the same seed.
Future<List<WeeklyMover>> weeklyMovers({
required DateTime asOf,
int minBars = MarketHistoryConfig.minBarsForGuess,
int windowDays = MarketHistoryConfig.windowDays,
int maxStalenessDays = 2,
Random? random,
}) async {
final DateTime until = asOf.toUtc();
final DateTime since =
until.subtract(Duration(days: windowDays));
final DateTime freshSince =
until.subtract(Duration(days: maxStalenessDays));
final List<String> active =
await _tradableAssetsDb.listActiveTradableSymbols();
if (active.isEmpty) {
return <WeeklyMover>[];
}
final Result rows = await _connection.execute(
Sql.named(
'''
WITH bars AS (
SELECT symbol, as_of, price
FROM market_data_snapshots
WHERE metric = 'bar'
AND timeframe = @timeframe
AND as_of >= @since
AND as_of < @until
AND symbol = ANY(@symbols)
AND ${MarketHistoryBarPlaceholder.sqlExcludePlaceholders}
AND price IS NOT NULL
),
agg AS (
SELECT
symbol,
COUNT(*)::int AS bar_count,
MIN(as_of) AS oldest_as_of,
MAX(as_of) AS newest_as_of
FROM bars
GROUP BY symbol
HAVING COUNT(*) >= @min_bars
AND MAX(as_of) >= @fresh_since
)
SELECT
a.symbol,
a.bar_count,
(
SELECT b.price
FROM bars b
WHERE b.symbol = a.symbol AND b.as_of = a.oldest_as_of
LIMIT 1
) AS open_close,
(
SELECT b.price
FROM bars b
WHERE b.symbol = a.symbol AND b.as_of = a.newest_as_of
LIMIT 1
) AS current_close
FROM agg a
ORDER BY a.symbol ASC
''',
),
parameters: <String, dynamic>{
'since': since,
'until': until,
'symbols': active,
'timeframe': MarketHistoryConfig.barTimeframe,
'min_bars': minBars,
'fresh_since': freshSince,
},
);
final List<WeeklyMover> movers = <WeeklyMover>[];
for (final ResultRow row in rows) {
final num? openClose = MarketDataDb.readOptionalNumeric(row[2]);
final num? currentClose = MarketDataDb.readOptionalNumeric(row[3]);
if (openClose == null || currentClose == null) {
continue;
}
movers.add(
WeeklyMover(
symbol: row[0]! as String,
openClose: openClose,
currentClose: currentClose,
days: (row[1]! as num).toInt(),
),
);
}
if (random != null && movers.length > 1) {
movers.shuffle(random);
}
return movers;
}
}