cyberhybridhub/server/lib/alpaca/alpaca_assets_client.dart
2026-05-31 11:17:12 -05:00

83 lines
2.4 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'alpaca_env.dart';
import 'alpaca_models.dart';
/// REST client for Alpaca's Trading-API `/v2/assets` endpoint.
///
/// The asset universe lives behind the trading host, NOT the data host —
/// see [AlpacaEnv.tradingBaseUrl]. We treat it as read-only here; the
/// tradable_assets sync (§2.2) is the only writer to the DB.
class AlpacaAssetsClient {
AlpacaAssetsClient({
required AlpacaEnv env,
http.Client? httpClient,
}) : _env = env,
_client = httpClient ?? http.Client();
final AlpacaEnv _env;
final http.Client _client;
/// `GET ${tradingBaseUrl}/v2/assets?status=active&asset_class=us_equity`.
///
/// Filters to `tradable=true` are applied **server-side** by the caller
/// (the asset-universe sync) so the client stays a thin wrapper and the
/// audit trail in [tradable_assets] still records inactive symbols when
/// they later disappear.
Future<List<AlpacaAsset>> listActiveTradable() async {
_env.requireCredentials();
final Uri uri = Uri.parse('${_env.tradingBaseUrl}/v2/assets').replace(
queryParameters: <String, String>{
'status': 'active',
'asset_class': 'us_equity',
},
);
final http.Response response;
try {
response = await _client.get(uri, headers: _env.authHeaders);
} on http.ClientException catch (e) {
throw AlpacaAssetsException(
'GET /v2/assets transport error: ${e.message}',
);
}
if (response.statusCode != 200) {
throw AlpacaAssetsException(
'GET /v2/assets failed: '
'${response.statusCode} ${response.body}',
);
}
final dynamic decoded = jsonDecode(response.body);
if (decoded is! List) {
throw AlpacaAssetsException(
'GET /v2/assets returned non-list body: ${response.body}',
);
}
return decoded
.whereType<Map>()
.map((Map<dynamic, dynamic> m) =>
AlpacaAsset.fromJson(Map<String, dynamic>.from(m)))
.toList(growable: false);
}
void close() => _client.close();
}
/// Thrown by [AlpacaAssetsClient] when an upstream HTTP call fails.
///
/// The message always includes the HTTP status code (when applicable) and
/// the response body so the caller's audit row can capture it verbatim.
class AlpacaAssetsException implements Exception {
AlpacaAssetsException(this.message);
final String message;
@override
String toString() => message;
}