83 lines
2.4 KiB
Dart
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;
|
|
}
|