feat: Add per-symbol trading controls for SOL and ETH

- Add SymbolSettings interface with enabled/positionSize/leverage fields
- Implement per-symbol ENV variables (SOLANA_*, ETHEREUM_*)
- Add SOL and ETH sections to settings UI with enable/disable toggles
- Add symbol-specific test buttons (SOL LONG/SHORT, ETH LONG/SHORT)
- Update execute and test endpoints to check symbol enabled status
- Add real-time risk/reward calculator per symbol
- Rename 'Position Sizing' to 'Global Fallback' for clarity
- Fix position manager P&L calculation for externally closed positions
- Fix zero P&L bug affecting 12 historical trades
- Add SQL scripts for recalculating historical P&L data
- Move archive TypeScript files to .archive to fix build

Defaults:
- SOL: 10 base × 10x leverage = 100 notional (profit trading)
- ETH:  base × 1x leverage =  notional (data collection)
- Global: 10 × 10x for BTC and other symbols

Configuration priority: Per-symbol ENV > Market config > Global ENV > Defaults
This commit is contained in:
mindesbunister
2025-11-03 10:28:48 +01:00
parent aa8e9f130a
commit 881a99242d
17 changed files with 1996 additions and 108 deletions

View File

@@ -0,0 +1,116 @@
/**
* Query Drift History API
* GET /api/drift/history
*
* Queries Drift Protocol directly to compare with database
*/
import { NextResponse } from 'next/server'
import { initializeDriftService, getDriftService } from '@/lib/drift/client'
import { getPrismaClient } from '@/lib/database/trades'
export async function GET() {
try {
console.log('🔍 Querying Drift Protocol...')
// Initialize Drift service if not already done
console.log('⏳ Calling initializeDriftService()...')
const driftService = await initializeDriftService()
console.log('✅ Drift service initialized, got service object')
console.log('⏳ Getting Drift client...')
const driftClient = driftService.getClient()
console.log('✅ Got Drift client')
// Get user account
const userAccount = driftClient.getUserAccount()
if (!userAccount) {
return NextResponse.json({ error: 'User account not found' }, { status: 404 })
}
// Get account equity and P&L
const equity = driftClient.getUser().getTotalCollateral()
const unrealizedPnL = driftClient.getUser().getUnrealizedPNL()
// Get settled P&L from perp positions
const perpPositions = userAccount.perpPositions
let totalSettledPnL = 0
const positionDetails: any[] = []
for (const position of perpPositions) {
if (position.marketIndex === 0 || position.marketIndex === 1 || position.marketIndex === 2) {
const marketName = position.marketIndex === 0 ? 'SOL-PERP' :
position.marketIndex === 1 ? 'BTC-PERP' : 'ETH-PERP'
const settledPnL = Number(position.settledPnl) / 1e6
const baseAssetAmount = Number(position.baseAssetAmount) / 1e9
totalSettledPnL += settledPnL
positionDetails.push({
market: marketName,
currentPosition: baseAssetAmount,
settledPnL: settledPnL,
})
}
}
// Get spot balance (USDC)
const spotPositions = userAccount.spotPositions
let usdcBalance = 0
let cumulativeDeposits = 0
for (const spot of spotPositions) {
if (spot.marketIndex === 0) { // USDC
usdcBalance = Number(spot.scaledBalance) / 1e9
cumulativeDeposits = Number(spot.cumulativeDeposits) / 1e6
}
}
// Query database for comparison
const prisma = getPrismaClient()
const dbStats = await prisma.trade.aggregate({
where: {
exitReason: { not: null },
entryTime: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
},
_sum: { realizedPnL: true },
_count: true
})
const dbPnL = Number(dbStats._sum.realizedPnL || 0)
const dbTrades = dbStats._count
const discrepancy = totalSettledPnL - dbPnL
const estimatedFeePerTrade = dbTrades > 0 ? discrepancy / dbTrades : 0
return NextResponse.json({
drift: {
totalCollateral: Number(equity) / 1e6,
unrealizedPnL: Number(unrealizedPnL) / 1e6,
settledPnL: totalSettledPnL,
usdcBalance,
cumulativeDeposits,
positions: positionDetails,
},
database: {
totalTrades: dbTrades,
totalPnL: dbPnL,
},
comparison: {
discrepancy,
estimatedFeePerTrade,
note: 'Discrepancy includes funding rates, trading fees, and any manual trades not tracked by bot',
}
})
} catch (error) {
console.error('❌ Error querying Drift:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}

View File

@@ -62,8 +62,19 @@ export async function GET() {
const env = parseEnvFile()
const settings = {
// Global fallback
MAX_POSITION_SIZE_USD: parseFloat(env.MAX_POSITION_SIZE_USD || '50'),
LEVERAGE: parseFloat(env.LEVERAGE || '5'),
// Per-symbol settings
SOLANA_ENABLED: env.SOLANA_ENABLED !== 'false',
SOLANA_POSITION_SIZE: parseFloat(env.SOLANA_POSITION_SIZE || '210'),
SOLANA_LEVERAGE: parseFloat(env.SOLANA_LEVERAGE || '10'),
ETHEREUM_ENABLED: env.ETHEREUM_ENABLED !== 'false',
ETHEREUM_POSITION_SIZE: parseFloat(env.ETHEREUM_POSITION_SIZE || '4'),
ETHEREUM_LEVERAGE: parseFloat(env.ETHEREUM_LEVERAGE || '1'),
// Risk management
STOP_LOSS_PERCENT: parseFloat(env.STOP_LOSS_PERCENT || '-1.5'),
TAKE_PROFIT_1_PERCENT: parseFloat(env.TAKE_PROFIT_1_PERCENT || '0.7'),
TAKE_PROFIT_1_SIZE_PERCENT: parseFloat(env.TAKE_PROFIT_1_SIZE_PERCENT || '50'),

View File

@@ -154,7 +154,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
volumeRatio: body.volumeRatio || 0,
pricePosition: body.pricePosition || 0,
direction: body.direction,
minScore: config.minQualityScore // Use config value
minScore: 60 // Default minimum quality score threshold
})
if (!qualityScore.passed) {

View File

@@ -13,6 +13,84 @@ import { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
import { createTrade } from '@/lib/database/trades'
/**
* Calculate signal quality score (same logic as check-risk endpoint)
*/
function calculateQualityScore(params: {
atr?: number
adx?: number
rsi?: number
volumeRatio?: number
pricePosition?: number
direction: 'long' | 'short'
}): number | undefined {
// If no metrics provided, return undefined
if (!params.atr || params.atr === 0) {
return undefined
}
let score = 50 // Base score
// ATR check
if (params.atr < 0.6) {
score -= 15
} else if (params.atr > 2.5) {
score -= 20
} else {
score += 10
}
// ADX check
if (params.adx && params.adx > 0) {
if (params.adx > 25) {
score += 15
} else if (params.adx < 18) {
score -= 15
} else {
score += 5
}
}
// RSI check
if (params.rsi && params.rsi > 0) {
if (params.direction === 'long') {
if (params.rsi > 50 && params.rsi < 70) {
score += 10
} else if (params.rsi > 70) {
score -= 10
}
} else {
if (params.rsi < 50 && params.rsi > 30) {
score += 10
} else if (params.rsi < 30) {
score -= 10
}
}
}
// Volume check
if (params.volumeRatio && params.volumeRatio > 0) {
if (params.volumeRatio > 1.2) {
score += 10
} else if (params.volumeRatio < 0.8) {
score -= 10
}
}
// Price position check
if (params.pricePosition && params.pricePosition > 0) {
if (params.direction === 'long' && params.pricePosition > 90) {
score -= 15
} else if (params.direction === 'short' && params.pricePosition < 10) {
score -= 15
} else {
score += 5
}
}
return score
}
export interface ExecuteTradeRequest {
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
direction: 'long' | 'short'
@@ -25,7 +103,6 @@ export interface ExecuteTradeRequest {
rsi?: number
volumeRatio?: number
pricePosition?: number
qualityScore?: number // Calculated by check-risk endpoint
}
export interface ExecuteTradeResponse {
@@ -44,7 +121,6 @@ export interface ExecuteTradeResponse {
tp2Percent?: number
entrySlippage?: number
timestamp?: string
qualityScore?: number // Signal quality score (0-100)
error?: string
message?: string
}
@@ -90,6 +166,28 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
// Get trading configuration
const config = getMergedConfig()
// Get symbol-specific position sizing
const { getPositionSizeForSymbol } = await import('@/config/trading')
const { size: positionSize, leverage, enabled } = getPositionSizeForSymbol(driftSymbol, config)
// Check if trading is enabled for this symbol
if (!enabled) {
console.log(`⛔ Trading disabled for ${driftSymbol}`)
return NextResponse.json(
{
success: false,
error: 'Symbol trading disabled',
message: `Trading is currently disabled for ${driftSymbol}. Enable it in settings.`,
},
{ status: 400 }
)
}
console.log(`📐 Symbol-specific sizing for ${driftSymbol}:`)
console.log(` Enabled: ${enabled}`)
console.log(` Position size: $${positionSize}`)
console.log(` Leverage: ${leverage}x`)
// Initialize Drift service if not already initialized
const driftService = await initializeDriftService()
@@ -141,12 +239,12 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
}
// Calculate position size with leverage
const positionSizeUSD = config.positionSize * config.leverage
const positionSizeUSD = positionSize * leverage
console.log(`💰 Opening ${body.direction} position:`)
console.log(` Symbol: ${driftSymbol}`)
console.log(` Base size: $${config.positionSize}`)
console.log(` Leverage: ${config.leverage}x`)
console.log(` Base size: $${positionSize}`)
console.log(` Leverage: ${leverage}x`)
console.log(` Total position: $${positionSizeUSD}`)
// Open position
@@ -246,6 +344,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
unrealizedPnL: 0,
peakPnL: 0,
peakPrice: entryPrice,
// MAE/MFE tracking
maxFavorableExcursion: 0,
maxAdverseExcursion: 0,
maxFavorablePrice: entryPrice,
@@ -287,6 +386,11 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
console.error('❌ Unexpected error placing exit orders:', err)
}
// Add to position manager for monitoring AFTER orders are placed
await positionManager.addTrade(activeTrade)
console.log('✅ Trade added to position manager for monitoring')
// Create response object
const response: ExecuteTradeResponse = {
success: true,
@@ -304,7 +408,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
tp2Percent: config.takeProfit2Percent,
entrySlippage: openResult.slippage,
timestamp: new Date().toISOString(),
qualityScore: body.qualityScore, // Include quality score in response
}
// Attach exit order signatures to response
@@ -314,6 +417,16 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
// Save trade to database
try {
// Calculate quality score if metrics available
const qualityScore = calculateQualityScore({
atr: body.atr,
adx: body.adx,
rsi: body.rsi,
volumeRatio: body.volumeRatio,
pricePosition: body.pricePosition,
direction: body.direction,
})
await createTrade({
positionId: openResult.transactionSignature!,
symbol: driftSymbol,
@@ -343,20 +456,19 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
rsiAtEntry: body.rsi,
volumeAtEntry: body.volumeRatio,
pricePositionAtEntry: body.pricePosition,
signalQualityScore: body.qualityScore,
signalQualityScore: qualityScore,
})
console.log('💾 Trade saved to database')
if (qualityScore !== undefined) {
console.log(`💾 Trade saved with quality score: ${qualityScore}/100`)
} else {
console.log('💾 Trade saved to database')
}
} catch (dbError) {
console.error('❌ Failed to save trade to database:', dbError)
// Don't fail the trade if database save fails
}
// NOW add to position manager for monitoring (after database save)
await positionManager.addTrade(activeTrade)
console.log('✅ Trade added to position manager for monitoring')
console.log('✅ Trade executed successfully!')
return NextResponse.json(response)

View File

@@ -55,9 +55,23 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
// Get symbol-specific position sizing
const { getPositionSizeForSymbol } = await import('@/config/trading')
const { size: positionSize, leverage } = getPositionSizeForSymbol(driftSymbol, config)
const { size: positionSize, leverage, enabled } = getPositionSizeForSymbol(driftSymbol, config)
// Check if trading is enabled for this symbol
if (!enabled) {
console.log(`⛔ Trading disabled for ${driftSymbol}`)
return NextResponse.json(
{
success: false,
error: 'Symbol trading disabled',
message: `Trading is currently disabled for ${driftSymbol}. Enable it in settings.`,
},
{ status: 400 }
)
}
console.log(`📐 Symbol-specific sizing for ${driftSymbol}:`)
console.log(` Enabled: ${enabled}`)
console.log(` Position size: $${positionSize}`)
console.log(` Leverage: ${leverage}x`)
@@ -185,6 +199,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
unrealizedPnL: 0,
peakPnL: 0,
peakPrice: entryPrice,
// MAE/MFE tracking
maxFavorableExcursion: 0,
maxAdverseExcursion: 0,
maxFavorablePrice: entryPrice,
@@ -194,6 +209,12 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
lastUpdateTime: Date.now(),
}
// Add to position manager for monitoring
const positionManager = await getInitializedPositionManager()
await positionManager.addTrade(activeTrade)
console.log('✅ Trade added to position manager for monitoring')
// Create response object
const response: TestTradeResponse = {
success: true,
@@ -246,7 +267,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
console.error('❌ Unexpected error placing exit orders:', err)
}
// Save trade to database FIRST (before Position Manager)
// Save trade to database
try {
await createTrade({
positionId: openResult.transactionSignature!,
@@ -279,12 +300,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<TestTrade
// Don't fail the trade if database save fails
}
// NOW add to position manager for monitoring (after database save)
const positionManager = await getInitializedPositionManager()
await positionManager.addTrade(activeTrade)
console.log('✅ Trade added to position manager for monitoring')
console.log('✅ Test trade executed successfully!')
return NextResponse.json(response)