Implementation:
- Added 7 orderbook fields to Trade model (spreadBps, imbalanceRatio, depths, impact, walls)
- Oracle-based estimates with 2bps spread assumption
- ENV flag: ENABLE_ORDERBOOK_LOGGING (defaults true)
- Execute wrapper lines 1037-1053 guards orderbook logic
Database:
- Direct SQL ALTER TABLE (avoided migration drift issues)
- All columns nullable DOUBLE PRECISION
- Prisma schema synced via db pull + generate
Deployment:
- Container rebuilt and deployed successfully
- All 7 columns verified accessible
- System operational, ready for live trade validation
Files changed:
- config/trading.ts (enableOrderbookLogging flag, line 127)
- types/trading.ts (orderbook interfaces)
- lib/database/trades.ts (createTrade saves orderbook data)
- app/api/trading/execute/route.ts (ENV wrapper lines 1037-1053)
- prisma/schema.prisma (7 orderbook fields)
- docs/ORDERBOOK_SHADOW_LOGGING.md (complete documentation)
Status: ✅ PRODUCTION READY - awaiting first trade for validation
837 lines
34 KiB
TypeScript
837 lines
34 KiB
TypeScript
/**
|
||
* 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+)
|
||
|
||
// Orderbook Shadow Logging (Phase 1 - Dec 19, 2025)
|
||
enableOrderbookLogging: boolean // Track orderbook metrics at trade entry
|
||
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: process.env.USE_ADAPTIVE_LEVERAGE === 'true' ? true : process.env.USE_ADAPTIVE_LEVERAGE === 'false' ? false : true, // Default true
|
||
enableOrderbookLogging: process.env.ENABLE_ORDERBOOK_LOGGING === 'true' ? true : process.env.ENABLE_ORDERBOOK_LOGGING === 'false' ? false : true, // Phase 1 shadow logging - default true
|
||
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)
|
||
// V11_V2 TEST (Dec 17, 2025): LONG quality bypass to validate filtering hypothesis
|
||
minSignalQualityScore: 91, // Global fallback (unchanged)
|
||
minSignalQualityScoreLong: 0, // V11_V2 TEST: DISABLED (was 90) - accept ALL LONG signals
|
||
minSignalQualityScoreShort: 90, // V11_V2 TEST: PRESERVED at 90 (working at 62.5% WR)
|
||
// TEST HYPOTHESIS: If quality filtering backward, disabling for LONGs should improve WR from 33.3% to 50%+
|
||
// TEST DATA: Blocked LONGs 100% success (4/4), Executed LONGs 33.3% success (2/6)
|
||
// 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()
|
||
|
||
// 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<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,
|
||
|
||
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>
|
||
): TradingConfig {
|
||
const envConfig = getConfigFromEnv()
|
||
const config = {
|
||
...DEFAULT_TRADING_CONFIG,
|
||
...envConfig,
|
||
...overrides,
|
||
}
|
||
|
||
validateTradingConfig(config)
|
||
return config
|
||
}
|