Files
trading_bot_v4/config/trading.ts
mindesbunister d2fbd125a0 fix: Make minSignalQualityScore configurable via settings + anti-chop improvements
CRITICAL BUG FIX:
- Settings page saved MIN_SIGNAL_QUALITY_SCORE to .env but check-risk had hardcoded value
- Now reads from config.minSignalQualityScore (defaults to 65, editable via /settings)
- Prevents settings changes from being ignored after restart

ANTI-CHOP FILTER FIXES:
- Fixed volume breakout bonus conflicting with anti-chop filter
- Volume breakout now requires ADX > 18 (trending market)
- Prevents high volume + low ADX from getting rewarded instead of penalized
- Anti-chop filter now properly blocks whipsaw traps at score 60

TESTING INFRASTRUCTURE:
- Added backtest script showing +17.1% P&L improvement (saved $242 in losses)
- Added test-signals.sh for comprehensive signal quality validation
- Added test-recent-signals.sh for analyzing actual trading session signals
- All tests passing: timeframe awareness, anti-chop, score thresholds

CHANGES:
- config/trading.ts: Added minSignalQualityScore to interface and defaults
- app/api/trading/check-risk/route.ts: Use config value instead of hardcoded 65
- lib/trading/signal-quality.ts: Fixed volume breakout bonus logic
- .env: Added MIN_SIGNAL_QUALITY_SCORE=65
- scripts/: Added comprehensive testing tools

BACKTEST RESULTS (Last 30 trades):
- Old system (score ≥60): $1,412.79 P&L
- New system (score ≥65 + anti-chop): $1,654.79 P&L
- Improvement: +$242.00 (+17.1%)
- Blocked 5 losing trades, missed 0 winners
2025-11-10 11:22:52 +01:00

470 lines
17 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
}
export interface TradingConfig {
// Position sizing (global fallback)
positionSize: number // USD amount to trade
leverage: number // Leverage multiplier
// Per-symbol settings
solana?: SymbolSettings
ethereum?: SymbolSettings
// Risk management (as percentages of entry price)
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
useAtrBasedTargets: boolean // Enable ATR-based TP2 scaling
atrMultiplierForTp2: number // Multiply ATR by this for dynamic TP2 (e.g., 2.0)
minTp2Percent: number // Minimum TP2 level regardless of ATR
maxTp2Percent: number // Maximum TP2 level 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
breakEvenTriggerPercent: number // When to move SL to breakeven
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
minSignalQualityScore: number // Minimum quality score for initial entry (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)
leverage: 10, // 10x leverage = $500 position size
// Per-symbol settings
solana: {
enabled: true,
positionSize: 210, // $210 base capital
leverage: 10, // 10x leverage = $2100 notional
},
ethereum: {
enabled: true,
positionSize: 4, // $4 base capital (DATA ONLY - minimum size)
leverage: 1, // 1x leverage = $4 notional
},
// Risk parameters (wider for DEX slippage/wicks)
stopLossPercent: -1.5, // -1.5% price = -15% account loss (closes 100%)
takeProfit1Percent: 0.7, // +0.7% price = +7% account gain (closes 50%)
takeProfit2Percent: 1.5, // +1.5% price = +15% account gain (closes 50%)
emergencyStopPercent: -2.0, // -2% hard stop = -20% account loss
// ATR-based dynamic targets (NEW)
useAtrBasedTargets: true, // Enable ATR-based TP2 scaling for big moves
atrMultiplierForTp2: 2.0, // TP2 = ATR × 2.0 (adapts to volatility)
minTp2Percent: 0.7, // Minimum TP2 (safety floor)
maxTp2Percent: 3.0, // Maximum TP2 (cap at 3% for 30% account gain)
// 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
breakEvenTriggerPercent: 0.4, // Move SL to this profit level after TP1 hits
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
minSignalQualityScore: 65, // Minimum quality score for initial entry (raised from 60)
// 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
},
}
// 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'
// 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,
}
}
// 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 dynamic TP2 level based on ATR (Average True Range)
* Higher ATR = higher volatility = larger TP2 target to capture big moves
*/
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
const dynamicTp2 = atrPercent * config.atrMultiplierForTp2
// 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.atrMultiplierForTp2} = ${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,
// 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,
},
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,
},
leverage: process.env.LEVERAGE
? parseInt(process.env.LEVERAGE)
: 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
useAtrBasedTargets: process.env.USE_ATR_BASED_TARGETS
? process.env.USE_ATR_BASED_TARGETS === 'true'
: undefined,
atrMultiplierForTp2: process.env.ATR_MULTIPLIER_FOR_TP2
? parseFloat(process.env.ATR_MULTIPLIER_FOR_TP2)
: 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,
breakEvenTriggerPercent: process.env.BREAKEVEN_TRIGGER_PERCENT
? parseFloat(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,
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
}
// Merge configurations
export function getMergedConfig(
overrides?: Partial<TradingConfig>
): TradingConfig {
const envConfig = getConfigFromEnv()
const config = {
...DEFAULT_TRADING_CONFIG,
...envConfig,
...overrides,
}
validateTradingConfig(config)
return config
}