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()
|
||||
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 { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
|
||||
driftSymbol,
|
||||
config,
|
||||
health.freeCollateral
|
||||
health.freeCollateral,
|
||||
qualityResult.score // Pass quality score for adaptive leverage
|
||||
)
|
||||
|
||||
// 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...')
|
||||
|
||||
// Save trade to database FIRST (CRITICAL: Must succeed before Position Manager)
|
||||
let qualityResult
|
||||
try {
|
||||
// Calculate quality score if metrics available
|
||||
console.log('🔍 DEBUG: Calling scoreSignalQuality()...')
|
||||
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)
|
||||
// Quality score already calculated earlier for adaptive leverage
|
||||
console.log('🔍 DEBUG: Using quality score from earlier calculation:', qualityResult.score)
|
||||
console.log('🔍 DEBUG: About to call createTrade()...')
|
||||
|
||||
await createTrade({
|
||||
|
||||
@@ -74,11 +74,13 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
|
||||
}
|
||||
|
||||
// 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 { size: positionSize, leverage, enabled, usePercentage } = await getActualPositionSizeForSymbol(
|
||||
driftSymbol,
|
||||
config,
|
||||
health.freeCollateral
|
||||
health.freeCollateral,
|
||||
100 // Test trades always use max leverage (quality 100)
|
||||
)
|
||||
|
||||
// Check if trading is enabled for this symbol
|
||||
|
||||
@@ -14,9 +14,15 @@ export interface SymbolSettings {
|
||||
export interface TradingConfig {
|
||||
// Position sizing (global fallback)
|
||||
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
|
||||
|
||||
// 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
|
||||
solana?: SymbolSettings
|
||||
ethereum?: SymbolSettings
|
||||
@@ -104,9 +110,16 @@ export interface MarketConfig {
|
||||
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
|
||||
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,
|
||||
@@ -308,12 +321,14 @@ export function calculateActualPositionSize(
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export async function getActualPositionSizeForSymbol(
|
||||
symbol: string,
|
||||
baseConfig: TradingConfig,
|
||||
freeCollateral: number
|
||||
freeCollateral: number,
|
||||
qualityScore?: number // NEW: Optional quality score for adaptive leverage
|
||||
): Promise<{ size: number; leverage: number; enabled: boolean; usePercentage: boolean }> {
|
||||
let symbolSettings: { size: number; leverage: number; enabled: boolean }
|
||||
let usePercentage = false
|
||||
@@ -351,9 +366,16 @@ export async function getActualPositionSizeForSymbol(
|
||||
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 {
|
||||
size: actualSize,
|
||||
leverage: symbolSettings.leverage,
|
||||
leverage: finalLeverage, // Use adaptive leverage if quality score provided
|
||||
enabled: symbolSettings.enabled,
|
||||
usePercentage,
|
||||
}
|
||||
@@ -468,6 +490,21 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
|
||||
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,
|
||||
|
||||
stopLossPercent: process.env.STOP_LOSS_PERCENT
|
||||
? parseFloat(process.env.STOP_LOSS_PERCENT)
|
||||
: undefined,
|
||||
@@ -613,6 +650,26 @@ export function getMinQualityScoreForDirection(
|
||||
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
|
||||
export function getMergedConfig(
|
||||
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