- Add bootstrap.ts as new entry point — resolves Key Vault secrets via DefaultAzureCredential before config/index.ts is evaluated, so all process.env reads pick up KV values (Azure CLI in dev, Managed Identity in prod). Falls back to .env if AZURE_KEYVAULT_URL is not set. - Define INVTTRDG_SECRETS mappings for Cosmos, Azure OpenAI, product-id - Add AZURE_OPENAI_ENDPOINT / KEY / DEPLOYMENT to config - aiClient: prefer AzureOpenAIProvider (AI Foundry) when Azure OpenAI config is present; falls back to direct OpenAI if not configured - Add @azure/identity, @azure/keyvault-secrets, @bytelyst/config deps - Update dev/start scripts to use bootstrap.ts entry point - Document AZURE_KEYVAULT_URL and Azure OpenAI vars in .env.example Key Vault: https://kv-mywisprai.vault.azure.net/ Secrets prefix: invttrdg-* Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
478 lines
33 KiB
TypeScript
478 lines
33 KiB
TypeScript
import * as dotenv from 'dotenv';
|
||
import logger from '../utils/logger.js';
|
||
import { listDynamicConfigEntries } from '../services/dynamicConfigRepository.js';
|
||
|
||
dotenv.config({ override: true });
|
||
|
||
export const config = {
|
||
PRODUCT_ID: process.env.PRODUCT_ID || 'invttrdg',
|
||
PLATFORM_API_URL: process.env.PLATFORM_API_URL || 'http://localhost:4003/api',
|
||
PLATFORM_AUTH_ENABLED: process.env.PLATFORM_AUTH_ENABLED !== 'false',
|
||
PLATFORM_JWT_ISSUER: process.env.PLATFORM_JWT_ISSUER || 'bytelyst-platform',
|
||
PLATFORM_JWT_PUBLIC_KEY: process.env.PLATFORM_JWT_PUBLIC_KEY || '',
|
||
PLATFORM_JWT_JWKS_URL: process.env.PLATFORM_JWT_JWKS_URL || '',
|
||
JWT_SECRET: process.env.JWT_SECRET || '',
|
||
COSMOS_ENDPOINT: process.env.COSMOS_ENDPOINT || '',
|
||
COSMOS_KEY: process.env.COSMOS_KEY || '',
|
||
COSMOS_DATABASE: process.env.COSMOS_DATABASE || 'invttrdg',
|
||
|
||
// Azure OpenAI (AI Foundry) — populated from Key Vault via bootstrap
|
||
AZURE_OPENAI_ENDPOINT: process.env.AZURE_OPENAI_ENDPOINT || '',
|
||
AZURE_OPENAI_KEY: process.env.AZURE_OPENAI_KEY || '',
|
||
AZURE_OPENAI_DEPLOYMENT: process.env.AZURE_OPENAI_DEPLOYMENT || '',
|
||
AZURE_KEYVAULT_URL: process.env.AZURE_KEYVAULT_URL || '',
|
||
|
||
// Plug-and-Play Provider Selection
|
||
PROVIDER: process.env.PROVIDER || 'alpaca',
|
||
DATA_PROVIDER: process.env.DATA_PROVIDER || process.env.PROVIDER || 'alpaca',
|
||
EXECUTION_PROVIDER: process.env.EXECUTION_PROVIDER || process.env.PROVIDER || 'alpaca',
|
||
|
||
// Asset Details
|
||
SYMBOL: process.env.SYMBOL || 'BTC/USD', // Legacy single symbol
|
||
SYMBOLS: (process.env.SYMBOLS || process.env.SYMBOL || 'BTC/USD').split(',').map(s => s.trim()), // Multi-asset support
|
||
TIMEFRAME: process.env.TIMEFRAME || '1Min',
|
||
POLLING_INTERVAL: parseInt(process.env.POLLING_INTERVAL || '60000', 10),
|
||
|
||
// Alpaca Specific
|
||
ALPACA_API_KEY: (process.env.ALPACA_API_KEY === 'your_key' ? '' : process.env.ALPACA_API_KEY) || '',
|
||
ALPACA_API_SECRET: (process.env.ALPACA_API_SECRET === 'your_secret' ? '' : process.env.ALPACA_API_SECRET) || '',
|
||
PAPER_TRADING: process.env.PAPER_TRADING === 'true',
|
||
ASSET_CLASS: (process.env.ASSET_CLASS || 'crypto') as 'crypto' | 'us_equity',
|
||
|
||
// CCXT Specific
|
||
EXCHANGE: process.env.EXCHANGE || 'binance',
|
||
CCXT_API_KEY: (process.env.CCXT_API_KEY === 'your_key' ? '' : process.env.CCXT_API_KEY) || '',
|
||
CCXT_API_SECRET: (process.env.CCXT_API_SECRET === 'your_secret' ? '' : process.env.CCXT_API_SECRET) || '',
|
||
|
||
// Notifications
|
||
WEBHOOK_URL: process.env.WEBHOOK_URL || '',
|
||
NOTIFICATION_PHONE_NUMBERS: (process.env.NOTIFICATION_PHONE_NUMBERS || '').split(',').map(s => s.trim()).filter(Boolean),
|
||
NOTIFICATION_API_HOST: process.env.NOTIFICATION_API_HOST || 'www.zenhustles.com',
|
||
NOTIFICATION_API_PATH: process.env.NOTIFICATION_API_PATH || '/api/whatsapp/send',
|
||
|
||
// Server
|
||
API_PORT: parseInt(process.env.PORT || process.env.API_PORT || '4018', 10),
|
||
ALLOWED_ORIGINS: (process.env.CORS_ALLOWED_ORIGINS
|
||
|| process.env.ALLOWED_ORIGINS
|
||
|| 'http://localhost:3048,http://localhost:5173,http://localhost:8081')
|
||
.split(',')
|
||
.map(s => s.trim())
|
||
.filter(Boolean),
|
||
|
||
// Supabase
|
||
SUPABASE_URL: process.env.SUPABASE_URL || '',
|
||
SUPABASE_KEY: process.env.SUPABASE_KEY || process.env.SUPABASE_ANON_KEY || '',
|
||
SUPABASE_JWT_ISSUER: process.env.SUPABASE_JWT_ISSUER || '',
|
||
SUPABASE_JWT_AUDIENCE: process.env.SUPABASE_JWT_AUDIENCE || '',
|
||
SNAPSHOT_USER_ID: process.env.SNAPSHOT_USER_ID || '',
|
||
|
||
// AI Configuration (Phase 2.3)
|
||
AI: {
|
||
PROVIDER: process.env.AI_PROVIDER || 'openai', // Default/Primary provider
|
||
OPENAI_API_KEY: process.env.OPENAI_API_KEY || process.env.AI_API_KEY || '',
|
||
GEMINI_API_KEY: process.env.GEMINI_API_KEY || process.env.AI_API_KEY || '',
|
||
PERPLEXITY_API_KEY: process.env.PERPLEXITY_API_KEY || '',
|
||
MODEL: process.env.AI_MODEL || 'gpt-4o', // Default model for primary provider
|
||
CONFIDENCE_THRESHOLD: parseInt(process.env.AI_CONFIDENCE_THRESHOLD || '70', 10),
|
||
FALLBACK_LIST: (process.env.AI_FALLBACK_LIST || 'openai,perplexity,gemini').split(',').map(s => s.trim().toLowerCase()),
|
||
CACHE_HOURS: parseInt(process.env.AI_CACHE_HOURS || '4', 10),
|
||
FAIL_OPEN: process.env.AI_FAIL_OPEN !== 'false',
|
||
},
|
||
|
||
// Low-Stress Mode (Toggleable Feature)
|
||
LOW_STRESS_MODE: process.env.LOW_STRESS_MODE === 'true',
|
||
|
||
// Alert Toggles
|
||
ENABLE_TREND_ALERTS: process.env.ENABLE_TREND_ALERTS !== 'false', // Default to true
|
||
ENABLE_PULSE_ALERTS: process.env.ENABLE_PULSE_ALERTS !== 'false', // Default to true
|
||
|
||
// Execution / Trading (Phase 5)
|
||
ENABLE_TRADING: process.env.ENABLE_TRADING === 'true',
|
||
TOTAL_CAPITAL: parseFloat(process.env.TOTAL_CAPITAL || '1000'),
|
||
MAX_OPEN_TRADES: parseInt(process.env.MAX_OPEN_TRADES || '3', 10),
|
||
MAX_OPEN_TRADES_PER_ACCOUNT: parseInt(process.env.MAX_OPEN_TRADES_PER_ACCOUNT || '6', 10),
|
||
COOLDOWN_MS: parseInt(process.env.COOLDOWN_MS || '3600000', 10), // Default 1 hour
|
||
PROFIT_EXIT_PERCENT: parseFloat(process.env.PROFIT_EXIT_PERCENT || '1.0'), // Default 1%
|
||
TRAILING_STOP_PERCENT: parseFloat(process.env.TRAILING_STOP_PERCENT || '0.001'), // Default 0.1%
|
||
PROFILE_SYNC_INTERVAL_MS: parseInt(process.env.PROFILE_SYNC_INTERVAL_MS || '60000', 10), // Default 1 min
|
||
MONITOR_INTERVAL_MS: parseInt(process.env.MONITOR_INTERVAL_MS || '60000', 10), // Default 1 min
|
||
ORDER_SYNC_INTERVAL_MS: parseInt(process.env.ORDER_SYNC_INTERVAL_MS || '60000', 10), // Default 1 min
|
||
STALE_ORDER_THRESHOLD_MINUTES: parseInt(process.env.STALE_ORDER_THRESHOLD_MINUTES || '2', 10), // Default 2 min
|
||
ORDER_SYNC_MISSING_GRACE_MINUTES: parseInt(process.env.ORDER_SYNC_MISSING_GRACE_MINUTES || '5', 10),
|
||
ORDER_SYNC_MISSING_CONFIRMATION_COUNT: parseInt(process.env.ORDER_SYNC_MISSING_CONFIRMATION_COUNT || '2', 10),
|
||
ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES: parseInt(process.env.ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES || '30', 10),
|
||
SYMBOL_DELAY_MS: parseInt(process.env.SYMBOL_DELAY_MS || '2000', 10), // Delay between symbols
|
||
ENABLE_BACKTEST: process.env.ENABLE_BACKTEST === 'true',
|
||
BACKTEST_CUSTOMER_ENABLED: process.env.BACKTEST_CUSTOMER_ENABLED === 'true',
|
||
BACKTEST_MAX_CSV_BYTES: parseInt(process.env.BACKTEST_MAX_CSV_BYTES || '5242880', 10), // 5MB
|
||
BACKTEST_MAX_ROWS: parseInt(process.env.BACKTEST_MAX_ROWS || '200000', 10),
|
||
CAPITAL_WATCHDOG_INTERVAL_MS: parseInt(process.env.CAPITAL_WATCHDOG_INTERVAL_MS || '60000', 10), // Default 1 min
|
||
DB_SNAPSHOT_INTERVAL_MS: parseInt(process.env.DB_SNAPSHOT_INTERVAL_MS || '300000', 10), // Default 5 min
|
||
ENABLE_DB_SNAPSHOTS: process.env.ENABLE_DB_SNAPSHOTS !== 'false', // Default true
|
||
ACCOUNT_SNAPSHOT_INTERVAL_MS: parseInt(process.env.ACCOUNT_SNAPSHOT_INTERVAL_MS || '60000', 10), // Default 1 min
|
||
DYNAMIC_CONFIG_REFRESH_MS: parseInt(process.env.DYNAMIC_CONFIG_REFRESH_MS || '60000', 10), // Default 1 min
|
||
EXCHANGE_STATE_MISMATCH_THROTTLE_MS: parseInt(process.env.EXCHANGE_STATE_MISMATCH_THROTTLE_MS || '300000', 10), // 5 min
|
||
REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE: process.env.REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE !== 'false',
|
||
|
||
// Order Execution Safety
|
||
ORDER_POLL_INTERVAL_MS: parseInt(process.env.ORDER_POLL_INTERVAL_MS || '3000', 10), // Poll every 3s
|
||
ORDER_POLL_MAX_ATTEMPTS: parseInt(process.env.ORDER_POLL_MAX_ATTEMPTS || '10', 10), // Max 10 polls (30s)
|
||
LIMIT_ORDER_TIMEOUT_MS: parseInt(process.env.LIMIT_ORDER_TIMEOUT_MS || '300000', 10), // 5 min timeout
|
||
MAX_SLIPPAGE_PERCENT: parseFloat(process.env.MAX_SLIPPAGE_PERCENT || '1.0'), // 1% max slippage
|
||
ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH: process.env.ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH !== 'false',
|
||
MIN_POSITION_QTY: parseFloat(process.env.MIN_POSITION_QTY || '0.0001'),
|
||
MAX_POSITION_QTY: parseFloat(process.env.MAX_POSITION_QTY || '1000000'),
|
||
QUANTITY_PRECISION: parseInt(process.env.QUANTITY_PRECISION || '6', 10),
|
||
MIN_NOTIONAL_USD: parseFloat(process.env.MIN_NOTIONAL_USD || '10'),
|
||
MAX_NOTIONAL_USD: parseFloat(process.env.MAX_NOTIONAL_USD || '100000'),
|
||
CAPITAL_RESERVE_PERCENT: parseFloat(process.env.CAPITAL_RESERVE_PERCENT || '0'),
|
||
ENTRY_CAPITAL_BUFFER_PCT: parseFloat(process.env.ENTRY_CAPITAL_BUFFER_PCT || '0.25'),
|
||
ENABLE_STRICT_CAPITAL_GUARD: process.env.ENABLE_STRICT_CAPITAL_GUARD !== 'false',
|
||
STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT: parseFloat(process.env.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT || process.env.MAX_SLIPPAGE_PERCENT || '1.0'),
|
||
STRICT_CAPITAL_FEE_BUFFER_PCT: parseFloat(process.env.STRICT_CAPITAL_FEE_BUFFER_PCT || '0.15'),
|
||
STRICT_CAPITAL_MIN_RESERVE_USD: parseFloat(process.env.STRICT_CAPITAL_MIN_RESERVE_USD || '0'),
|
||
ENTRY_AUTO_REDUCE_ALERT_MIN_PCT: parseFloat(process.env.ENTRY_AUTO_REDUCE_ALERT_MIN_PCT || '0.05'),
|
||
ENTRY_AUTO_REDUCE_ALERT_MIN_USD: parseFloat(process.env.ENTRY_AUTO_REDUCE_ALERT_MIN_USD || '25'),
|
||
ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS: parseInt(process.env.ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS || '1800000', 10),
|
||
CAPITAL_INVARIANT_EPSILON_USD: parseFloat(process.env.CAPITAL_INVARIANT_EPSILON_USD || '2'),
|
||
CAPITAL_INVARIANT_EPSILON_PCT: parseFloat(process.env.CAPITAL_INVARIANT_EPSILON_PCT || '0.00005'),
|
||
CAPITAL_INVARIANT_ALERT_THROTTLE_MS: parseInt(process.env.CAPITAL_INVARIANT_ALERT_THROTTLE_MS || '600000', 10),
|
||
CAPITAL_LEDGER_DRIFT_ALERT_PCT: parseFloat(process.env.CAPITAL_LEDGER_DRIFT_ALERT_PCT || '10'),
|
||
CAPITAL_LEDGER_DRIFT_MIN_USD: parseFloat(process.env.CAPITAL_LEDGER_DRIFT_MIN_USD || '10'),
|
||
CAPITAL_LEDGER_DRIFT_SCOPE: String(process.env.CAPITAL_LEDGER_DRIFT_SCOPE || 'auto').trim().toLowerCase(),
|
||
OPERATIONAL_EVENTS_MAX_BUFFER: parseInt(process.env.OPERATIONAL_EVENTS_MAX_BUFFER || '2000', 10),
|
||
|
||
// Alpaca omnibus sub-tagging
|
||
ENABLE_ALPACA_SUBTAG: process.env.ENABLE_ALPACA_SUBTAG !== 'false',
|
||
SUBTAG_OMNIBUS_ONLY: process.env.SUBTAG_OMNIBUS_ONLY === 'true',
|
||
ALPACA_SUBTAG_ENV: process.env.ALPACA_SUBTAG_ENV || '',
|
||
ALPACA_SUBTAG_MAX_LENGTH: parseInt(process.env.ALPACA_SUBTAG_MAX_LENGTH || '48', 10),
|
||
ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE: (process.env.ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE || '')
|
||
.split(',')
|
||
.map(s => s.trim())
|
||
.filter(Boolean),
|
||
ALPACA_OMNIBUS_PROFILE_ALLOWLIST: (process.env.ALPACA_OMNIBUS_PROFILE_ALLOWLIST || '')
|
||
.split(',')
|
||
.map(s => s.trim())
|
||
.filter(Boolean),
|
||
|
||
// Reconciliation EXIT Backfill (Data-only safety path)
|
||
ENABLE_RECON_EXIT_BACKFILL: process.env.ENABLE_RECON_EXIT_BACKFILL !== 'false',
|
||
RECON_EXIT_BACKFILL_DRY_RUN: process.env.RECON_EXIT_BACKFILL_DRY_RUN === 'true',
|
||
RECON_EXIT_BACKFILL_REQUIRE_PAUSE: process.env.RECON_EXIT_BACKFILL_REQUIRE_PAUSE !== 'false',
|
||
RECON_EXIT_BACKFILL_DUST_ABS_QTY: parseFloat(process.env.RECON_EXIT_BACKFILL_DUST_ABS_QTY || '0.001'),
|
||
RECON_EXIT_BACKFILL_DUST_REL_PCT: parseFloat(process.env.RECON_EXIT_BACKFILL_DUST_REL_PCT || '0.002'),
|
||
RECON_EXIT_BACKFILL_LOOKBACK_HOURS: parseInt(process.env.RECON_EXIT_BACKFILL_LOOKBACK_HOURS || '72', 10),
|
||
RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION: process.env.RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION !== 'false',
|
||
RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH: process.env.RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH === 'true',
|
||
RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES: parseInt(process.env.RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES || '5', 10),
|
||
RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST: (process.env.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST || '')
|
||
.split(',')
|
||
.map(s => s.trim())
|
||
.filter(Boolean),
|
||
|
||
// Reconciliation missing-order coverage sync (data-only safety path)
|
||
ENABLE_RECON_ORDER_COVERAGE_SYNC: process.env.ENABLE_RECON_ORDER_COVERAGE_SYNC !== 'false',
|
||
RECON_ORDER_COVERAGE_DRY_RUN: process.env.RECON_ORDER_COVERAGE_DRY_RUN === 'true',
|
||
RECON_ORDER_COVERAGE_REQUIRE_PAUSE: process.env.RECON_ORDER_COVERAGE_REQUIRE_PAUSE === 'true',
|
||
RECON_ORDER_COVERAGE_LOOKBACK_HOURS: parseInt(process.env.RECON_ORDER_COVERAGE_LOOKBACK_HOURS || '72', 10),
|
||
RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE: parseInt(process.env.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE || '500', 10),
|
||
RECON_ORDER_COVERAGE_MAX_FETCH_PAGES: parseInt(process.env.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES || '25', 10),
|
||
RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE: parseInt(process.env.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE || '200', 10),
|
||
RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS: parseInt(process.env.RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS || '2000', 10),
|
||
RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION: process.env.RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION !== 'false',
|
||
RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS: process.env.RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS !== 'false',
|
||
RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT: parseInt(process.env.RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT || '1', 10),
|
||
RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS: parseInt(process.env.RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS || '0', 10),
|
||
|
||
// Reconciliation sub-tag repair (data-only; fills missing legacy sub_tag for traceability)
|
||
ENABLE_RECON_SUBTAG_REPAIR: process.env.ENABLE_RECON_SUBTAG_REPAIR !== 'false',
|
||
RECON_SUBTAG_REPAIR_DRY_RUN: process.env.RECON_SUBTAG_REPAIR_DRY_RUN === 'true',
|
||
RECON_SUBTAG_REPAIR_LOOKBACK_HOURS: parseInt(process.env.RECON_SUBTAG_REPAIR_LOOKBACK_HOURS || '720', 10),
|
||
RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE: parseInt(process.env.RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE || '500', 10),
|
||
|
||
// Canonical lifecycle API sizing and truncation visibility
|
||
CANONICAL_LIFECYCLE_MAX_ROWS: parseInt(process.env.CANONICAL_LIFECYCLE_MAX_ROWS || '200000', 10),
|
||
CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS: parseInt(process.env.CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS || '600000', 10),
|
||
|
||
// Reconciliation SLO alerts
|
||
RECONCILIATION_SLO_ALERT_STREAK: parseInt(process.env.RECONCILIATION_SLO_ALERT_STREAK || '2', 10),
|
||
RECONCILIATION_SLO_ALERT_THROTTLE_MS: parseInt(process.env.RECONCILIATION_SLO_ALERT_THROTTLE_MS || '600000', 10),
|
||
RECONCILIATION_SLO_MISMATCH_THRESHOLD: parseInt(process.env.RECONCILIATION_SLO_MISMATCH_THRESHOLD || '1', 10),
|
||
RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD: parseInt(process.env.RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD || '1', 10),
|
||
RECONCILIATION_SLO_MISSING_DB_THRESHOLD: parseInt(process.env.RECONCILIATION_SLO_MISSING_DB_THRESHOLD || '1', 10),
|
||
|
||
// Reconciliation integrity watchdog (hard alerts for lifecycle drift risks)
|
||
ENABLE_RECON_INTEGRITY_WATCHDOG: process.env.ENABLE_RECON_INTEGRITY_WATCHDOG !== 'false',
|
||
RECON_INTEGRITY_WATCHDOG_THROTTLE_MS: parseInt(process.env.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS || '600000', 10),
|
||
RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD: parseInt(process.env.RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD || '1', 10),
|
||
RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD: parseInt(process.env.RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD || '1', 10),
|
||
ENABLE_RECON_WATCHDOG_AUTO_RESUME: process.env.ENABLE_RECON_WATCHDOG_AUTO_RESUME !== 'false',
|
||
RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS: parseInt(process.env.RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS || '900000', 10),
|
||
RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES: parseInt(process.env.RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES || '2', 10),
|
||
RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS: parseInt(process.env.RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS || '1800000', 10),
|
||
|
||
// Reconciliation position-parity heartbeat (automated ghost self-healing path)
|
||
ENABLE_RECON_POSITION_PARITY_HEARTBEAT: process.env.ENABLE_RECON_POSITION_PARITY_HEARTBEAT !== 'false',
|
||
RECON_POSITION_PARITY_DRY_RUN: process.env.RECON_POSITION_PARITY_DRY_RUN === 'true',
|
||
RECON_POSITION_PARITY_CONFIRMATIONS: parseInt(process.env.RECON_POSITION_PARITY_CONFIRMATIONS || '3', 10),
|
||
RECON_POSITION_PARITY_DUST_ABS_QTY: parseFloat(process.env.RECON_POSITION_PARITY_DUST_ABS_QTY || '0.0001'),
|
||
RECON_POSITION_PARITY_MAX_NOTIONAL_PCT: parseFloat(process.env.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT || '0.5'),
|
||
RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION: process.env.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION !== 'false',
|
||
RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION: process.env.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION !== 'false',
|
||
|
||
// Pro Strategy Configuration (Modular Rules)
|
||
PRO_STRATEGY: {
|
||
ENABLED_RULES: (process.env.ENABLED_RULES || 'trend_bias,momentum,zone,session,entry_trigger,ai_analysis,risk_management').split(',').map(s => s.trim()),
|
||
PARAMETERS: {
|
||
// Trend Bias (4H)
|
||
TREND_TIMEFRAME: process.env.R_TREND_TIMEFRAME || '4h',
|
||
// Execution (15m) -- Phase 2 Upgrade
|
||
EXECUTION_TIMEFRAME: process.env.R_EXECUTION_TIMEFRAME || (process.env.LOW_STRESS_MODE === 'true' ? '1h' : '15m'),
|
||
|
||
TREND_EMA_FAST: parseInt(process.env.R_TREND_EMA_FAST || '50', 10),
|
||
TREND_EMA_SLOW: parseInt(process.env.R_TREND_EMA_SLOW || '200', 10),
|
||
|
||
// Momentum (15m)
|
||
MOMENTUM_TIMEFRAME: process.env.R_MOMENTUM_TIMEFRAME || '15m',
|
||
RSI_PERIOD: parseInt(process.env.R_RSI_PERIOD || '14', 10),
|
||
RSI_OVERBOUGHT: parseInt(process.env.R_RSI_OVERBOUGHT || '70', 10),
|
||
RSI_OVERSOLD: parseInt(process.env.R_RSI_OVERSOLD || '30', 10),
|
||
|
||
// Zone / Location
|
||
ZONE_EMA_PERIOD: parseInt(process.env.R_ZONE_EMA_PERIOD || '20', 10),
|
||
|
||
// Session (UTC Hours) — JSON-configurable
|
||
SESSION_WINDOWS: JSON.parse(process.env.R_SESSION_WINDOWS || JSON.stringify([
|
||
{ start: 0, end: 9 }, // Tokyo (TOK)
|
||
{ start: 22, end: 7 }, // Sydney (SYD)
|
||
{ start: 7, end: 16 }, // London (LDN)
|
||
{ start: 13, end: 22 } // New York (NY)
|
||
])),
|
||
|
||
// Risk
|
||
ATR_PERIOD: parseInt(process.env.R_ATR_PERIOD || '14', 10),
|
||
RISK_PER_TRADE: parseFloat(process.env.R_RISK_PER_TRADE || '0.01'),
|
||
RISK_REWARD_RATIO: parseFloat(process.env.R_RISK_REWARD_RATIO || '1.5'),
|
||
SL_MULTIPLIER: parseFloat(process.env.R_SL_MULTIPLIER || '1.5'),
|
||
MAX_TP_CAP: parseFloat(process.env.R_MAX_TP_CAP || '0.01'),
|
||
}
|
||
},
|
||
};
|
||
|
||
const toNumber = (value: unknown, fallback: number): number => {
|
||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||
if (typeof value === 'string') {
|
||
const parsed = Number(value);
|
||
if (Number.isFinite(parsed)) return parsed;
|
||
}
|
||
return fallback;
|
||
};
|
||
|
||
const toBoolean = (value: unknown, fallback: boolean): boolean => {
|
||
if (typeof value === 'boolean') return value;
|
||
if (typeof value === 'string') {
|
||
const normalized = value.trim().toLowerCase();
|
||
if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
|
||
if (['false', '0', 'no', 'off'].includes(normalized)) return false;
|
||
}
|
||
return fallback;
|
||
};
|
||
|
||
const toCsvArray = (value: unknown, fallback: string[]): string[] => {
|
||
if (Array.isArray(value)) {
|
||
return value.map(v => String(v).trim()).filter(Boolean);
|
||
}
|
||
if (typeof value === 'string') {
|
||
return value.split(',').map(s => s.trim()).filter(Boolean);
|
||
}
|
||
return fallback;
|
||
};
|
||
|
||
const dynamicConfigParsers: Record<string, (value: unknown) => unknown> = {
|
||
SYMBOLS: (value) => toCsvArray(value, config.SYMBOLS),
|
||
POLLING_INTERVAL: (value) => toNumber(value, config.POLLING_INTERVAL),
|
||
ENABLE_TRADING: (value) => toBoolean(value, config.ENABLE_TRADING),
|
||
TOTAL_CAPITAL: (value) => toNumber(value, config.TOTAL_CAPITAL),
|
||
MAX_OPEN_TRADES: (value) => toNumber(value, config.MAX_OPEN_TRADES),
|
||
MAX_OPEN_TRADES_PER_ACCOUNT: (value) => toNumber(value, config.MAX_OPEN_TRADES_PER_ACCOUNT),
|
||
COOLDOWN_MS: (value) => toNumber(value, config.COOLDOWN_MS),
|
||
PROFIT_EXIT_PERCENT: (value) => toNumber(value, config.PROFIT_EXIT_PERCENT),
|
||
TRAILING_STOP_PERCENT: (value) => toNumber(value, config.TRAILING_STOP_PERCENT),
|
||
PROFILE_SYNC_INTERVAL_MS: (value) => toNumber(value, config.PROFILE_SYNC_INTERVAL_MS),
|
||
MONITOR_INTERVAL_MS: (value) => toNumber(value, config.MONITOR_INTERVAL_MS),
|
||
ORDER_SYNC_INTERVAL_MS: (value) => toNumber(value, config.ORDER_SYNC_INTERVAL_MS),
|
||
STALE_ORDER_THRESHOLD_MINUTES: (value) => toNumber(value, config.STALE_ORDER_THRESHOLD_MINUTES),
|
||
ORDER_SYNC_MISSING_GRACE_MINUTES: (value) => toNumber(value, config.ORDER_SYNC_MISSING_GRACE_MINUTES),
|
||
ORDER_SYNC_MISSING_CONFIRMATION_COUNT: (value) => toNumber(value, config.ORDER_SYNC_MISSING_CONFIRMATION_COUNT),
|
||
ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES: (value) => toNumber(value, config.ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES),
|
||
SYMBOL_DELAY_MS: (value) => toNumber(value, config.SYMBOL_DELAY_MS),
|
||
ENABLE_BACKTEST: (value) => toBoolean(value, config.ENABLE_BACKTEST),
|
||
BACKTEST_CUSTOMER_ENABLED: (value) => toBoolean(value, config.BACKTEST_CUSTOMER_ENABLED),
|
||
BACKTEST_MAX_CSV_BYTES: (value) => toNumber(value, config.BACKTEST_MAX_CSV_BYTES),
|
||
BACKTEST_MAX_ROWS: (value) => toNumber(value, config.BACKTEST_MAX_ROWS),
|
||
ORDER_POLL_INTERVAL_MS: (value) => toNumber(value, config.ORDER_POLL_INTERVAL_MS),
|
||
ORDER_POLL_MAX_ATTEMPTS: (value) => toNumber(value, config.ORDER_POLL_MAX_ATTEMPTS),
|
||
LIMIT_ORDER_TIMEOUT_MS: (value) => toNumber(value, config.LIMIT_ORDER_TIMEOUT_MS),
|
||
MAX_SLIPPAGE_PERCENT: (value) => toNumber(value, config.MAX_SLIPPAGE_PERCENT),
|
||
ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH: (value) => toBoolean(value, config.ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH),
|
||
MIN_POSITION_QTY: (value) => toNumber(value, config.MIN_POSITION_QTY),
|
||
MAX_POSITION_QTY: (value) => toNumber(value, config.MAX_POSITION_QTY),
|
||
QUANTITY_PRECISION: (value) => toNumber(value, config.QUANTITY_PRECISION),
|
||
MIN_NOTIONAL_USD: (value) => toNumber(value, config.MIN_NOTIONAL_USD),
|
||
MAX_NOTIONAL_USD: (value) => toNumber(value, config.MAX_NOTIONAL_USD),
|
||
CAPITAL_RESERVE_PERCENT: (value) => toNumber(value, config.CAPITAL_RESERVE_PERCENT),
|
||
ENTRY_CAPITAL_BUFFER_PCT: (value) => toNumber(value, config.ENTRY_CAPITAL_BUFFER_PCT),
|
||
ENABLE_STRICT_CAPITAL_GUARD: (value) => toBoolean(value, config.ENABLE_STRICT_CAPITAL_GUARD),
|
||
STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT: (value) => toNumber(value, config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT),
|
||
STRICT_CAPITAL_FEE_BUFFER_PCT: (value) => toNumber(value, config.STRICT_CAPITAL_FEE_BUFFER_PCT),
|
||
STRICT_CAPITAL_MIN_RESERVE_USD: (value) => toNumber(value, config.STRICT_CAPITAL_MIN_RESERVE_USD),
|
||
ENTRY_AUTO_REDUCE_ALERT_MIN_PCT: (value) => toNumber(value, config.ENTRY_AUTO_REDUCE_ALERT_MIN_PCT),
|
||
ENTRY_AUTO_REDUCE_ALERT_MIN_USD: (value) => toNumber(value, config.ENTRY_AUTO_REDUCE_ALERT_MIN_USD),
|
||
ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS: (value) => toNumber(value, config.ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS),
|
||
CAPITAL_INVARIANT_EPSILON_USD: (value) => toNumber(value, config.CAPITAL_INVARIANT_EPSILON_USD),
|
||
CAPITAL_INVARIANT_EPSILON_PCT: (value) => toNumber(value, config.CAPITAL_INVARIANT_EPSILON_PCT),
|
||
CAPITAL_INVARIANT_ALERT_THROTTLE_MS: (value) => toNumber(value, config.CAPITAL_INVARIANT_ALERT_THROTTLE_MS),
|
||
CAPITAL_LEDGER_DRIFT_ALERT_PCT: (value) => toNumber(value, config.CAPITAL_LEDGER_DRIFT_ALERT_PCT),
|
||
CAPITAL_LEDGER_DRIFT_MIN_USD: (value) => toNumber(value, config.CAPITAL_LEDGER_DRIFT_MIN_USD),
|
||
CAPITAL_LEDGER_DRIFT_SCOPE: (value) => String(value ?? config.CAPITAL_LEDGER_DRIFT_SCOPE).trim().toLowerCase(),
|
||
OPERATIONAL_EVENTS_MAX_BUFFER: (value) => toNumber(value, config.OPERATIONAL_EVENTS_MAX_BUFFER),
|
||
ENABLE_ALPACA_SUBTAG: (value) => toBoolean(value, config.ENABLE_ALPACA_SUBTAG),
|
||
SUBTAG_OMNIBUS_ONLY: (value) => toBoolean(value, config.SUBTAG_OMNIBUS_ONLY),
|
||
ALPACA_SUBTAG_ENV: (value) => String(value ?? config.ALPACA_SUBTAG_ENV),
|
||
ALPACA_SUBTAG_MAX_LENGTH: (value) => toNumber(value, config.ALPACA_SUBTAG_MAX_LENGTH),
|
||
ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE: (value) => toCsvArray(value, config.ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE),
|
||
ALPACA_OMNIBUS_PROFILE_ALLOWLIST: (value) => toCsvArray(value, config.ALPACA_OMNIBUS_PROFILE_ALLOWLIST),
|
||
ALLOWED_ORIGINS: (value) => toCsvArray(value, config.ALLOWED_ORIGINS),
|
||
DB_SNAPSHOT_INTERVAL_MS: (value) => toNumber(value, config.DB_SNAPSHOT_INTERVAL_MS),
|
||
ENABLE_DB_SNAPSHOTS: (value) => toBoolean(value, config.ENABLE_DB_SNAPSHOTS),
|
||
ACCOUNT_SNAPSHOT_INTERVAL_MS: (value) => toNumber(value, config.ACCOUNT_SNAPSHOT_INTERVAL_MS),
|
||
DYNAMIC_CONFIG_REFRESH_MS: (value) => toNumber(value, config.DYNAMIC_CONFIG_REFRESH_MS),
|
||
EXCHANGE_STATE_MISMATCH_THROTTLE_MS: (value) => toNumber(value, config.EXCHANGE_STATE_MISMATCH_THROTTLE_MS),
|
||
REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE: (value) => toBoolean(value, config.REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE),
|
||
ENABLE_RECON_EXIT_BACKFILL: (value) => toBoolean(value, config.ENABLE_RECON_EXIT_BACKFILL),
|
||
RECON_EXIT_BACKFILL_DRY_RUN: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_DRY_RUN),
|
||
RECON_EXIT_BACKFILL_REQUIRE_PAUSE: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_REQUIRE_PAUSE),
|
||
RECON_EXIT_BACKFILL_DUST_ABS_QTY: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_DUST_ABS_QTY),
|
||
RECON_EXIT_BACKFILL_DUST_REL_PCT: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_DUST_REL_PCT),
|
||
RECON_EXIT_BACKFILL_LOOKBACK_HOURS: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_LOOKBACK_HOURS),
|
||
RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION),
|
||
RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH),
|
||
RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES),
|
||
RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST: (value) => toCsvArray(value, config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST),
|
||
ENABLE_RECON_ORDER_COVERAGE_SYNC: (value) => toBoolean(value, config.ENABLE_RECON_ORDER_COVERAGE_SYNC),
|
||
RECON_ORDER_COVERAGE_DRY_RUN: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_DRY_RUN),
|
||
RECON_ORDER_COVERAGE_REQUIRE_PAUSE: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_REQUIRE_PAUSE),
|
||
RECON_ORDER_COVERAGE_LOOKBACK_HOURS: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS),
|
||
RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE),
|
||
RECON_ORDER_COVERAGE_MAX_FETCH_PAGES: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES),
|
||
RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE),
|
||
RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS),
|
||
RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION),
|
||
RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS),
|
||
RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT),
|
||
RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS),
|
||
ENABLE_RECON_SUBTAG_REPAIR: (value) => toBoolean(value, config.ENABLE_RECON_SUBTAG_REPAIR),
|
||
RECON_SUBTAG_REPAIR_DRY_RUN: (value) => toBoolean(value, config.RECON_SUBTAG_REPAIR_DRY_RUN),
|
||
RECON_SUBTAG_REPAIR_LOOKBACK_HOURS: (value) => toNumber(value, config.RECON_SUBTAG_REPAIR_LOOKBACK_HOURS),
|
||
RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE: (value) => toNumber(value, config.RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE),
|
||
CANONICAL_LIFECYCLE_MAX_ROWS: (value) => toNumber(value, config.CANONICAL_LIFECYCLE_MAX_ROWS),
|
||
CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS: (value) => toNumber(value, config.CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS),
|
||
RECONCILIATION_SLO_ALERT_STREAK: (value) => toNumber(value, config.RECONCILIATION_SLO_ALERT_STREAK),
|
||
RECONCILIATION_SLO_ALERT_THROTTLE_MS: (value) => toNumber(value, config.RECONCILIATION_SLO_ALERT_THROTTLE_MS),
|
||
RECONCILIATION_SLO_MISMATCH_THRESHOLD: (value) => toNumber(value, config.RECONCILIATION_SLO_MISMATCH_THRESHOLD),
|
||
RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD: (value) => toNumber(value, config.RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD),
|
||
RECONCILIATION_SLO_MISSING_DB_THRESHOLD: (value) => toNumber(value, config.RECONCILIATION_SLO_MISSING_DB_THRESHOLD),
|
||
ENABLE_RECON_INTEGRITY_WATCHDOG: (value) => toBoolean(value, config.ENABLE_RECON_INTEGRITY_WATCHDOG),
|
||
RECON_INTEGRITY_WATCHDOG_THROTTLE_MS: (value) => toNumber(value, config.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS),
|
||
RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD: (value) => toNumber(value, config.RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD),
|
||
RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD: (value) => toNumber(value, config.RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD),
|
||
ENABLE_RECON_WATCHDOG_AUTO_RESUME: (value) => toBoolean(value, config.ENABLE_RECON_WATCHDOG_AUTO_RESUME),
|
||
RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS: (value) => toNumber(value, config.RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS),
|
||
RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES: (value) => toNumber(value, config.RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES),
|
||
RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS: (value) => toNumber(value, config.RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS),
|
||
ENABLE_RECON_POSITION_PARITY_HEARTBEAT: (value) => toBoolean(value, config.ENABLE_RECON_POSITION_PARITY_HEARTBEAT),
|
||
RECON_POSITION_PARITY_DRY_RUN: (value) => toBoolean(value, config.RECON_POSITION_PARITY_DRY_RUN),
|
||
RECON_POSITION_PARITY_CONFIRMATIONS: (value) => toNumber(value, config.RECON_POSITION_PARITY_CONFIRMATIONS),
|
||
RECON_POSITION_PARITY_DUST_ABS_QTY: (value) => toNumber(value, config.RECON_POSITION_PARITY_DUST_ABS_QTY),
|
||
RECON_POSITION_PARITY_MAX_NOTIONAL_PCT: (value) => toNumber(value, config.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT),
|
||
RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION: (value) => toBoolean(value, config.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION),
|
||
RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION: (value) => toBoolean(value, config.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION),
|
||
};
|
||
|
||
const aiConfigParsers: Record<string, (value: unknown) => unknown> = {
|
||
CONFIDENCE_THRESHOLD: (value) => toNumber(value, config.AI.CONFIDENCE_THRESHOLD),
|
||
CACHE_HOURS: (value) => toNumber(value, config.AI.CACHE_HOURS),
|
||
FALLBACK_LIST: (value) => toCsvArray(value, config.AI.FALLBACK_LIST),
|
||
FAIL_OPEN: (value) => toBoolean(value, config.AI.FAIL_OPEN),
|
||
};
|
||
|
||
export function applyDynamicConfigEntries(data: Array<{ key: string; value: unknown }>) {
|
||
if (!Array.isArray(data) || data.length === 0) {
|
||
return [] as string[];
|
||
}
|
||
|
||
const loadedKeys: string[] = [];
|
||
data.forEach((item: any) => {
|
||
const { key, value } = item;
|
||
if (key in config) {
|
||
const parser = dynamicConfigParsers[key];
|
||
(config as any)[key] = parser ? parser(value) : value;
|
||
loadedKeys.push(key);
|
||
} else if (key in config.AI) {
|
||
const parser = aiConfigParsers[key];
|
||
(config.AI as any)[key] = parser ? parser(value) : value;
|
||
loadedKeys.push(`AI.${key}`);
|
||
}
|
||
});
|
||
|
||
return loadedKeys;
|
||
}
|
||
|
||
/**
|
||
* Loads global configuration from Cosmos-backed dynamic config storage to override .env defaults.
|
||
*/
|
||
export const loadDynamicConfig = async () => {
|
||
try {
|
||
logger.info('--- Loading Dynamic Global Config from control-plane storage ---');
|
||
const data = await listDynamicConfigEntries();
|
||
|
||
if (data && data.length > 0) {
|
||
const loadedKeys = applyDynamicConfigEntries(data);
|
||
logger.info(`✅ Dynamic Config Loaded: ${loadedKeys.join(', ')}`);
|
||
} else {
|
||
logger.info('ℹ️ No dynamic config overrides found in control-plane storage. Using .env defaults.');
|
||
}
|
||
} catch (err: any) {
|
||
logger.error(`[Config] Unexpected error loading dynamic config: ${err.message}`);
|
||
}
|
||
};
|
||
|
||
export const validateConfig = () => {
|
||
// Treat "your_key" as empty
|
||
const isPlaceholder = (val: string) => !val || val === 'your_key' || val === 'your_secret';
|
||
|
||
const hasPerUserStoreConfigured =
|
||
Boolean(config.COSMOS_ENDPOINT && config.COSMOS_KEY) ||
|
||
Boolean(config.SUPABASE_URL && config.SUPABASE_KEY);
|
||
|
||
if (config.PROVIDER === 'alpaca') {
|
||
if (isPlaceholder(config.ALPACA_API_KEY) || isPlaceholder(config.ALPACA_API_SECRET)) {
|
||
if (hasPerUserStoreConfigured) {
|
||
logger.warn(
|
||
'⚠️ Alpaca keys in .env are placeholders/missing. Bot will attempt to use keys from the configured user store (Cosmos or legacy Supabase).'
|
||
);
|
||
} else {
|
||
logger.error('❌ Missing Alpaca API credentials and no user store (Cosmos or Supabase) configured!');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
} else if (config.PROVIDER === 'ccxt') {
|
||
if (!config.EXCHANGE || config.EXCHANGE === 'binance') { // binance is default but check for keys
|
||
if (isPlaceholder(config.CCXT_API_KEY) && !hasPerUserStoreConfigured) {
|
||
logger.warn('⚠️ CCXT keys are placeholders/missing and no user store (Cosmos or Supabase) configured.');
|
||
}
|
||
}
|
||
}
|
||
};
|