feat: Direction-specific quality thresholds (long=90, short=95)

- DATA-DRIVEN: 227 trades analysis showed longs 71.4% WR vs shorts 28.6% WR at quality 90-94
- LONG threshold: 90 (captures profitable 90-94 signals: +4.77 total, +.40 avg)
- SHORT threshold: 95 (blocks toxic 90-94 signals: -53.76 total, -9.11 avg)
- Historical validation: Quality 90+ longs +00.62 vs shorts -77.90

Modified files:
- config/trading.ts: Added minSignalQualityScoreLong/Short fields + getMinQualityScoreForDirection()
- lib/trading/signal-quality.ts: Accept direction-specific minScore parameter
- app/api/trading/check-risk/route.ts: Use direction-specific thresholds
- .env: Added MIN_SIGNAL_QUALITY_SCORE_LONG=90 and _SHORT=95

Fallback logic: direction-specific → global → 60 default
Backward compatible with existing code
This commit is contained in:
mindesbunister
2025-11-23 15:01:56 +01:00
parent 625566224a
commit 01aaa0932a
5 changed files with 111 additions and 9 deletions

2
.env
View File

@@ -391,6 +391,8 @@ USE_TRAILING_STOP=true
TRAILING_STOP_PERCENT=0.5
TRAILING_STOP_ACTIVATION=0.4
MIN_SIGNAL_QUALITY_SCORE=91
MIN_SIGNAL_QUALITY_SCORE_LONG=90 # Nov 23, 2025: Longs have 71.4% WR at quality 90-94 (+$44.77 on 7 trades)
MIN_SIGNAL_QUALITY_SCORE_SHORT=95 # Nov 23, 2025: Shorts toxic at quality 90-94 (28.6% WR, -$553.76 on 7 trades)
SOLANA_ENABLED=true
SOLANA_POSITION_SIZE=100
SOLANA_LEVERAGE=15

View File

@@ -2900,7 +2900,75 @@ trade.realizedPnL += actualRealizedPnL // NOT: result.realizedPnL from SDK
* Runner P&L calculated on actual runner size
- **Lesson:** Container restarts during active trades are high-risk events. All startup validation MUST use correct schema fields and understand trade lifecycle state (pre-TP1 vs post-TP1).
54. **Settings UI quality score variable name mismatch (CRITICAL - Fixed Nov 19, 2025):**
54. **MFE/MAE storing dollars instead of percentages (CRITICAL - Fixed Nov 23, 2025):**
- **Symptom:** Database showing maxFavorableExcursion = 64.08% when TradingView charts showed 0.48% actual max profit
- **Root Cause:** Position Manager storing DOLLAR amounts instead of PERCENTAGES in MFE/MAE fields
- **Discovery:** User provided TradingView screenshots showing 0.48% max profit, database query showed 64.08% stored value
- **Real incident (Nov 22-23, 2025):**
* Trade cmiahpupc0000pe07g2dh58ow (quality 90 SHORT)
* Actual max profit: 0.48% per TradingView chart
* Database stored: 64.08 (interpreted as 64.08%)
* Actual calculation: $64.08 profit / $7,756 position = 0.83%
* Even 0.83% was wrong - actual TradingView showed 0.48%
* **Discrepancy: 133× inflation (64.08% vs 0.48%)**
- **Bug mechanism:**
```typescript
// BEFORE (BROKEN - line 1127 of position-manager.ts):
const profitPercent = this.calculateProfitPercent(entry, currentPrice, direction)
const currentPnLDollars = (trade.currentSize * profitPercent) / 100
// Track MAE/MFE in DOLLAR amounts (not percentages!) ← WRONG COMMENT
// CRITICAL: Database schema expects DOLLARS ← WRONG ASSUMPTION
if (currentPnLDollars > trade.maxFavorableExcursion) {
trade.maxFavorableExcursion = currentPnLDollars // Storing $64.08
trade.maxFavorablePrice = currentPrice
}
if (currentPnLDollars < trade.maxAdverseExcursion) {
trade.maxAdverseExcursion = currentPnLDollars // Storing $-82.59
trade.maxAdversePrice = currentPrice
}
// AFTER (FIXED):
// Track MAE/MFE in PERCENTAGE (not dollars!)
// CRITICAL FIX (Nov 23, 2025): Schema expects % (0.48 = 0.48%), not dollar amounts
// Bug was storing $64.08 when actual was 0.48%, causing 100× inflation in analysis
if (profitPercent > trade.maxFavorableExcursion) {
trade.maxFavorableExcursion = profitPercent // Storing 0.48%
trade.maxFavorablePrice = currentPrice
}
if (profitPercent < trade.maxAdverseExcursion) {
trade.maxAdverseExcursion = profitPercent // Storing -0.82%
trade.maxAdversePrice = currentPrice
}
```
- **Schema confirmation:**
```prisma
// prisma/schema.prisma lines 54-55
maxFavorableExcursion Float? // Best profit % reached during trade
maxAdverseExcursion Float? // Worst drawdown % during trade
```
- **Impact:**
* All 14 quality 90 trades: MFE/MAE values inflated by 100-133×
* Example: Database 64.08% when actual 0.48% = 133× inflation
* Quality tier analysis: Used wrong MFE values but directional conclusions valid
* TP1-only simulations: Percentages wrong but improvement trend correct
* Historical data: Cannot auto-correct (requires manual TradingView chart review)
* Future trades: Will track correctly with deployed fix
- **User response:** "first we need to find the reason why we store wrong data. thats a big problem"
- **Investigation:** Grep searched position-manager.ts for MFE assignments, found line 1127 storing currentPnLDollars
- **Fix implementation:**
* Changed assignment from currentPnLDollars to profitPercent
* Updated comment explaining percentage storage
* Docker build: Completed successfully (~90 seconds)
* Container restart: 13:18:54 UTC Nov 23, 2025
* Git commit: 6255662 "critical: Fix MFE/MAE storing dollars instead of percentages"
* Verification: Container timestamp 50 seconds newer than commit ✅
- **Validation required:** Monitor next trade's MFE/MAE values, compare to TradingView chart
- **Expected behavior:** Should show ~0.5% max profit, not ~50% (percentages not dollars)
- **Status:** ✅ Fix deployed and running in production
- **Lesson:** Always verify data storage units match schema expectations. Comments saying "stores dollars" don't override schema comments saying "stores percentages." When user reports data discrepancies between charts and database, investigate storage logic immediately - don't assume data is correct. All financial metrics need unit validation (dollars vs percentages, tokens vs USD, etc.).
55. **Settings UI quality score variable name mismatch (CRITICAL - Fixed Nov 19, 2025):**
- **Symptom:** User changes "Min Signal Quality" in settings UI (e.g., 60 → 81), but trades continue executing with old threshold
- **Root Cause:** Settings API reading/writing wrong ENV variable name
- **Variable name inconsistency:**

View File

@@ -6,7 +6,7 @@
*/
import { NextRequest, NextResponse } from 'next/server'
import { getMergedConfig, TradingConfig } from '@/config/trading'
import { getMergedConfig, TradingConfig, getMinQualityScoreForDirection } from '@/config/trading'
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
import { getLastTradeTime, getLastTradeTimeForSymbol, getTradesInLastHour, getTodayPnL, createBlockedSignal } from '@/lib/database/trades'
import { getPythPriceMonitor } from '@/lib/pyth/price-monitor'
@@ -329,6 +329,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
const latestPrice = priceMonitor.getCachedPrice(body.symbol)
const currentPrice = latestPrice?.price || body.currentPrice
// Use direction-specific quality threshold (Nov 23, 2025)
const minQualityScore = getMinQualityScoreForDirection(body.direction, config)
const qualityScore = await scoreSignalQuality({
atr: body.atr || 0,
adx: body.adx || 0,
@@ -339,13 +342,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
symbol: body.symbol,
currentPrice: currentPrice,
timeframe: body.timeframe, // Pass timeframe for context-aware scoring
minScore: config.minSignalQualityScore // Use config value
minScore: minQualityScore // Use direction-specific threshold
})
if (!qualityScore.passed) {
console.log('🚫 Risk check BLOCKED: Signal quality too low', {
score: qualityScore.score,
threshold: config.minSignalQualityScore,
direction: body.direction,
threshold: minQualityScore,
reasons: qualityScore.reasons
})
@@ -367,7 +371,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
signalQualityScore: qualityScore.score,
signalQualityVersion: 'v4', // Update this when scoring logic changes
scoreBreakdown: { reasons: qualityScore.reasons },
minScoreRequired: config.minSignalQualityScore,
minScoreRequired: minQualityScore, // Use direction-specific threshold
blockReason: 'QUALITY_SCORE_TOO_LOW',
blockDetails: `Score: ${qualityScore.score}/${config.minSignalQualityScore} - ${qualityScore.reasons.join(', ')}`,
})

View File

@@ -58,8 +58,10 @@ export interface TradingConfig {
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)
// Signal Quality (Direction-specific thresholds - Nov 23, 2025)
minSignalQualityScore: number // Global fallback (0-100)
minSignalQualityScoreLong?: number // Override for LONG signals (0-100)
minSignalQualityScoreShort?: number // Override for SHORT signals (0-100)
// Position Scaling (add to winning positions)
enablePositionScaling: boolean // Allow scaling into existing positions
@@ -156,8 +158,12 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = {
trailingStopMaxPercent: 0.9, // Cap trailing distance at 0.9%
trailingStopActivation: 0.5, // Activate trailing when runner is +0.5% in profit
// Signal Quality
minSignalQualityScore: 91, // Raised to 91 on Nov 21, 2025 after trade #7 (ADX 19.0 weak trend, quality 90, -$387 loss) - v8 averaging 93.6 with 57.1% WR
// Signal Quality (Direction-specific thresholds - Nov 23, 2025 DATA-DRIVEN UPDATE)
minSignalQualityScore: 91, // Global fallback (unchanged)
minSignalQualityScoreLong: 90, // LONGS: 71.4% WR at quality 90-94 (+$44.77 on 7 trades, +$6.40 avg)
minSignalQualityScoreShort: 95, // SHORTS: Keep strict (quality 90-94 = 28.6% WR, -$553.76 on 7 trades)
// Historical validation: Quality 90+ longs = 50% WR +$600.62 (38 trades), shorts = 47.4% WR -$177.90
// v8 data: 3 longs 100% WR +$565, 7 shorts 42.9% WR -$311
// Position Scaling (conservative defaults)
enablePositionScaling: false, // Disabled by default - enable after testing
@@ -552,6 +558,12 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
minSignalQualityScore: process.env.MIN_SIGNAL_QUALITY_SCORE
? parseInt(process.env.MIN_SIGNAL_QUALITY_SCORE)
: undefined,
minSignalQualityScoreLong: process.env.MIN_SIGNAL_QUALITY_SCORE_LONG
? parseInt(process.env.MIN_SIGNAL_QUALITY_SCORE_LONG)
: undefined,
minSignalQualityScoreShort: process.env.MIN_SIGNAL_QUALITY_SCORE_SHORT
? parseInt(process.env.MIN_SIGNAL_QUALITY_SCORE_SHORT)
: undefined,
enablePositionScaling: process.env.ENABLE_POSITION_SCALING
? process.env.ENABLE_POSITION_SCALING === 'true'
: undefined,
@@ -587,6 +599,20 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
return config
}
// Get minimum quality score for a specific direction (Nov 23, 2025)
export function getMinQualityScoreForDirection(
direction: 'long' | 'short',
config: TradingConfig
): number {
if (direction === 'long' && config.minSignalQualityScoreLong !== undefined) {
return config.minSignalQualityScoreLong
}
if (direction === 'short' && config.minSignalQualityScoreShort !== undefined) {
return config.minSignalQualityScoreShort
}
return config.minSignalQualityScore
}
// Merge configurations
export function getMergedConfig(
overrides?: Partial<TradingConfig>

View File

@@ -263,6 +263,8 @@ export async function scoreSignalQuality(params: {
}
}
// Direction-specific threshold support (Nov 23, 2025)
// Use provided minScore, or fall back to 60 if not specified
const minScore = params.minScore || 60
const passed = score >= minScore