166 lines
5.9 KiB
Dart
166 lines
5.9 KiB
Dart
import 'dart:io';
|
|
|
|
import 'alpaca/alpaca_env.dart';
|
|
import 'market_history_env.dart';
|
|
import 'package:dotenv/dotenv.dart';
|
|
|
|
class ServerEnv {
|
|
ServerEnv._({
|
|
required this.databaseUrl,
|
|
required this.port,
|
|
required this.firebaseWebApiKey,
|
|
required this.questionWorkerEnabled,
|
|
required this.questionWorkerIntervalSeconds,
|
|
required this.questionPipelineTestMode,
|
|
required this.prospectiveAnswerClosenessEnabled,
|
|
required this.tradingEnabled,
|
|
required this.tradingWorkerIngestEnabled,
|
|
required this.tradingWorkerEvalEnabled,
|
|
required this.tradingDevEndpointsEnabled,
|
|
required this.adminPortalEnabled,
|
|
required this.adminFirebaseUids,
|
|
required this.marketHistory,
|
|
required this.alpaca,
|
|
});
|
|
|
|
final String databaseUrl;
|
|
final int port;
|
|
final String firebaseWebApiKey;
|
|
final bool questionWorkerEnabled;
|
|
final int questionWorkerIntervalSeconds;
|
|
final bool questionPipelineTestMode;
|
|
|
|
/// When true, prospective answers earn closeness bonus points and wrong
|
|
/// direction scores `-2` instead of `-1`. Default false (`+1`/`-1` only).
|
|
final bool prospectiveAnswerClosenessEnabled;
|
|
|
|
final bool tradingEnabled;
|
|
final bool tradingWorkerIngestEnabled;
|
|
final bool tradingWorkerEvalEnabled;
|
|
|
|
/// Mounts dev-only endpoints under `/v1/me/trading/dev/*` (e.g. `force-fire`).
|
|
/// Default false — never enable in production.
|
|
final bool tradingDevEndpointsEnabled;
|
|
final bool adminPortalEnabled;
|
|
final Set<String> adminFirebaseUids;
|
|
|
|
/// Rolling market-history sync, retention, and guess-rule settings.
|
|
final MarketHistoryEnv marketHistory;
|
|
|
|
/// Shorthand for [MarketHistoryEnv.syncEnabled].
|
|
bool get marketHistorySyncEnabled => marketHistory.syncEnabled;
|
|
|
|
final AlpacaEnv alpaca;
|
|
|
|
/// Validates cross-flag constraints after [load].
|
|
void assertConsistent() {
|
|
marketHistory.assertConsistent(tradingEnabled: tradingEnabled);
|
|
}
|
|
|
|
static ServerEnv load() {
|
|
final DotEnv env = DotEnv(includePlatformEnvironment: true)
|
|
..load(['.env']);
|
|
|
|
// Build a sanitized snapshot of the Alpaca-relevant keys for AlpacaEnv.
|
|
const List<String> alpacaKeys = <String>[
|
|
'ALPACA_API_KEY_ID',
|
|
'ALPACA_API_SECRET_KEY',
|
|
'ALPACA_TRADING_BASE_URL',
|
|
'ALPACA_DATA_BASE_URL',
|
|
'ALPACA_DATA_FEED',
|
|
'ALPACA_ALLOW_LIVE',
|
|
];
|
|
final Map<String, String> envMap = <String, String>{
|
|
for (final String key in alpacaKeys)
|
|
if (env[key] != null && env[key]!.isNotEmpty) key: env[key]!,
|
|
};
|
|
|
|
final String? databaseUrl = env['DATABASE_URL'];
|
|
if (databaseUrl == null || databaseUrl.isEmpty) {
|
|
stderr.writeln('DATABASE_URL is required in server/.env');
|
|
exit(1);
|
|
}
|
|
|
|
final String? apiKey = env['FIREBASE_WEB_API_KEY'];
|
|
if (apiKey == null || apiKey.isEmpty) {
|
|
stderr.writeln('FIREBASE_WEB_API_KEY is required in server/.env');
|
|
exit(1);
|
|
}
|
|
|
|
final int port = int.tryParse(env['PORT'] ?? '3000') ?? 3000;
|
|
final bool workerEnabled =
|
|
(env['QUESTION_WORKER_ENABLED'] ?? 'true').toLowerCase() != 'false';
|
|
final int workerIntervalSeconds =
|
|
int.tryParse(env['QUESTION_WORKER_INTERVAL_SECONDS'] ?? '60') ?? 60;
|
|
final bool pipelineTestMode =
|
|
(env['QUESTION_PIPELINE_TEST_MODE'] ?? 'false').toLowerCase() == 'true';
|
|
final bool prospectiveAnswerClosenessEnabled =
|
|
(env['PROSPECTIVE_ANSWER_CLOSENESS_ENABLED'] ?? 'false')
|
|
.toLowerCase() ==
|
|
'true';
|
|
final bool tradingEnabled =
|
|
(env['TRADING_ENABLED'] ?? 'false').toLowerCase() == 'true';
|
|
final bool tradingWorkerIngestEnabled =
|
|
(env['TRADING_WORKER_INGEST_ENABLED'] ?? 'true').toLowerCase() !=
|
|
'false';
|
|
final bool tradingWorkerEvalEnabled =
|
|
(env['TRADING_WORKER_EVAL_ENABLED'] ?? 'true').toLowerCase() != 'false';
|
|
final bool tradingDevEndpointsEnabled =
|
|
(env['TRADING_DEV_ENDPOINTS_ENABLED'] ?? 'false').toLowerCase() ==
|
|
'true';
|
|
final bool adminPortalEnabled =
|
|
(env['ADMIN_PORTAL_ENABLED'] ?? 'false').toLowerCase() == 'true';
|
|
final Set<String> adminFirebaseUids = (env['ADMIN_FIREBASE_UIDS'] ?? '')
|
|
.split(',')
|
|
.map((String value) => value.trim())
|
|
.where((String value) => value.isNotEmpty)
|
|
.toSet();
|
|
final Map<String, String> stringEnv =
|
|
Map<String, String>.from(Platform.environment);
|
|
const List<String> marketHistoryKeys = <String>[
|
|
'MARKET_HISTORY_SYNC_ENABLED',
|
|
'MARKET_HISTORY_WINDOW_DAYS',
|
|
'MARKET_HISTORY_RETENTION_DAYS',
|
|
'MARKET_HISTORY_ARCHIVE_ENABLED',
|
|
'MARKET_UNIVERSE_REFRESH_HOURS',
|
|
'MARKET_HISTORY_SYNC_HOURS',
|
|
'MARKET_HISTORY_CLEANUP_HOURS',
|
|
'MARKET_HISTORY_SYNC_HOUR_UTC',
|
|
'HISTORY_SYNC_BATCH_SIZE',
|
|
'HISTORY_SYNC_MAX_SYMBOLS',
|
|
'MIN_BARS_FOR_GUESS',
|
|
'GUESS_COOLDOWN_HOURS',
|
|
];
|
|
for (final String key in marketHistoryKeys) {
|
|
final String? value = env[key];
|
|
if (value != null && value.isNotEmpty) {
|
|
stringEnv[key] = value;
|
|
}
|
|
}
|
|
final MarketHistoryEnv marketHistory =
|
|
MarketHistoryEnv.fromMap(stringEnv);
|
|
|
|
final AlpacaEnv alpaca = AlpacaEnv.fromMap(envMap)..assertPaperOnly();
|
|
|
|
final ServerEnv loaded = ServerEnv._(
|
|
databaseUrl: databaseUrl,
|
|
port: port,
|
|
firebaseWebApiKey: apiKey,
|
|
questionWorkerEnabled: workerEnabled,
|
|
questionWorkerIntervalSeconds: workerIntervalSeconds,
|
|
questionPipelineTestMode: pipelineTestMode,
|
|
prospectiveAnswerClosenessEnabled: prospectiveAnswerClosenessEnabled,
|
|
tradingEnabled: tradingEnabled,
|
|
tradingWorkerIngestEnabled: tradingWorkerIngestEnabled,
|
|
tradingWorkerEvalEnabled: tradingWorkerEvalEnabled,
|
|
tradingDevEndpointsEnabled: tradingDevEndpointsEnabled,
|
|
adminPortalEnabled: adminPortalEnabled,
|
|
adminFirebaseUids: adminFirebaseUids,
|
|
marketHistory: marketHistory,
|
|
alpaca: alpaca,
|
|
);
|
|
loaded.assertConsistent();
|
|
return loaded;
|
|
}
|
|
}
|