feat: Implement adaptive leverage based on signal quality score
- Quality-based risk adjustment: 95+ = 15x, 90-94 = 10x, <90 = blocked - Data-driven decision: v8 quality 95+ = 100% WR (4/4 wins) - Config fields: useAdaptiveLeverage, highQualityLeverage, lowQualityLeverage, qualityLeverageThreshold - Helper function: getLeverageForQualityScore() returns appropriate leverage tier - Position sizing: Modified getActualPositionSizeForSymbol() to accept optional qualityScore param - Execute endpoint: Calculate quality score early (before sizing) for leverage determination - Test endpoint: Uses quality 100 for maximum leverage on manual test trades - ENV variables: USE_ADAPTIVE_LEVERAGE, HIGH_QUALITY_LEVERAGE, LOW_QUALITY_LEVERAGE, QUALITY_LEVERAGE_THRESHOLD - Impact: 33% less exposure on borderline quality signals (90-94) - Example: $540 × 10x = $5,400 vs $8,100 (saves $2,700 exposure on volatile signals) - Files changed: * config/trading.ts (interface, config, ENV, helper function, position sizing) * app/api/trading/execute/route.ts (early quality calculation, pass to sizing) * app/api/trading/test/route.ts (quality 100 for test trades)
This commit is contained in:
@@ -167,12 +167,28 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
const health = await driftService.getAccountHealth()
|
const health = await driftService.getAccountHealth()
|
||||||
console.log(`🩺 Account health: Free collateral $${health.freeCollateral.toFixed(2)}`)
|
console.log(`🩺 Account health: Free collateral $${health.freeCollateral.toFixed(2)}`)
|
||||||
|
|
||||||
// Get symbol-specific position sizing (supports percentage-based sizing)
|
// Calculate quality score EARLY for adaptive leverage (Nov 24, 2025)
|
||||||
|
// This needs to happen before position sizing so leverage can be adjusted based on quality
|
||||||
|
const qualityResult = await scoreSignalQuality({
|
||||||
|
atr: body.atr || 0,
|
||||||
|
adx: body.adx || 0,
|
||||||
|
rsi: body.rsi || 0,
|
||||||
|
volumeRatio: body.volumeRatio || 0,
|
||||||
|
pricePosition: body.pricePosition || 0,
|
||||||
|
direction: body.direction,
|
||||||
|
symbol: driftSymbol,
|
||||||
|
currentPrice: body.signalPrice || 0,
|
||||||
|
timeframe: body.timeframe,
|
||||||
|
})
|
||||||
|
console.log(`📊 Signal quality score: ${qualityResult.score} (calculated early for adaptive leverage)`)
|
||||||
|
|
||||||
|
// Get symbol-specific position sizing with quality score for adaptive leverage
|
||||||
const { getActualPositionSizeForSymbol } = await import('@/config/trading')
|
const { getActualPositionSizeForSymbol } = await import('@/config/trading')
|
||||||
const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
|
const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
|
||||||
driftSymbol,
|
driftSymbol,
|
||||||
config,
|
config,
|
||||||
health.freeCollateral
|
health.freeCollateral,
|
||||||
|
qualityResult.score // Pass quality score for adaptive leverage
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if trading is enabled for this symbol
|
// Check if trading is enabled for this symbol
|
||||||
@@ -748,23 +764,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
console.log('🔍 DEBUG: Exit orders section complete, about to calculate quality score...')
|
console.log('🔍 DEBUG: Exit orders section complete, about to calculate quality score...')
|
||||||
|
|
||||||
// Save trade to database FIRST (CRITICAL: Must succeed before Position Manager)
|
// Save trade to database FIRST (CRITICAL: Must succeed before Position Manager)
|
||||||
let qualityResult
|
|
||||||
try {
|
try {
|
||||||
// Calculate quality score if metrics available
|
// Quality score already calculated earlier for adaptive leverage
|
||||||
console.log('🔍 DEBUG: Calling scoreSignalQuality()...')
|
console.log('🔍 DEBUG: Using quality score from earlier calculation:', qualityResult.score)
|
||||||
qualityResult = await scoreSignalQuality({
|
|
||||||
atr: body.atr || 0,
|
|
||||||
adx: body.adx || 0,
|
|
||||||
rsi: body.rsi || 0,
|
|
||||||
volumeRatio: body.volumeRatio || 0,
|
|
||||||
pricePosition: body.pricePosition || 0,
|
|
||||||
direction: body.direction,
|
|
||||||
symbol: driftSymbol,
|
|
||||||
currentPrice: openResult.fillPrice,
|
|
||||||
timeframe: body.timeframe,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('🔍 DEBUG: scoreSignalQuality() completed, score:', qualityResult.score)
|
|
||||||
console.log('🔍 DEBUG: About to call createTrade()...')
|
console.log('🔍 DEBUG: About to call createTrade()...')
|
||||||
|
|
||||||
await createTrade({
|
await createTrade({
|
||||||
|
|||||||
@@ -74,11 +74,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get symbol-specific position sizing (with percentage support)
|
// Get symbol-specific position sizing (with percentage support)
|
||||||
|
// Test trades use quality score 100 for maximum leverage (manual override)
|
||||||
const { getActualPositionSizeForSymbol } = await import('@/config/trading')
|
const { getActualPositionSizeForSymbol } = await import('@/config/trading')
|
||||||
const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
|
const { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
|
||||||
driftSymbol,
|
driftSymbol,
|
||||||
config,
|
config,
|
||||||
health.freeCollateral
|
health.freeCollateral,
|
||||||
|
100 // Test trades always use max leverage (quality 100)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if trading is enabled for this symbol
|
// Check if trading is enabled for this symbol
|
||||||
|
|||||||
@@ -14,9 +14,15 @@ export interface SymbolSettings {
|
|||||||
export interface TradingConfig {
|
export interface TradingConfig {
|
||||||
// Position sizing (global fallback)
|
// Position sizing (global fallback)
|
||||||
positionSize: number // USD amount to trade (or percentage if usePercentageSize=true)
|
positionSize: number // USD amount to trade (or percentage if usePercentageSize=true)
|
||||||
leverage: number // Leverage multiplier
|
leverage: number // Leverage multiplier (LEGACY - used when adaptive disabled)
|
||||||
usePercentageSize: boolean // If true, positionSize is % of free collateral
|
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)
|
||||||
|
|
||||||
// Per-symbol settings
|
// Per-symbol settings
|
||||||
solana?: SymbolSettings
|
solana?: SymbolSettings
|
||||||
ethereum?: SymbolSettings
|
ethereum?: SymbolSettings
|
||||||
@@ -104,9 +110,16 @@ export interface MarketConfig {
|
|||||||
export const DEFAULT_TRADING_CONFIG: TradingConfig = {
|
export const DEFAULT_TRADING_CONFIG: TradingConfig = {
|
||||||
// Position sizing (global fallback)
|
// Position sizing (global fallback)
|
||||||
positionSize: 50, // $50 base capital (SAFE FOR TESTING) OR percentage if usePercentageSize=true
|
positionSize: 50, // $50 base capital (SAFE FOR TESTING) OR percentage if usePercentageSize=true
|
||||||
leverage: 10, // 10x leverage = $500 position size
|
leverage: 10, // 10x leverage = $500 position size (LEGACY - used when adaptive disabled)
|
||||||
usePercentageSize: false, // False = fixed USD, True = percentage of portfolio
|
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
|
// Per-symbol settings
|
||||||
solana: {
|
solana: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -308,12 +321,14 @@ export function calculateActualPositionSize(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get actual position size for symbol with percentage support
|
* Get actual position size for symbol with percentage support
|
||||||
|
* Now supports adaptive leverage based on quality score (Nov 24, 2025)
|
||||||
* This is the main function to use when opening positions
|
* This is the main function to use when opening positions
|
||||||
*/
|
*/
|
||||||
export async function getActualPositionSizeForSymbol(
|
export async function getActualPositionSizeForSymbol(
|
||||||
symbol: string,
|
symbol: string,
|
||||||
baseConfig: TradingConfig,
|
baseConfig: TradingConfig,
|
||||||
freeCollateral: number
|
freeCollateral: number,
|
||||||
|
qualityScore?: number // NEW: Optional quality score for adaptive leverage
|
||||||
): Promise<{ size: number; leverage: number; enabled: boolean; usePercentage: boolean }> {
|
): Promise<{ size: number; leverage: number; enabled: boolean; usePercentage: boolean }> {
|
||||||
let symbolSettings: { size: number; leverage: number; enabled: boolean }
|
let symbolSettings: { size: number; leverage: number; enabled: boolean }
|
||||||
let usePercentage = false
|
let usePercentage = false
|
||||||
@@ -351,9 +366,16 @@ export async function getActualPositionSizeForSymbol(
|
|||||||
freeCollateral
|
freeCollateral
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NEW (Nov 24, 2025): Apply adaptive leverage based on quality score
|
||||||
|
let finalLeverage = symbolSettings.leverage
|
||||||
|
if (qualityScore !== undefined && baseConfig.useAdaptiveLeverage) {
|
||||||
|
finalLeverage = getLeverageForQualityScore(qualityScore, baseConfig)
|
||||||
|
console.log(`📊 Adaptive leverage: Quality ${qualityScore} → ${finalLeverage}x leverage (threshold: ${baseConfig.qualityLeverageThreshold})`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
size: actualSize,
|
size: actualSize,
|
||||||
leverage: symbolSettings.leverage,
|
leverage: finalLeverage, // Use adaptive leverage if quality score provided
|
||||||
enabled: symbolSettings.enabled,
|
enabled: symbolSettings.enabled,
|
||||||
usePercentage,
|
usePercentage,
|
||||||
}
|
}
|
||||||
@@ -468,6 +490,21 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
|
|||||||
leverage: process.env.LEVERAGE
|
leverage: process.env.LEVERAGE
|
||||||
? parseInt(process.env.LEVERAGE)
|
? parseInt(process.env.LEVERAGE)
|
||||||
: undefined,
|
: 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,
|
||||||
|
|
||||||
stopLossPercent: process.env.STOP_LOSS_PERCENT
|
stopLossPercent: process.env.STOP_LOSS_PERCENT
|
||||||
? parseFloat(process.env.STOP_LOSS_PERCENT)
|
? parseFloat(process.env.STOP_LOSS_PERCENT)
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -613,6 +650,26 @@ export function getMinQualityScoreForDirection(
|
|||||||
return config.minSignalQualityScore
|
return config.minSignalQualityScore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get leverage based on signal quality score (Nov 24, 2025)
|
||||||
|
// Data-driven: v8 quality 95+ = 100% WR (4/4 wins), quality 90-94 more volatile
|
||||||
|
export function getLeverageForQualityScore(
|
||||||
|
qualityScore: number,
|
||||||
|
config: TradingConfig
|
||||||
|
): number {
|
||||||
|
// If adaptive leverage disabled, use fixed leverage
|
||||||
|
if (!config.useAdaptiveLeverage) {
|
||||||
|
return config.leverage
|
||||||
|
}
|
||||||
|
|
||||||
|
// High quality signals get maximum leverage
|
||||||
|
if (qualityScore >= config.qualityLeverageThreshold) {
|
||||||
|
return config.highQualityLeverage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lower quality signals get reduced leverage
|
||||||
|
return config.lowQualityLeverage
|
||||||
|
}
|
||||||
|
|
||||||
// Merge configurations
|
// Merge configurations
|
||||||
export function getMergedConfig(
|
export function getMergedConfig(
|
||||||
overrides?: Partial<TradingConfig>
|
overrides?: Partial<TradingConfig>
|
||||||
|
|||||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user