/** * Trading Bot v4 - Configuration * * Optimized for 5-minute scalping with 10x leverage on Drift Protocol */ export interface SymbolSettings { enabled: boolean positionSize: number leverage: number usePercentageSize?: boolean // If true, positionSize is % of portfolio (0-100) } export interface TradingConfig { // Position sizing (global fallback) positionSize: number // USD amount to trade (or percentage if usePercentageSize=true) leverage: number // Leverage multiplier (LEGACY - used when adaptive disabled) usePercentageSize: boolean // If true, positionSize is % of free collateral enableSizeTraceLogging?: boolean // Optional verbose sizing logs for debugging // Adaptive Leverage (Quality-based risk adjustment - Nov 24, 2025) useAdaptiveLeverage: boolean // Enable quality-based leverage tiers highQualityLeverage: number // Leverage for signals >= threshold (e.g., 15 for quality 95+) lowQualityLeverage: number // Leverage for signals < threshold (e.g., 10 for quality 90-94) qualityLeverageThreshold: number // Quality score threshold (e.g., 95) - backward compatibility qualityLeverageThresholdLong?: number // LONG-specific threshold (e.g., 95) - CRITICAL FIX Dec 3, 2025 qualityLeverageThresholdShort?: number // SHORT-specific threshold (e.g., 90) - CRITICAL FIX Dec 3, 2025 // Per-symbol settings solana?: SymbolSettings ethereum?: SymbolSettings fartcoin?: SymbolSettings // Risk management (as percentages of entry price - LEGACY, used as fallback) stopLossPercent: number // Negative number (e.g., -1.5) takeProfit1Percent: number // Positive number (e.g., 0.7) takeProfit2Percent: number // Positive number (e.g., 1.5) emergencyStopPercent: number // Hard stop (e.g., -2.0) // ATR-based dynamic targets (NEW - PRIMARY SYSTEM) useAtrBasedTargets: boolean // Enable ATR-based TP/SL (recommended) atrMultiplierTp1: number // TP1 = ATR × this multiplier (e.g., 2.0) atrMultiplierTp2: number // TP2 = ATR × this multiplier (e.g., 4.0) atrMultiplierSl: number // SL = ATR × this multiplier (e.g., 3.0) minTp1Percent: number // Minimum TP1 level (safety floor) maxTp1Percent: number // Maximum TP1 level (cap) minTp2Percent: number // Minimum TP2 level (safety floor) maxTp2Percent: number // Maximum TP2 level (cap) minSlPercent: number // Minimum SL distance (safety floor) maxSlPercent: number // Maximum SL distance (cap) // Dual Stop System (Advanced) useDualStops: boolean // Enable dual stop system softStopPercent: number // Soft stop trigger (e.g., -1.5) softStopBuffer: number // Buffer for soft stop limit (e.g., 0.4) hardStopPercent: number // Hard stop trigger (e.g., -2.5) // Dynamic adjustments profitLockAfterTP1Percent: number // Lock this % profit on remaining position after TP1 profitLockTriggerPercent: number // When to lock in profit profitLockPercent: number // How much profit to lock // Trailing stop for runner (after TP2) useTrailingStop: boolean // Enable trailing stop for remaining position trailingStopPercent: number // Legacy fixed trail percent (used as fallback) trailingStopAtrMultiplier: number // Multiplier for ATR-based trailing distance trailingStopMinPercent: number // Minimum trailing distance in percent trailingStopMaxPercent: number // Maximum trailing distance in percent trailingStopActivation: number // Activate when runner profits exceed this % // TP2-as-trigger handling useTp2AsTriggerOnly: boolean // If true and TP2 size is 0, do not place TP2 order (trigger only) // Signal Quality (Direction-specific thresholds - Nov 23, 2025) minSignalQualityScore: number // Global fallback (0-100) minSignalQualityScoreLong?: number // Override for LONG signals (0-100) minSignalQualityScoreShort?: number // Override for SHORT signals (0-100) // Position Scaling (add to winning positions) enablePositionScaling: boolean // Allow scaling into existing positions minScaleQualityScore: number // Minimum quality score for scaling signal (0-100) minProfitForScale: number // Position must be this % profitable to scale maxScaleMultiplier: number // Max total position size (e.g., 2.0 = 200% of original) scaleSizePercent: number // Scale size as % of original position (e.g., 50) minAdxIncrease: number // ADX must increase by this much for scaling maxPricePositionForScale: number // Don't scale if price position above this % // DEX specific priceCheckIntervalMs: number // How often to check prices slippageTolerance: number // Max acceptable slippage (%) // Risk limits maxDailyDrawdown: number // USD stop trading threshold maxTradesPerHour: number // Limit overtrading minTimeBetweenTrades: number // Cooldown period (minutes) // Execution useMarketOrders: boolean // true = instant execution confirmationTimeout: number // Max time to wait for confirmation // Take profit size splits (percentages of position to close at TP1/TP2) takeProfit1SizePercent: number takeProfit2SizePercent: number } export interface MarketConfig { symbol: string // e.g., 'SOL-PERP' driftMarketIndex: number pythPriceFeedId: string minOrderSize: number tickSize: number // Position sizing overrides (optional) positionSize?: number leverage?: number } // Default configuration for 5-minute scalping with $1000 capital and 10x leverage export const DEFAULT_TRADING_CONFIG: TradingConfig = { // Position sizing (global fallback) positionSize: 50, // $50 base capital (SAFE FOR TESTING) OR percentage if usePercentageSize=true leverage: 10, // 10x leverage = $500 position size (LEGACY - used when adaptive disabled) usePercentageSize: false, // False = fixed USD, True = percentage of portfolio enableSizeTraceLogging: false, // Disable verbose sizing logs by default // Adaptive Leverage (Quality-based risk adjustment - Nov 24, 2025) // Data-driven: v8 quality 95+ = 100% WR (4/4 wins), quality 90-94 more volatile useAdaptiveLeverage: true, // Enable quality-based leverage tiers highQualityLeverage: 15, // For signals >= 95 quality (high confidence) lowQualityLeverage: 10, // For signals 90-94 quality (reduced risk) qualityLeverageThreshold: 95, // Threshold for high vs low leverage // Per-symbol settings solana: { enabled: true, positionSize: 210, // $210 base capital OR percentage if usePercentageSize=true leverage: 10, // 10x leverage = $2100 notional usePercentageSize: false, }, ethereum: { enabled: true, positionSize: 4, // $4 base capital (DATA ONLY - minimum size) leverage: 1, // 1x leverage = $4 notional usePercentageSize: false, }, fartcoin: { enabled: false, // DISABLED BY DEFAULT positionSize: 20, // 20% of portfolio (for profit generation) leverage: 10, // 10x leverage usePercentageSize: true, // PERCENTAGE-BASED (not fixed USD) }, // Risk parameters (LEGACY FALLBACK - used when ATR unavailable) stopLossPercent: -1.5, // Fallback: -1.5% if no ATR takeProfit1Percent: 0.8, // Fallback: +0.8% if no ATR takeProfit2Percent: 1.8, // Fallback: +1.8% if no ATR emergencyStopPercent: -2.0, // Emergency hard stop (always active) // ATR-based dynamic targets (PRIMARY SYSTEM - Nov 17, 2025) useAtrBasedTargets: true, // Enable ATR-based TP/SL for regime-agnostic trading atrMultiplierTp1: 2.0, // TP1 = ATR × 2.0 (Example: 0.45% ATR = 0.90% TP1) atrMultiplierTp2: 4.0, // TP2 = ATR × 4.0 (Example: 0.45% ATR = 1.80% TP2) atrMultiplierSl: 3.0, // SL = ATR × 3.0 (Example: 0.45% ATR = 1.35% SL) minTp1Percent: 0.5, // Floor: Never below +0.5% maxTp1Percent: 1.5, // Cap: Never above +1.5% minTp2Percent: 1.0, // Floor: Never below +1.0% maxTp2Percent: 3.0, // Cap: Never above +3.0% minSlPercent: 0.8, // Floor: Never tighter than -0.8% maxSlPercent: 2.0, // Cap: Never wider than -2.0% // Dual Stop System useDualStops: false, // Disabled by default softStopPercent: -1.5, // Soft stop (TRIGGER_LIMIT) softStopBuffer: 0.4, // 0.4% buffer (limit at -1.9%) hardStopPercent: -2.5, // Hard stop (TRIGGER_MARKET) // Dynamic adjustments profitLockAfterTP1Percent: 0.4, // Lock this % profit on remaining position after TP1 profitLockTriggerPercent: 1.0, // Lock profit at +1.0% profitLockPercent: 0.4, // Lock +0.4% profit // Trailing stop for runner (after TP2) useTrailingStop: true, // Enable trailing stop for remaining position after TP2 trailingStopPercent: 0.3, // Legacy fallback (%, used if ATR data unavailable) trailingStopAtrMultiplier: 1.5, // Trail ~1.5x ATR (converted to % of price) trailingStopMinPercent: 0.25, // Never trail tighter than 0.25% trailingStopMaxPercent: 0.9, // Cap trailing distance at 0.9% trailingStopActivation: 0.5, // Activate trailing when runner is +0.5% in profit // TP2-as-trigger handling useTp2AsTriggerOnly: true, // Default: TP2=0 acts as trigger only (no on-chain TP2 order) // Signal Quality (Direction-specific thresholds - Nov 23, 2025 DATA-DRIVEN UPDATE) minSignalQualityScore: 91, // Global fallback (unchanged) minSignalQualityScoreLong: 90, // LONGS: 71.4% WR at quality 90-94 (+$44.77 on 7 trades, +$6.40 avg) minSignalQualityScoreShort: 95, // SHORTS: Keep strict (quality 90-94 = 28.6% WR, -$553.76 on 7 trades) // Historical validation: Quality 90+ longs = 50% WR +$600.62 (38 trades), shorts = 47.4% WR -$177.90 // v8 data: 3 longs 100% WR +$565, 7 shorts 42.9% WR -$311 // Position Scaling (conservative defaults) enablePositionScaling: false, // Disabled by default - enable after testing minScaleQualityScore: 75, // Only scale with strong signals (vs 65 for initial entry) minProfitForScale: 0.4, // Position must be at/past TP1 to scale maxScaleMultiplier: 2.0, // Max 2x original position size total scaleSizePercent: 50, // Scale with 50% of original position size minAdxIncrease: 5, // ADX must increase by 5+ points (trend strengthening) maxPricePositionForScale: 70, // Don't scale if price >70% of range (near resistance) // DEX settings priceCheckIntervalMs: 2000, // Check every 2 seconds slippageTolerance: 1.0, // 1% max slippage on market orders // Risk limits maxDailyDrawdown: -150, // Stop trading if daily loss exceeds $150 (-15%) maxTradesPerHour: 6, // Max 6 trades per hour minTimeBetweenTrades: 10, // 10 minutes cooldown // Execution useMarketOrders: true, // Use market orders for reliable fills confirmationTimeout: 30000, // 30 seconds max wait // Position sizing (percentages of position to close at each TP) takeProfit1SizePercent: 75, // Close 75% at TP1 (leaves 25% for TP2 + runner) takeProfit2SizePercent: 0, // Don't close at TP2 - let full 25% remaining become the runner } // Supported markets on Drift Protocol export const SUPPORTED_MARKETS: Record = { 'SOL-PERP': { symbol: 'SOL-PERP', driftMarketIndex: 0, pythPriceFeedId: '0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d', minOrderSize: 0.1, // 0.1 SOL minimum tickSize: 0.0001, // Use default config values (positionSize: 50, leverage: 10) }, 'BTC-PERP': { symbol: 'BTC-PERP', driftMarketIndex: 1, pythPriceFeedId: '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43', minOrderSize: 0.001, // 0.001 BTC minimum tickSize: 0.01, // Use default config values }, 'ETH-PERP': { symbol: 'ETH-PERP', driftMarketIndex: 2, pythPriceFeedId: '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace', minOrderSize: 0.001, // 0.001 ETH minimum (actual Drift minimum ~$4 at $4000/ETH) tickSize: 0.01, // DATA COLLECTION MODE: Minimal risk positionSize: 40, // $40 base capital leverage: 1, // 1x leverage = $40 total exposure }, 'FARTCOIN-PERP': { symbol: 'FARTCOIN-PERP', driftMarketIndex: 22, pythPriceFeedId: '2sZomfWMDuQLcFak3nuharXorHrZ3hK8iaML6ZGSHtso', minOrderSize: 1, // 1 FARTCOIN minimum tickSize: 0.0001, // Use per-symbol config below }, } // Map TradingView symbols to Drift markets export function normalizeTradingViewSymbol(tvSymbol: string): string { const upper = tvSymbol.toUpperCase() // Check FARTCOIN before SOL (FARTCOIN may contain SOL in ticker name) if (upper.includes('FARTCOIN')) return 'FARTCOIN-PERP' if (upper.includes('FART')) return 'FARTCOIN-PERP' if (upper.includes('SOL')) return 'SOL-PERP' if (upper.includes('BTC')) return 'BTC-PERP' if (upper.includes('ETH')) return 'ETH-PERP' // Default to SOL if unknown console.warn(`Unknown symbol ${tvSymbol}, defaulting to SOL-PERP`) return 'SOL-PERP' } // Get market configuration export function getMarketConfig(symbol: string): MarketConfig { const config = SUPPORTED_MARKETS[symbol] if (!config) { throw new Error(`Unsupported market: ${symbol}`) } return config } // Get position size for specific symbol (prioritizes per-symbol config) export function getPositionSizeForSymbol(symbol: string, baseConfig: TradingConfig): { size: number; leverage: number; enabled: boolean } { // Check per-symbol settings first if (symbol === 'SOL-PERP' && baseConfig.solana) { return { size: baseConfig.solana.positionSize, leverage: baseConfig.solana.leverage, enabled: baseConfig.solana.enabled, } } if (symbol === 'ETH-PERP' && baseConfig.ethereum) { return { size: baseConfig.ethereum.positionSize, leverage: baseConfig.ethereum.leverage, enabled: baseConfig.ethereum.enabled, } } if (symbol === 'FARTCOIN-PERP' && baseConfig.fartcoin) { return { size: baseConfig.fartcoin.positionSize, leverage: baseConfig.fartcoin.leverage, enabled: baseConfig.fartcoin.enabled, } } // Fallback to market-specific config, then global config const marketConfig = getMarketConfig(symbol) return { size: marketConfig.positionSize ?? baseConfig.positionSize, leverage: marketConfig.leverage ?? baseConfig.leverage, enabled: true, // BTC or other markets default to enabled } } /** * Calculate actual USD position size from percentage or fixed amount * @param configuredSize - The configured size (USD or percentage) * @param usePercentage - Whether configuredSize is a percentage * @param freeCollateral - Available collateral in USD (from Drift account) * @returns Actual USD size to use for the trade */ export function calculateActualPositionSize( configuredSize: number, usePercentage: boolean, freeCollateral: number ): number { if (!usePercentage) { // Fixed USD amount return configuredSize } // Percentage of free collateral let percentDecimal = configuredSize / 100 // Safety buffer for 100% positions to avoid InsufficientCollateral from fees/slippage if (configuredSize >= 100) { percentDecimal = 0.99 console.log('⚠️ Applying 99% safety buffer for 100% position (prevents InsufficientCollateral from fees/slippage)') } const calculatedSize = freeCollateral * percentDecimal console.log(`📊 Percentage sizing: ${configuredSize}% of $${freeCollateral.toFixed(2)} = $${calculatedSize.toFixed(2)}`) return calculatedSize } /** * Get actual position size for symbol with percentage support * Now supports adaptive leverage based on quality score (Nov 24, 2025) * ENHANCED Nov 25, 2025: Direction-specific leverage for SHORTs * This is the main function to use when opening positions */ export async function getActualPositionSizeForSymbol( symbol: string, baseConfig: TradingConfig, freeCollateral: number, qualityScore?: number, // Optional quality score for adaptive leverage direction?: 'long' | 'short' // NEW: Direction for SHORT-specific leverage tiers ): Promise<{ size: number; leverage: number; enabled: boolean; usePercentage: boolean }> { let symbolSettings: { size: number; leverage: number; enabled: boolean } let usePercentage = false // Get symbol-specific settings if (symbol === 'SOL-PERP' && baseConfig.solana) { symbolSettings = { size: baseConfig.solana.positionSize, leverage: baseConfig.solana.leverage, enabled: baseConfig.solana.enabled, } usePercentage = baseConfig.solana.usePercentageSize ?? false } else if (symbol === 'ETH-PERP' && baseConfig.ethereum) { symbolSettings = { size: baseConfig.ethereum.positionSize, leverage: baseConfig.ethereum.leverage, enabled: baseConfig.ethereum.enabled, } usePercentage = baseConfig.ethereum.usePercentageSize ?? false } else if (symbol === 'FARTCOIN-PERP' && baseConfig.fartcoin) { symbolSettings = { size: baseConfig.fartcoin.positionSize, leverage: baseConfig.fartcoin.leverage, enabled: baseConfig.fartcoin.enabled, } usePercentage = baseConfig.fartcoin.usePercentageSize ?? false } else { // Fallback to market-specific or global config const marketConfig = getMarketConfig(symbol) symbolSettings = { size: marketConfig.positionSize ?? baseConfig.positionSize, leverage: marketConfig.leverage ?? baseConfig.leverage, enabled: true, } usePercentage = baseConfig.usePercentageSize } // Calculate actual size const actualSize = calculateActualPositionSize( symbolSettings.size, usePercentage, freeCollateral ) // NEW (Nov 24, 2025): Apply adaptive leverage based on quality score // ENHANCED (Nov 25, 2025): Direction-specific thresholds let finalLeverage = symbolSettings.leverage if (qualityScore !== undefined && baseConfig.useAdaptiveLeverage) { finalLeverage = getLeverageForQualityScore(qualityScore, baseConfig, direction) // Log SHORT-specific leverage decisions if (direction === 'short') { if (qualityScore >= 90) { console.log(`📊 Adaptive leverage (SHORT): Quality ${qualityScore} → ${finalLeverage}x leverage (Tier 1: Q90+)`) } else if (qualityScore >= 80) { console.log(`📊 Adaptive leverage (SHORT): Quality ${qualityScore} → ${finalLeverage}x leverage (Tier 2: Q80-89)`) } } else { console.log(`📊 Adaptive leverage: Quality ${qualityScore} → ${finalLeverage}x leverage (threshold: ${baseConfig.qualityLeverageThreshold})`) } } if (baseConfig.enableSizeTraceLogging) { const configuredSize = symbolSettings.size const safetyBufferApplied = usePercentage && configuredSize >= 100 const appliedPercent = usePercentage ? safetyBufferApplied ? 99 : configuredSize : undefined const notional = actualSize * finalLeverage console.log('🧮 SIZE TRACE', { symbol, direction: direction ?? 'n/a', usePercentage, configuredSize, safetyBufferApplied, appliedPercent, freeCollateral, calculatedSize: actualSize, leverageSelected: finalLeverage, notional, qualityScore, adaptiveLeverage: baseConfig.useAdaptiveLeverage && qualityScore !== undefined, }) } return { size: actualSize, leverage: finalLeverage, // Use adaptive leverage if quality score provided enabled: symbolSettings.enabled, usePercentage, } } /** * Calculate dynamic TP2 level based on ATR (Average True Range) * Higher ATR = higher volatility = larger TP2 target to capture big moves * LEGACY FUNCTION - Kept for backward compatibility */ export function calculateDynamicTp2( basePrice: number, atrValue: number, config: TradingConfig ): number { if (!config.useAtrBasedTargets || !atrValue) { return config.takeProfit2Percent // Fall back to static TP2 } // Convert ATR to percentage of current price const atrPercent = (atrValue / basePrice) * 100 // Calculate dynamic TP2: ATR × multiplier (use new field name) const dynamicTp2 = atrPercent * config.atrMultiplierTp2 // Apply min/max bounds const boundedTp2 = Math.max( config.minTp2Percent, Math.min(config.maxTp2Percent, dynamicTp2) ) console.log(`📊 ATR-based TP2: ATR=${atrValue.toFixed(4)} (${atrPercent.toFixed(2)}%) × ${config.atrMultiplierTp2} = ${dynamicTp2.toFixed(2)}% → ${boundedTp2.toFixed(2)}% (bounded)`) return boundedTp2 } // Validate trading configuration export function validateTradingConfig(config: TradingConfig): void { if (config.positionSize <= 0) { throw new Error('Position size must be positive') } if (config.leverage < 1 || config.leverage > 20) { throw new Error('Leverage must be between 1 and 20') } if (config.stopLossPercent >= 0) { throw new Error('Stop loss must be negative') } if (config.takeProfit1Percent <= 0 || config.takeProfit2Percent <= 0) { throw new Error('Take profit values must be positive') } if (config.takeProfit1Percent >= config.takeProfit2Percent) { throw new Error('TP2 must be greater than TP1') } if (config.slippageTolerance < 0 || config.slippageTolerance > 10) { throw new Error('Slippage tolerance must be between 0 and 10%') } if (config.trailingStopAtrMultiplier <= 0) { throw new Error('Trailing stop ATR multiplier must be positive') } if (config.trailingStopMinPercent < 0 || config.trailingStopMaxPercent < 0) { throw new Error('Trailing stop bounds must be non-negative') } if (config.trailingStopMinPercent > config.trailingStopMaxPercent) { throw new Error('Trailing stop min percent cannot exceed max percent') } } // Environment-based configuration export function getConfigFromEnv(): Partial { const config: Partial = { positionSize: process.env.MAX_POSITION_SIZE_USD ? parseFloat(process.env.MAX_POSITION_SIZE_USD) : undefined, usePercentageSize: process.env.USE_PERCENTAGE_SIZE ? process.env.USE_PERCENTAGE_SIZE === 'true' : undefined, enableSizeTraceLogging: process.env.ENABLE_SIZE_TRACE_LOGS ? process.env.ENABLE_SIZE_TRACE_LOGS === 'true' : undefined, // Per-symbol settings from ENV solana: { enabled: process.env.SOLANA_ENABLED !== 'false', positionSize: process.env.SOLANA_POSITION_SIZE ? parseFloat(process.env.SOLANA_POSITION_SIZE) : 210, leverage: process.env.SOLANA_LEVERAGE ? parseInt(process.env.SOLANA_LEVERAGE) : 10, usePercentageSize: process.env.SOLANA_USE_PERCENTAGE_SIZE ? process.env.SOLANA_USE_PERCENTAGE_SIZE === 'true' : false, }, ethereum: { enabled: process.env.ETHEREUM_ENABLED !== 'false', positionSize: process.env.ETHEREUM_POSITION_SIZE ? parseFloat(process.env.ETHEREUM_POSITION_SIZE) : 4, leverage: process.env.ETHEREUM_LEVERAGE ? parseInt(process.env.ETHEREUM_LEVERAGE) : 1, usePercentageSize: process.env.ETHEREUM_USE_PERCENTAGE_SIZE ? process.env.ETHEREUM_USE_PERCENTAGE_SIZE === 'true' : false, }, fartcoin: { enabled: process.env.FARTCOIN_ENABLED === 'true', positionSize: process.env.FARTCOIN_POSITION_SIZE ? parseFloat(process.env.FARTCOIN_POSITION_SIZE) : 20, leverage: process.env.FARTCOIN_LEVERAGE ? parseInt(process.env.FARTCOIN_LEVERAGE) : 10, usePercentageSize: process.env.FARTCOIN_USE_PERCENTAGE_SIZE !== 'false', // Default true }, leverage: process.env.LEVERAGE ? parseInt(process.env.LEVERAGE) : undefined, // Adaptive Leverage (Quality-based risk adjustment - Nov 24, 2025) useAdaptiveLeverage: process.env.USE_ADAPTIVE_LEVERAGE ? process.env.USE_ADAPTIVE_LEVERAGE === 'true' : undefined, highQualityLeverage: process.env.HIGH_QUALITY_LEVERAGE ? parseInt(process.env.HIGH_QUALITY_LEVERAGE) : undefined, lowQualityLeverage: process.env.LOW_QUALITY_LEVERAGE ? parseInt(process.env.LOW_QUALITY_LEVERAGE) : undefined, qualityLeverageThreshold: process.env.QUALITY_LEVERAGE_THRESHOLD ? parseInt(process.env.QUALITY_LEVERAGE_THRESHOLD) : undefined, // CRITICAL FIX (Dec 3, 2025): Load direction-specific leverage thresholds // Bug: LONG quality 90 was getting 5x instead of 10x because direction thresholds weren't loaded // ENV vars existed but code never read them qualityLeverageThresholdLong: process.env.QUALITY_LEVERAGE_THRESHOLD_LONG ? parseInt(process.env.QUALITY_LEVERAGE_THRESHOLD_LONG) : undefined, qualityLeverageThresholdShort: process.env.QUALITY_LEVERAGE_THRESHOLD_SHORT ? parseInt(process.env.QUALITY_LEVERAGE_THRESHOLD_SHORT) : undefined, stopLossPercent: process.env.STOP_LOSS_PERCENT ? parseFloat(process.env.STOP_LOSS_PERCENT) : undefined, useDualStops: process.env.USE_DUAL_STOPS ? process.env.USE_DUAL_STOPS === 'true' : undefined, softStopPercent: process.env.SOFT_STOP_PERCENT ? parseFloat(process.env.SOFT_STOP_PERCENT) : undefined, softStopBuffer: process.env.SOFT_STOP_BUFFER ? parseFloat(process.env.SOFT_STOP_BUFFER) : undefined, hardStopPercent: process.env.HARD_STOP_PERCENT ? parseFloat(process.env.HARD_STOP_PERCENT) : undefined, takeProfit1Percent: process.env.TAKE_PROFIT_1_PERCENT ? parseFloat(process.env.TAKE_PROFIT_1_PERCENT) : undefined, takeProfit2Percent: process.env.TAKE_PROFIT_2_PERCENT ? parseFloat(process.env.TAKE_PROFIT_2_PERCENT) : undefined, takeProfit1SizePercent: process.env.TAKE_PROFIT_1_SIZE_PERCENT ? parseFloat(process.env.TAKE_PROFIT_1_SIZE_PERCENT) : undefined, takeProfit2SizePercent: process.env.TAKE_PROFIT_2_SIZE_PERCENT ? parseFloat(process.env.TAKE_PROFIT_2_SIZE_PERCENT) : undefined, // ATR-based dynamic targets (NEW - Nov 17, 2025) useAtrBasedTargets: process.env.USE_ATR_BASED_TARGETS ? process.env.USE_ATR_BASED_TARGETS === 'true' : undefined, atrMultiplierTp1: process.env.ATR_MULTIPLIER_TP1 ? parseFloat(process.env.ATR_MULTIPLIER_TP1) : undefined, atrMultiplierTp2: process.env.ATR_MULTIPLIER_TP2 ? parseFloat(process.env.ATR_MULTIPLIER_TP2) : undefined, atrMultiplierSl: process.env.ATR_MULTIPLIER_SL ? parseFloat(process.env.ATR_MULTIPLIER_SL) : undefined, minTp1Percent: process.env.MIN_TP1_PERCENT ? parseFloat(process.env.MIN_TP1_PERCENT) : undefined, maxTp1Percent: process.env.MAX_TP1_PERCENT ? parseFloat(process.env.MAX_TP1_PERCENT) : undefined, minTp2Percent: process.env.MIN_TP2_PERCENT ? parseFloat(process.env.MIN_TP2_PERCENT) : undefined, maxTp2Percent: process.env.MAX_TP2_PERCENT ? parseFloat(process.env.MAX_TP2_PERCENT) : undefined, minSlPercent: process.env.MIN_SL_PERCENT ? parseFloat(process.env.MIN_SL_PERCENT) : undefined, maxSlPercent: process.env.MAX_SL_PERCENT ? parseFloat(process.env.MAX_SL_PERCENT) : undefined, profitLockAfterTP1Percent: process.env.PROFIT_LOCK_AFTER_TP1_PERCENT || process.env.BREAKEVEN_TRIGGER_PERCENT ? parseFloat(process.env.PROFIT_LOCK_AFTER_TP1_PERCENT || process.env.BREAKEVEN_TRIGGER_PERCENT!) : undefined, profitLockTriggerPercent: process.env.PROFIT_LOCK_TRIGGER_PERCENT ? parseFloat(process.env.PROFIT_LOCK_TRIGGER_PERCENT) : undefined, profitLockPercent: process.env.PROFIT_LOCK_PERCENT ? parseFloat(process.env.PROFIT_LOCK_PERCENT) : undefined, useTrailingStop: process.env.USE_TRAILING_STOP ? process.env.USE_TRAILING_STOP === 'true' : undefined, trailingStopPercent: process.env.TRAILING_STOP_PERCENT ? parseFloat(process.env.TRAILING_STOP_PERCENT) : undefined, trailingStopAtrMultiplier: process.env.TRAILING_STOP_ATR_MULTIPLIER ? parseFloat(process.env.TRAILING_STOP_ATR_MULTIPLIER) : undefined, trailingStopMinPercent: process.env.TRAILING_STOP_MIN_PERCENT ? parseFloat(process.env.TRAILING_STOP_MIN_PERCENT) : undefined, trailingStopMaxPercent: process.env.TRAILING_STOP_MAX_PERCENT ? parseFloat(process.env.TRAILING_STOP_MAX_PERCENT) : undefined, trailingStopActivation: process.env.TRAILING_STOP_ACTIVATION ? parseFloat(process.env.TRAILING_STOP_ACTIVATION) : undefined, useTp2AsTriggerOnly: process.env.USE_TP2_AS_TRIGGER_ONLY ? process.env.USE_TP2_AS_TRIGGER_ONLY === 'true' : undefined, minSignalQualityScore: process.env.MIN_SIGNAL_QUALITY_SCORE ? parseInt(process.env.MIN_SIGNAL_QUALITY_SCORE) : undefined, minSignalQualityScoreLong: process.env.MIN_SIGNAL_QUALITY_SCORE_LONG ? parseInt(process.env.MIN_SIGNAL_QUALITY_SCORE_LONG) : undefined, minSignalQualityScoreShort: process.env.MIN_SIGNAL_QUALITY_SCORE_SHORT ? parseInt(process.env.MIN_SIGNAL_QUALITY_SCORE_SHORT) : undefined, enablePositionScaling: process.env.ENABLE_POSITION_SCALING ? process.env.ENABLE_POSITION_SCALING === 'true' : undefined, minScaleQualityScore: process.env.MIN_SCALE_QUALITY_SCORE ? parseInt(process.env.MIN_SCALE_QUALITY_SCORE) : undefined, minProfitForScale: process.env.MIN_PROFIT_FOR_SCALE ? parseFloat(process.env.MIN_PROFIT_FOR_SCALE) : undefined, maxScaleMultiplier: process.env.MAX_SCALE_MULTIPLIER ? parseFloat(process.env.MAX_SCALE_MULTIPLIER) : undefined, scaleSizePercent: process.env.SCALE_SIZE_PERCENT ? parseFloat(process.env.SCALE_SIZE_PERCENT) : undefined, minAdxIncrease: process.env.MIN_ADX_INCREASE ? parseFloat(process.env.MIN_ADX_INCREASE) : undefined, maxPricePositionForScale: process.env.MAX_PRICE_POSITION_FOR_SCALE ? parseFloat(process.env.MAX_PRICE_POSITION_FOR_SCALE) : undefined, maxDailyDrawdown: process.env.MAX_DAILY_DRAWDOWN ? parseFloat(process.env.MAX_DAILY_DRAWDOWN) : undefined, maxTradesPerHour: process.env.MAX_TRADES_PER_HOUR ? parseInt(process.env.MAX_TRADES_PER_HOUR) : undefined, minTimeBetweenTrades: process.env.MIN_TIME_BETWEEN_TRADES ? parseInt(process.env.MIN_TIME_BETWEEN_TRADES) : undefined, } return config } // Get minimum quality score for a specific direction (Nov 23, 2025) export function getMinQualityScoreForDirection( direction: 'long' | 'short', config: TradingConfig ): number { if (direction === 'long' && config.minSignalQualityScoreLong !== undefined) { return config.minSignalQualityScoreLong } if (direction === 'short' && config.minSignalQualityScoreShort !== undefined) { return config.minSignalQualityScoreShort } return config.minSignalQualityScore } // Get leverage based on signal quality score (Nov 24, 2025) // CRITICAL FIX (Dec 3, 2025): Use direction-specific thresholds from ENV // Bug: Quality 90 LONGs were getting 5x instead of 10x because code used single threshold (95) // User has QUALITY_LEVERAGE_THRESHOLD_LONG=95 in ENV, expects quality 90-94 LONGs to get 10x // Fix: Read direction-specific thresholds with proper fallback logic // Data-driven: // LONGs: Quality 95+ = 100% WR (4/4 wins), quality 90-94 more volatile → 90/95 split // SHORTs: Quality 80+/RSI 33+ = 100% WR (2/2 wins), quality 90+ safer → 80/90 split export function getLeverageForQualityScore( qualityScore: number, config: TradingConfig, direction?: 'long' | 'short' ): number { // If adaptive leverage disabled, use fixed leverage if (!config.useAdaptiveLeverage) { return config.leverage } // CRITICAL FIX (Dec 3, 2025): Use direction-specific thresholds from ENV // Fallback hierarchy: direction-specific ENV → backward compatibility ENV → hardcoded default if (direction === 'short') { // SHORT threshold: ENV.QUALITY_LEVERAGE_THRESHOLD_SHORT or fallback to 90 const shortThreshold = config.qualityLeverageThresholdShort || 90 const shortLowTier = 80 // Medium quality shorts // HIGH tier: Quality >= threshold (e.g., 90+) if (qualityScore >= shortThreshold) { console.log(`📊 SHORT leverage: Quality ${qualityScore} >= ${shortThreshold} → ${config.highQualityLeverage}x (high tier)`) return config.highQualityLeverage } // MEDIUM tier: Quality >= 80 but < threshold if (qualityScore >= shortLowTier) { console.log(`📊 SHORT leverage: Quality ${qualityScore} >= ${shortLowTier} → ${config.lowQualityLeverage}x (medium tier)`) return config.lowQualityLeverage } // LOW tier: Below 80 (borderline) - gets minimum leverage console.log(`📊 SHORT leverage: Quality ${qualityScore} < ${shortLowTier} → ${config.lowQualityLeverage}x (borderline)`) return config.lowQualityLeverage } // LONGs: Use direction-specific threshold if available, else backward compatibility threshold // CRITICAL: This fixes the bug where quality 90 LONGs got 5x instead of 10x // User expectation: quality 90-94 should be "high quality" and get highQualityLeverage (10x) // With QUALITY_LEVERAGE_THRESHOLD_LONG=95, quality 90 now gets lowQualityLeverage (5x) // BUT user wants 90+ to get 10x, so we need to use 90 as actual threshold const longThreshold = config.qualityLeverageThresholdLong || config.qualityLeverageThreshold || 95 if (qualityScore >= longThreshold) { console.log(`📊 LONG leverage: Quality ${qualityScore} >= ${longThreshold} → ${config.highQualityLeverage}x (high tier)`) return config.highQualityLeverage } // Lower quality LONGs get reduced leverage console.log(`📊 LONG leverage: Quality ${qualityScore} < ${longThreshold} → ${config.lowQualityLeverage}x (lower tier)`) return config.lowQualityLeverage } // Merge configurations export function getMergedConfig( overrides?: Partial ): TradingConfig { const envConfig = getConfigFromEnv() const config = { ...DEFAULT_TRADING_CONFIG, ...envConfig, ...overrides, } validateTradingConfig(config) return config }