181 lines
5.5 KiB
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);
|
|
});
|
|
});
|
|
}
|