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:
116
app/api/drift/history/route.ts
Normal file
116
app/api/drift/history/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,8 +9,19 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface TradingSettings {
|
||||
// Global fallback settings
|
||||
MAX_POSITION_SIZE_USD: number
|
||||
LEVERAGE: number
|
||||
|
||||
// Per-symbol settings
|
||||
SOLANA_ENABLED: boolean
|
||||
SOLANA_POSITION_SIZE: number
|
||||
SOLANA_LEVERAGE: number
|
||||
ETHEREUM_ENABLED: boolean
|
||||
ETHEREUM_POSITION_SIZE: number
|
||||
ETHEREUM_LEVERAGE: number
|
||||
|
||||
// Risk management
|
||||
STOP_LOSS_PERCENT: number
|
||||
TAKE_PROFIT_1_PERCENT: number
|
||||
TAKE_PROFIT_1_SIZE_PERCENT: number
|
||||
@@ -95,8 +106,8 @@ export default function SettingsPage() {
|
||||
setRestarting(false)
|
||||
}
|
||||
|
||||
const testTrade = async (direction: 'long' | 'short') => {
|
||||
if (!confirm(`⚠️ This will execute a REAL ${direction.toUpperCase()} trade with current settings. Continue?`)) {
|
||||
const testTrade = async (direction: 'long' | 'short', symbol: string = 'SOLUSDT') => {
|
||||
if (!confirm(`⚠️ This will execute a REAL ${direction.toUpperCase()} trade on ${symbol} with current settings. Continue?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,7 +120,7 @@ export default function SettingsPage() {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
symbol: 'SOLUSDT',
|
||||
symbol: symbol,
|
||||
direction: direction,
|
||||
}),
|
||||
})
|
||||
@@ -122,7 +133,7 @@ export default function SettingsPage() {
|
||||
: `SL: $${data.stopLoss?.toFixed(4)}`
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `✅ ${direction.toUpperCase()} test trade executed! Size: $${data.positionSize?.toFixed(2)} | Entry: $${data.entryPrice?.toFixed(4)} | ${dualStopsMsg} | TX: ${data.positionId?.substring(0, 8)}...`
|
||||
text: `✅ ${symbol} ${direction.toUpperCase()} test trade executed! Size: $${data.positionSize?.toFixed(2)} | Entry: $${data.entryPrice?.toFixed(4)} | ${dualStopsMsg} | TX: ${data.positionId?.substring(0, 8)}...`
|
||||
})
|
||||
} else {
|
||||
setMessage({ type: 'error', text: `Failed: ${data.error || data.message}` })
|
||||
@@ -138,11 +149,13 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, [key]: value })
|
||||
}
|
||||
|
||||
const calculateRisk = () => {
|
||||
const calculateRisk = (baseSize?: number, leverage?: number) => {
|
||||
if (!settings) return null
|
||||
const maxLoss = settings.MAX_POSITION_SIZE_USD * settings.LEVERAGE * (Math.abs(settings.STOP_LOSS_PERCENT) / 100)
|
||||
const tp1Gain = settings.MAX_POSITION_SIZE_USD * settings.LEVERAGE * (settings.TAKE_PROFIT_1_PERCENT / 100) * (settings.TAKE_PROFIT_1_SIZE_PERCENT / 100)
|
||||
const tp2Gain = settings.MAX_POSITION_SIZE_USD * settings.LEVERAGE * (settings.TAKE_PROFIT_2_PERCENT / 100) * (settings.TAKE_PROFIT_2_SIZE_PERCENT / 100)
|
||||
const size = baseSize ?? settings.MAX_POSITION_SIZE_USD
|
||||
const lev = leverage ?? settings.LEVERAGE
|
||||
const maxLoss = size * lev * (Math.abs(settings.STOP_LOSS_PERCENT) / 100)
|
||||
const tp1Gain = size * lev * (settings.TAKE_PROFIT_1_PERCENT / 100) * (settings.TAKE_PROFIT_1_SIZE_PERCENT / 100)
|
||||
const tp2Gain = size * lev * (settings.TAKE_PROFIT_2_PERCENT / 100) * (settings.TAKE_PROFIT_2_SIZE_PERCENT / 100)
|
||||
const fullWin = tp1Gain + tp2Gain
|
||||
|
||||
return { maxLoss, tp1Gain, tp2Gain, fullWin }
|
||||
@@ -217,8 +230,150 @@ export default function SettingsPage() {
|
||||
|
||||
{/* Settings Sections */}
|
||||
<div className="space-y-6">
|
||||
{/* Position Sizing */}
|
||||
<Section title="💰 Position Sizing" description="Control your trade size and leverage">
|
||||
{/* Per-Symbol Position Sizing */}
|
||||
<Section title="<EFBFBD> Solana (SOL-PERP)" description="Individual settings for Solana perpetual trading">
|
||||
<div className="mb-4 p-3 bg-purple-500/10 border border-purple-500/30 rounded-lg">
|
||||
<p className="text-sm text-purple-400">
|
||||
Enable/disable Solana trading and set symbol-specific position sizing. When enabled, these settings override global defaults for SOL trades.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-slate-700/30 rounded-lg mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="text-white font-medium mb-1">🟢 Enable Solana Trading</div>
|
||||
<div className="text-slate-400 text-sm">
|
||||
Accept SOL-PERP trade signals from TradingView
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => updateSetting('SOLANA_ENABLED', !settings.SOLANA_ENABLED)}
|
||||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
||||
settings.SOLANA_ENABLED ? 'bg-green-500' : 'bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
||||
settings.SOLANA_ENABLED ? 'translate-x-7' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<Setting
|
||||
label="SOL Position Size (USD)"
|
||||
value={settings.SOLANA_POSITION_SIZE}
|
||||
onChange={(v) => updateSetting('SOLANA_POSITION_SIZE', v)}
|
||||
min={1}
|
||||
max={10000}
|
||||
step={1}
|
||||
description={`Base capital for SOL trades. With ${settings.SOLANA_LEVERAGE}x leverage = $${(settings.SOLANA_POSITION_SIZE * settings.SOLANA_LEVERAGE).toFixed(0)} notional position.`}
|
||||
/>
|
||||
<Setting
|
||||
label="SOL Leverage"
|
||||
value={settings.SOLANA_LEVERAGE}
|
||||
onChange={(v) => updateSetting('SOLANA_LEVERAGE', v)}
|
||||
min={1}
|
||||
max={20}
|
||||
step={1}
|
||||
description="Leverage multiplier for Solana positions only."
|
||||
/>
|
||||
{(() => {
|
||||
const solRisk = calculateRisk(settings.SOLANA_POSITION_SIZE, settings.SOLANA_LEVERAGE)
|
||||
return solRisk && (
|
||||
<div className="p-4 bg-slate-700/50 rounded-lg">
|
||||
<div className="text-sm text-slate-300 mb-2">SOL Risk/Reward</div>
|
||||
<div className="flex gap-4 text-xs">
|
||||
<div>
|
||||
<span className="text-red-400">Max Loss: </span>
|
||||
<span className="text-white font-bold">${solRisk.maxLoss.toFixed(2)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-green-400">Full Win: </span>
|
||||
<span className="text-white font-bold">${solRisk.fullWin.toFixed(2)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-purple-400">R:R </span>
|
||||
<span className="text-white font-bold">1:{(solRisk.fullWin / solRisk.maxLoss).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</Section>
|
||||
|
||||
<Section title="⚡ Ethereum (ETH-PERP)" description="Individual settings for Ethereum perpetual trading">
|
||||
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<p className="text-sm text-blue-400">
|
||||
Enable/disable Ethereum trading and set symbol-specific position sizing. When enabled, these settings override global defaults for ETH trades.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-slate-700/30 rounded-lg mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="text-white font-medium mb-1">🟢 Enable Ethereum Trading</div>
|
||||
<div className="text-slate-400 text-sm">
|
||||
Accept ETH-PERP trade signals from TradingView
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => updateSetting('ETHEREUM_ENABLED', !settings.ETHEREUM_ENABLED)}
|
||||
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-colors ${
|
||||
settings.ETHEREUM_ENABLED ? 'bg-green-500' : 'bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-6 w-6 transform rounded-full bg-white transition-transform ${
|
||||
settings.ETHEREUM_ENABLED ? 'translate-x-7' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<Setting
|
||||
label="ETH Position Size (USD)"
|
||||
value={settings.ETHEREUM_POSITION_SIZE}
|
||||
onChange={(v) => updateSetting('ETHEREUM_POSITION_SIZE', v)}
|
||||
min={1}
|
||||
max={10000}
|
||||
step={1}
|
||||
description={`Base capital for ETH trades. With ${settings.ETHEREUM_LEVERAGE}x leverage = $${(settings.ETHEREUM_POSITION_SIZE * settings.ETHEREUM_LEVERAGE).toFixed(0)} notional position. Drift minimum: ~$38-40 (0.01 ETH).`}
|
||||
/>
|
||||
<Setting
|
||||
label="ETH Leverage"
|
||||
value={settings.ETHEREUM_LEVERAGE}
|
||||
onChange={(v) => updateSetting('ETHEREUM_LEVERAGE', v)}
|
||||
min={1}
|
||||
max={20}
|
||||
step={1}
|
||||
description="Leverage multiplier for Ethereum positions only."
|
||||
/>
|
||||
{(() => {
|
||||
const ethRisk = calculateRisk(settings.ETHEREUM_POSITION_SIZE, settings.ETHEREUM_LEVERAGE)
|
||||
return ethRisk && (
|
||||
<div className="p-4 bg-slate-700/50 rounded-lg">
|
||||
<div className="text-sm text-slate-300 mb-2">ETH Risk/Reward</div>
|
||||
<div className="flex gap-4 text-xs">
|
||||
<div>
|
||||
<span className="text-red-400">Max Loss: </span>
|
||||
<span className="text-white font-bold">${ethRisk.maxLoss.toFixed(2)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-green-400">Full Win: </span>
|
||||
<span className="text-white font-bold">${ethRisk.fullWin.toFixed(2)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-purple-400">R:R </span>
|
||||
<span className="text-white font-bold">1:{(ethRisk.fullWin / ethRisk.maxLoss).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</Section>
|
||||
|
||||
{/* Global Position Sizing (Fallback) */}
|
||||
<Section title="💰 Global Position Sizing (Fallback)" description="Default settings for symbols without specific config (e.g., BTC)">
|
||||
<div className="mb-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<p className="text-sm text-yellow-400">
|
||||
These are fallback values used for any symbol that doesn't have its own specific settings (like BTC-PERP). SOL and ETH ignore these when their own settings are configured above.
|
||||
</p>
|
||||
</div>
|
||||
<Setting
|
||||
label="Position Size (USD)"
|
||||
value={settings.MAX_POSITION_SIZE_USD}
|
||||
@@ -226,7 +381,7 @@ export default function SettingsPage() {
|
||||
min={10}
|
||||
max={10000}
|
||||
step={10}
|
||||
description="Base USD amount per trade. With 5x leverage, $50 = $250 position."
|
||||
description="Base USD amount per trade for unspecified symbols."
|
||||
/>
|
||||
<Setting
|
||||
label="Leverage"
|
||||
@@ -235,7 +390,7 @@ export default function SettingsPage() {
|
||||
min={1}
|
||||
max={20}
|
||||
step={1}
|
||||
description="Multiplier for your position. Higher = more profit AND more risk."
|
||||
description="Leverage multiplier for unspecified symbols."
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -466,21 +621,40 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
|
||||
{/* Test Trade Buttons */}
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => testTrade('long')}
|
||||
disabled={testing}
|
||||
className="flex-1 bg-gradient-to-r from-green-500 to-emerald-500 text-white font-bold py-4 px-6 rounded-lg hover:from-green-600 hover:to-emerald-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-green-400"
|
||||
>
|
||||
{testing ? '🧪 Executing...' : '🧪 Test LONG (REAL)'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => testTrade('short')}
|
||||
disabled={testing}
|
||||
className="flex-1 bg-gradient-to-r from-red-500 to-orange-500 text-white font-bold py-4 px-6 rounded-lg hover:from-red-600 hover:to-orange-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-red-400"
|
||||
>
|
||||
{testing ? '🧪 Executing...' : '🧪 Test SHORT (REAL)'}
|
||||
</button>
|
||||
<div className="space-y-4">
|
||||
<div className="text-center text-slate-300 text-sm font-bold">🧪 Test Trades (REAL MONEY)</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => testTrade('long', 'SOLUSDT')}
|
||||
disabled={testing || !settings.SOLANA_ENABLED}
|
||||
className="flex-1 bg-gradient-to-r from-purple-500 to-purple-600 text-white font-bold py-4 px-6 rounded-lg hover:from-purple-600 hover:to-purple-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-purple-400"
|
||||
>
|
||||
{testing ? '🧪 Executing...' : '💎 Test SOL LONG'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => testTrade('short', 'SOLUSDT')}
|
||||
disabled={testing || !settings.SOLANA_ENABLED}
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-red-500 text-white font-bold py-4 px-6 rounded-lg hover:from-purple-700 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-purple-400"
|
||||
>
|
||||
{testing ? '🧪 Executing...' : '💎 Test SOL SHORT'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => testTrade('long', 'ETHUSDT')}
|
||||
disabled={testing || !settings.ETHEREUM_ENABLED}
|
||||
className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white font-bold py-4 px-6 rounded-lg hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-blue-400"
|
||||
>
|
||||
{testing ? '🧪 Executing...' : '⚡ Test ETH LONG'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => testTrade('short', 'ETHUSDT')}
|
||||
disabled={testing || !settings.ETHEREUM_ENABLED}
|
||||
className="flex-1 bg-gradient-to-r from-blue-600 to-red-500 text-white font-bold py-4 px-6 rounded-lg hover:from-blue-700 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-blue-400"
|
||||
>
|
||||
{testing ? '🧪 Executing...' : '⚡ Test ETH SHORT'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user