cyberhybridhub/server/test/integration/market_data_ingest_test.dart

181 lines
5.5 KiB
Dart

@Tags(['integration', 'postgres'])
library;
import 'package:cyberhybridhub_server/alpaca/alpaca_env.dart';
import 'package:cyberhybridhub_server/alpaca/alpaca_market_data_client.dart';
import 'package:cyberhybridhub_server/trading/market_data_db.dart';
import 'package:cyberhybridhub_server/trading/market_data_ingest.dart';
import 'package:cyberhybridhub_server/trading/trading_config.dart';
import 'package:postgres/postgres.dart';
import 'package:test/test.dart';
import '../helpers/fixture_loader.dart';
import '../helpers/mock_http_client.dart';
import '../helpers/test_db.dart';
void main() {
TestDb? testDb;
setUpAll(() async {
testDb = await TestDb.open();
});
tearDown(() async {
if (testDb != null) {
await testDb!.truncateTradingTables();
}
});
tearDownAll(() async {
await testDb?.close();
});
group('MarketDataIngest', () {
test('writes last_trade, daily_bar, prev_close snapshots for SPY', () async {
if (testDb == null) {
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
return;
}
const String uid = 'market-ingest-metrics-uid';
await testDb!.seedUser(uid);
await testDb!.tradingConfigDb.upsertUserConfig(
firebaseUid: uid,
templateName: 'default_paper_watchlist',
config: <String, dynamic>{
'data_inputs': <Map<String, dynamic>>[
<String, dynamic>{
'id': 'primary_watchlist',
'symbols': <String>['SPY'],
},
],
},
enabled: true,
);
final EffectiveTradingConfig? config =
await testDb!.tradingConfigDb.resolveEffectiveConfig(uid);
expect(config, isNotNull);
final FixtureLoader fixtures = FixtureLoader();
final MockHttpClient mock = MockHttpClient()
..whenGetJson(
'/trades/latest',
await fixtures.loadJson('alpaca_latest_trade.json'),
)
..whenGetJson(
'/bars',
await fixtures.loadJson('alpaca_daily_bars.json'),
);
final AlpacaEnv env = AlpacaEnv.fromMap(<String, String>{
'ALPACA_API_KEY_ID': 'test-key',
'ALPACA_API_SECRET_KEY': 'test-secret',
});
final MarketDataIngest ingest = MarketDataIngest(
marketDataDb: testDb!.marketDataDb,
tradingStateDb: testDb!.userTradingStateDb,
alpacaClient: AlpacaMarketDataClient(env: env, httpClient: mock),
);
final MarketDataIngestResult result = await ingest.runIfDue(
firebaseUid: uid,
config: config!,
now: DateTime.utc(2026, 5, 23, 12),
);
expect(result.inputsFetched, 1);
expect(result.snapshotsWritten, 3);
final MarketDataSnapshot? lastTrade =
await testDb!.marketDataDb.latestForSymbol('SPY', 'last_trade');
final MarketDataSnapshot? dailyBar =
await testDb!.marketDataDb.latestForSymbol('SPY', 'daily_bar');
final MarketDataSnapshot? prevClose =
await testDb!.marketDataDb.latestForSymbol('SPY', 'prev_close');
expect(lastTrade?.price, 492.15);
expect(dailyBar?.price, 500.0);
expect(prevClose?.price, 498.0);
final Result rows = await testDb!.connection.execute(
Sql.named(
'''
SELECT metric, price::float
FROM market_data_snapshots
WHERE symbol = 'SPY'
ORDER BY metric
''',
),
);
expect(rows, hasLength(3));
});
test('second runIfDue within poll_interval_seconds skips HTTP', () async {
if (testDb == null) {
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
return;
}
const String uid = 'market-ingest-poll-uid';
await testDb!.seedUser(uid);
await testDb!.tradingConfigDb.upsertUserConfig(
firebaseUid: uid,
templateName: 'default_paper_watchlist',
config: <String, dynamic>{
'data_inputs': <Map<String, dynamic>>[
<String, dynamic>{
'id': 'primary_watchlist',
'source': 'alpaca',
'asset_class': 'us_equity',
'symbols': <String>['SPY'],
'feed': 'iex',
'poll_interval_seconds': 60,
'metrics': <String>['last_trade'],
},
],
},
enabled: true,
);
final EffectiveTradingConfig? config =
await testDb!.tradingConfigDb.resolveEffectiveConfig(uid);
expect(config, isNotNull);
final FixtureLoader fixtures = FixtureLoader();
final MockHttpClient mock = MockHttpClient()
..whenGetJson(
'/trades/latest',
await fixtures.loadJson('alpaca_latest_trade.json'),
);
final AlpacaEnv env = AlpacaEnv.fromMap(<String, String>{
'ALPACA_API_KEY_ID': 'test-key',
'ALPACA_API_SECRET_KEY': 'test-secret',
});
final MarketDataIngest ingest = MarketDataIngest(
marketDataDb: testDb!.marketDataDb,
tradingStateDb: testDb!.userTradingStateDb,
alpacaClient: AlpacaMarketDataClient(env: env, httpClient: mock),
);
final DateTime tick = DateTime.utc(2026, 5, 23, 12);
await ingest.runIfDue(
firebaseUid: uid,
config: config!,
now: tick,
);
final int afterFirst = mock.requests.length;
await ingest.runIfDue(
firebaseUid: uid,
config: config,
now: tick.add(const Duration(seconds: 30)),
);
expect(afterFirst, 1);
expect(mock.requests.length, 1);
});
});
}