Files
trading_bot_v4/config/trading.ts
copilot-swe-agent[bot] 2df6c69b92 feat: Add FARTCOIN-PERP market support with percentage-based sizing
- Added FARTCOIN-PERP to SUPPORTED_MARKETS (market index 22)
- Updated TradingConfig interface with fartcoin symbol settings
- Added default config: 20% portfolio, 10x leverage, disabled by default
- Updated normalizeTradingViewSymbol to detect FARTCOIN
- Enhanced getPositionSizeForSymbol for FARTCOIN-PERP handling
- Enhanced getActualPositionSizeForSymbol for percentage-based sizing
- Added FARTCOIN ENV variable loading in getConfigFromEnv
- Updated Settings UI with FARTCOIN section and percentage badge
- Added FARTCOIN fields to settings API endpoints (GET/POST)
- Created comprehensive documentation in docs/markets/FARTCOIN-PERP.md
- Build successful: TypeScript compilation and static generation complete

Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
2025-12-06 17:44:19 +00:00

790 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
/**
* 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
// 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 %
// 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
// 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
// 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<string, MarketConfig> = {
'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()
if (upper.includes('SOL')) return 'SOL-PERP'
if (upper.includes('BTC')) return 'BTC-PERP'
if (upper.includes('ETH')) return 'ETH-PERP'
if (upper.includes('FARTCOIN')) return 'FARTCOIN-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
// CRITICAL: Safety buffer for 100% positions
// Drift's margin calculation includes fees and buffer, so 100% exact causes InsufficientCollateral
// Use 99% when user configures 100% to leave room for 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})`)
}
}
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<TradingConfig> {
const config: Partial<TradingConfig> = {
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,
// 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,
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>
): TradingConfig {
const envConfig = getConfigFromEnv()
const config = {
...DEFAULT_TRADING_CONFIG,
...envConfig,
...overrides,
}
validateTradingConfig(config)
return config
}