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:
mindesbunister
2025-11-24 00:47:09 +01:00
parent 1e64e8145b
commit bfdb0ba779
4 changed files with 85 additions and 23 deletions

View File

@@ -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({

View File

@@ -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

View File

@@ -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

File diff suppressed because one or more lines are too long