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

89 lines
2.8 KiB
Dart

/// Alpaca API credentials and endpoints (server-only).
class AlpacaEnv {
AlpacaEnv({
required this.apiKeyId,
required this.apiSecretKey,
required this.tradingBaseUrl,
required this.dataBaseUrl,
required this.dataFeed,
required this.allowLive,
});
static const String defaultPaperTradingUrl =
'https://paper-api.alpaca.markets';
static const String defaultDataUrl = 'https://data.alpaca.markets';
static const String liveTradingHost = 'api.alpaca.markets';
final String apiKeyId;
final String apiSecretKey;
final String tradingBaseUrl;
final String dataBaseUrl;
final String dataFeed;
final bool allowLive;
bool get hasCredentials =>
apiKeyId.isNotEmpty && apiSecretKey.isNotEmpty;
/// HTTP headers that authenticate every Alpaca REST call.
///
/// Shared across `AlpacaMarketDataClient`, `AlpacaTradingClient`, and
/// `AlpacaAssetsClient`; spread it (`...env.authHeaders`) and add
/// content-type/accept where the request body needs them.
Map<String, String> get authHeaders => <String, String>{
'APCA-API-KEY-ID': apiKeyId,
'APCA-API-SECRET-KEY': apiSecretKey,
};
bool get isPaperUrl =>
tradingBaseUrl.contains('paper-api') ||
!tradingBaseUrl.contains(liveTradingHost);
factory AlpacaEnv.fromMap(Map<String, String> env) {
return AlpacaEnv(
apiKeyId: env['ALPACA_API_KEY_ID'] ?? '',
apiSecretKey: env['ALPACA_API_SECRET_KEY'] ?? '',
tradingBaseUrl: _normalizeBaseUrl(
env['ALPACA_TRADING_BASE_URL'] ?? defaultPaperTradingUrl,
),
dataBaseUrl: _normalizeBaseUrl(
env['ALPACA_DATA_BASE_URL'] ?? defaultDataUrl,
),
dataFeed: env['ALPACA_DATA_FEED'] ?? 'iex',
allowLive: (env['ALPACA_ALLOW_LIVE'] ?? 'false').toLowerCase() == 'true',
);
}
/// Strips a trailing `/v2` (or `/v2/`) and trailing slashes from a base URL
/// so the clients can append `/v2/...` without producing `/v2/v2/...`.
static String _normalizeBaseUrl(String raw) {
String url = raw.trim();
while (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
if (url.endsWith('/v2')) {
url = url.substring(0, url.length - 3);
}
return url;
}
/// Refuses live trading host unless [allowLive] is true.
void assertPaperOnly() {
final Uri uri = Uri.parse(tradingBaseUrl);
final bool isLiveHost = uri.host == liveTradingHost;
if (isLiveHost && !allowLive) {
throw StateError(
'Live Alpaca trading URL is not allowed when ALPACA_ALLOW_LIVE=false',
);
}
}
/// Requires non-empty API credentials before outbound Alpaca calls.
void requireCredentials() {
if (!hasCredentials) {
throw StateError(
'ALPACA_API_KEY_ID and ALPACA_API_SECRET_KEY are required',
);
}
}
}