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), // Research data — Financial Modeling Prep (free tier: 250 req/day) // Register free at https://financialmodelingprep.com/developer/docs FMP_API_KEY: process.env.FMP_API_KEY || '', // 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 || '5000', 10), // Default 5 sec 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), // Tab visibility flags — opt-out model: default enabled, set =false to disable TAB_MARKETPLACE_ENABLED: process.env.TAB_MARKETPLACE_ENABLED !== 'false', TAB_MEMBERSHIP_ENABLED: process.env.TAB_MEMBERSHIP_ENABLED !== 'false', 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_CRYPTO_MARKET_BUFFER_PCT: parseFloat(process.env.STRICT_CAPITAL_CRYPTO_MARKET_BUFFER_PCT || '3'), 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 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_CRYPTO_MARKET_BUFFER_PCT: (value) => toNumber(value, config.STRICT_CAPITAL_CRYPTO_MARKET_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 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.'); } } } };