Files
trading_bot_v4/config/trading.ts
mindesbunister 324e5ba002 refactor: Rename breakEvenTriggerPercent to profitLockAfterTP1Percent for clarity
- Renamed config variable to accurately reflect behavior (locks profit, not breakeven)
- Updated log messages to say 'lock +X% profit' instead of misleading 'breakeven'
- Maintains backwards compatibility (accepts old BREAKEVEN_TRIGGER_PERCENT env var)
- Updated .env with new variable name and explanatory comment

Why: Config was named 'breakeven' but actually locks profit at entry ± X%
For SHORT at $141.51 with 0.3% lock: SL moves to $141.08 (not breakeven $141.51)
This protects remaining runner position after TP1 by allowing small profit giveback

Files changed:
- config/trading.ts: Interface + default + env parsing
- lib/trading/position-manager.ts: Usage + log message
- .env: Variable rename with migration comment
2025-11-15 11:06:44 +01:00

564 lines
21 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
usePercentageSize: boolean // If true, positionSize is % of free collateral
// 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
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
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) OR percentage if usePercentageSize=true
leverage: 10, // 10x leverage = $500 position size
usePercentageSize: false, // False = fixed USD, True = percentage of portfolio
// 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,
},
// 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
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
minSignalQualityScore: 60, // Minimum quality score for initial entry (lowered from 65 on Nov 12, 2025 - data showed 60-64 tier outperformed)
// 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 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
const percentDecimal = configuredSize / 100
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
* This is the main function to use when opening positions
*/
export async function getActualPositionSizeForSymbol(
symbol: string,
baseConfig: TradingConfig,
freeCollateral: number
): 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 {
// 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
)
return {
size: actualSize,
leverage: symbolSettings.leverage,
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
*/
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,
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,
},
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,
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,
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
}