learning_ai_invt_trdg/backend/src/config/index.ts

487 lines
34 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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_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<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.');
}
}
}
};