import 'dart:convert'; import 'package:postgres/postgres.dart'; /// Normalized market data row persisted for rule evaluation. class MarketDataSnapshot { MarketDataSnapshot({ required this.symbol, required this.metric, required this.asOf, this.id, this.assetClass = 'us_equity', this.feed = 'iex', this.price, this.volume, this.raw, this.createdAt, }); final int? id; final String symbol; final String assetClass; final String feed; final String metric; final num? price; final num? volume; final DateTime asOf; final Map? raw; final DateTime? createdAt; } /// Postgres access for [market_data_snapshots]. class MarketDataDb { MarketDataDb(this._connection); final Connection _connection; Future insertSnapshot({ required String symbol, required String metric, required DateTime asOf, String assetClass = 'us_equity', String feed = 'iex', num? price, num? volume, Map? raw, }) async { final Result result = await _connection.execute( Sql.named( ''' INSERT INTO market_data_snapshots ( symbol, asset_class, feed, metric, price, volume, as_of, raw ) VALUES ( @symbol, @asset_class, @feed, @metric, @price, @volume, @as_of, @raw::jsonb ) RETURNING id, symbol, asset_class, feed, metric, price, volume, as_of, raw, created_at ''', ), parameters: { 'symbol': symbol, 'asset_class': assetClass, 'feed': feed, 'metric': metric, 'price': price, 'volume': volume, 'as_of': asOf.toUtc(), 'raw': raw == null ? null : jsonEncode(raw), }, ); return _rowToSnapshot(result.first); } /// Newest snapshot for [symbol] and [metric] by [as_of]. Future latestForSymbol( String symbol, String metric, ) async { final Result result = await _connection.execute( Sql.named( ''' SELECT id, symbol, asset_class, feed, metric, price, volume, as_of, raw, created_at FROM market_data_snapshots WHERE symbol = @symbol AND metric = @metric ORDER BY as_of DESC LIMIT 1 ''', ), parameters: { 'symbol': symbol, 'metric': metric, }, ); if (result.isEmpty) { return null; } return _rowToSnapshot(result.first); } MarketDataSnapshot _rowToSnapshot(ResultRow row) { final Object? rawValue = row[8]; Map? raw; if (rawValue is Map) { raw = rawValue; } else if (rawValue != null) { raw = jsonDecode(rawValue.toString()) as Map; } return MarketDataSnapshot( id: (row[0]! as num).toInt(), symbol: row[1]! as String, assetClass: row[2]! as String, feed: row[3]! as String, metric: row[4]! as String, price: _readOptionalNumeric(row[5]), volume: _readOptionalNumeric(row[6]), asOf: (row[7]! as DateTime).toUtc(), raw: raw, createdAt: (row[9]! as DateTime).toUtc(), ); } static num? _readOptionalNumeric(Object? value) { if (value == null) { return null; } if (value is num) { return value; } if (value is String) { return num.parse(value); } return num.parse(value.toString()); } }