feat: implement dual stop system and database tracking
- Add PostgreSQL database with Prisma ORM - Trade model: tracks entry/exit, P&L, order signatures, config snapshots - PriceUpdate model: tracks price movements for drawdown analysis - SystemEvent model: logs errors and system events - DailyStats model: aggregated performance metrics - Implement dual stop loss system (enabled by default) - Soft stop (TRIGGER_LIMIT) at -1.5% to avoid wicks - Hard stop (TRIGGER_MARKET) at -2.5% to guarantee exit - Configurable via USE_DUAL_STOPS, SOFT_STOP_PERCENT, HARD_STOP_PERCENT - Backward compatible with single stop modes - Add database service layer (lib/database/trades.ts) - createTrade(): save new trades with all details - updateTradeExit(): close trades with P&L calculations - addPriceUpdate(): track price movements during trade - getTradeStats(): calculate win rate, profit factor, avg win/loss - logSystemEvent(): log errors and system events - Update execute endpoint to use dual stops and save to database - Calculate dual stop prices when enabled - Pass dual stop parameters to placeExitOrders - Save complete trade record to database after execution - Add test trade button to settings page - New /api/trading/test endpoint for executing test trades - Displays detailed results including dual stop prices - Confirmation dialog before execution - Shows entry price, position size, stops, and TX signature - Generate Prisma client in Docker build - Update DATABASE_URL for container networking
This commit is contained in:
@@ -11,6 +11,7 @@ import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
||||
import { normalizeTradingViewSymbol } from '@/config/trading'
|
||||
import { getMergedConfig } from '@/config/trading'
|
||||
import { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||
import { createTrade } from '@/lib/database/trades'
|
||||
|
||||
export interface ExecuteTradeRequest {
|
||||
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
||||
@@ -135,6 +136,26 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
body.direction
|
||||
)
|
||||
|
||||
// Calculate dual stop prices if enabled
|
||||
let softStopPrice: number | undefined
|
||||
let hardStopPrice: number | undefined
|
||||
|
||||
if (config.useDualStops) {
|
||||
softStopPrice = calculatePrice(
|
||||
entryPrice,
|
||||
config.softStopPercent,
|
||||
body.direction
|
||||
)
|
||||
hardStopPrice = calculatePrice(
|
||||
entryPrice,
|
||||
config.hardStopPercent,
|
||||
body.direction
|
||||
)
|
||||
console.log('🛡️🛡️ Dual stop system enabled:')
|
||||
console.log(` Soft stop: $${softStopPrice.toFixed(4)} (${config.softStopPercent}%)`)
|
||||
console.log(` Hard stop: $${hardStopPrice.toFixed(4)} (${config.hardStopPercent}%)`)
|
||||
}
|
||||
|
||||
const tp1Price = calculatePrice(
|
||||
entryPrice,
|
||||
config.takeProfit1Percent,
|
||||
@@ -211,6 +232,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
}
|
||||
|
||||
// Place on-chain TP/SL orders so they appear in Drift UI (reduce-only LIMIT orders)
|
||||
let exitOrderSignatures: string[] = []
|
||||
try {
|
||||
const exitRes = await placeExitOrders({
|
||||
symbol: driftSymbol,
|
||||
@@ -221,12 +243,18 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
||||
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
||||
direction: body.direction,
|
||||
// Dual stop parameters
|
||||
useDualStops: config.useDualStops,
|
||||
softStopPrice: softStopPrice,
|
||||
softStopBuffer: config.softStopBuffer,
|
||||
hardStopPrice: hardStopPrice,
|
||||
})
|
||||
|
||||
if (!exitRes.success) {
|
||||
console.error('❌ Failed to place on-chain exit orders:', exitRes.error)
|
||||
} else {
|
||||
console.log('📨 Exit orders placed on-chain:', exitRes.signatures)
|
||||
exitOrderSignatures = exitRes.signatures || []
|
||||
}
|
||||
|
||||
// Attach signatures to response when available
|
||||
@@ -237,7 +265,38 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
console.error('❌ Unexpected error placing exit orders:', err)
|
||||
}
|
||||
|
||||
// TODO: Save trade to database (add Prisma integration later)
|
||||
// Save trade to database
|
||||
try {
|
||||
await createTrade({
|
||||
positionId: openResult.transactionSignature!,
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
entryPrice,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice,
|
||||
takeProfit1Price: tp1Price,
|
||||
takeProfit2Price: tp2Price,
|
||||
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
||||
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
||||
configSnapshot: config,
|
||||
entryOrderTx: openResult.transactionSignature!,
|
||||
tp1OrderTx: exitOrderSignatures[0],
|
||||
tp2OrderTx: exitOrderSignatures[1],
|
||||
slOrderTx: config.useDualStops ? undefined : exitOrderSignatures[2],
|
||||
softStopOrderTx: config.useDualStops ? exitOrderSignatures[2] : undefined,
|
||||
hardStopOrderTx: config.useDualStops ? exitOrderSignatures[3] : undefined,
|
||||
softStopPrice,
|
||||
hardStopPrice,
|
||||
signalStrength: body.signalStrength,
|
||||
timeframe: body.timeframe,
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
console.log('✅ Trade executed successfully!')
|
||||
|
||||
|
||||
303
app/api/trading/test/route.ts
Normal file
303
app/api/trading/test/route.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Test Trade API Endpoint
|
||||
*
|
||||
* Executes a test trade with current settings (no authentication required from settings page)
|
||||
* POST /api/trading/test
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { initializeDriftService } from '@/lib/drift/client'
|
||||
import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
||||
import { normalizeTradingViewSymbol } from '@/config/trading'
|
||||
import { getMergedConfig } from '@/config/trading'
|
||||
import { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||
import { createTrade } from '@/lib/database/trades'
|
||||
|
||||
export interface TestTradeRequest {
|
||||
symbol?: string // Default: SOLUSDT
|
||||
direction?: 'long' | 'short' // Default: long
|
||||
}
|
||||
|
||||
export interface TestTradeResponse {
|
||||
success: boolean
|
||||
positionId?: string
|
||||
symbol?: string
|
||||
direction?: 'long' | 'short'
|
||||
entryPrice?: number
|
||||
positionSize?: number
|
||||
stopLoss?: number
|
||||
takeProfit1?: number
|
||||
takeProfit2?: number
|
||||
softStopPrice?: number
|
||||
hardStopPrice?: number
|
||||
useDualStops?: boolean
|
||||
timestamp?: string
|
||||
error?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest): Promise<NextResponse<TestTradeResponse>> {
|
||||
try {
|
||||
// Parse request body
|
||||
const body: TestTradeRequest = await request.json().catch(() => ({}))
|
||||
|
||||
const symbol = body.symbol || 'SOLUSDT'
|
||||
const direction = body.direction || 'long'
|
||||
|
||||
console.log('🧪 Test trade request:', { symbol, direction })
|
||||
|
||||
// Normalize symbol
|
||||
const driftSymbol = normalizeTradingViewSymbol(symbol)
|
||||
console.log(`📊 Normalized symbol: ${symbol} → ${driftSymbol}`)
|
||||
|
||||
// Get trading configuration
|
||||
const config = getMergedConfig()
|
||||
|
||||
// Initialize Drift service if not already initialized
|
||||
const driftService = await initializeDriftService()
|
||||
|
||||
// Check account health before trading
|
||||
const health = await driftService.getAccountHealth()
|
||||
console.log('💊 Account health:', health)
|
||||
|
||||
if (health.freeCollateral <= 0) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Insufficient collateral',
|
||||
message: `Free collateral: $${health.freeCollateral.toFixed(2)}`,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate position size with leverage
|
||||
const positionSizeUSD = config.positionSize * config.leverage
|
||||
|
||||
console.log(`💰 Opening ${direction} position:`)
|
||||
console.log(` Symbol: ${driftSymbol}`)
|
||||
console.log(` Base size: $${config.positionSize}`)
|
||||
console.log(` Leverage: ${config.leverage}x`)
|
||||
console.log(` Total position: $${positionSizeUSD}`)
|
||||
|
||||
// Open position
|
||||
const openResult = await openPosition({
|
||||
symbol: driftSymbol,
|
||||
direction: direction,
|
||||
sizeUSD: positionSizeUSD,
|
||||
slippageTolerance: config.slippageTolerance,
|
||||
})
|
||||
|
||||
if (!openResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Position open failed',
|
||||
message: openResult.error,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate stop loss and take profit prices
|
||||
const entryPrice = openResult.fillPrice!
|
||||
|
||||
const stopLossPrice = calculatePrice(
|
||||
entryPrice,
|
||||
config.stopLossPercent,
|
||||
direction
|
||||
)
|
||||
|
||||
// Calculate dual stop prices if enabled
|
||||
let softStopPrice: number | undefined
|
||||
let hardStopPrice: number | undefined
|
||||
|
||||
if (config.useDualStops) {
|
||||
softStopPrice = calculatePrice(
|
||||
entryPrice,
|
||||
config.softStopPercent,
|
||||
direction
|
||||
)
|
||||
hardStopPrice = calculatePrice(
|
||||
entryPrice,
|
||||
config.hardStopPercent,
|
||||
direction
|
||||
)
|
||||
console.log('🛡️🛡️ Dual stop system enabled:')
|
||||
console.log(` Soft stop: $${softStopPrice.toFixed(4)} (${config.softStopPercent}%)`)
|
||||
console.log(` Hard stop: $${hardStopPrice.toFixed(4)} (${config.hardStopPercent}%)`)
|
||||
}
|
||||
|
||||
const tp1Price = calculatePrice(
|
||||
entryPrice,
|
||||
config.takeProfit1Percent,
|
||||
direction
|
||||
)
|
||||
|
||||
const tp2Price = calculatePrice(
|
||||
entryPrice,
|
||||
config.takeProfit2Percent,
|
||||
direction
|
||||
)
|
||||
|
||||
console.log('📊 Trade targets:')
|
||||
console.log(` Entry: $${entryPrice.toFixed(4)}`)
|
||||
console.log(` SL: $${stopLossPrice.toFixed(4)} (${config.stopLossPercent}%)`)
|
||||
console.log(` TP1: $${tp1Price.toFixed(4)} (${config.takeProfit1Percent}%)`)
|
||||
console.log(` TP2: $${tp2Price.toFixed(4)} (${config.takeProfit2Percent}%)`)
|
||||
|
||||
// Calculate emergency stop
|
||||
const emergencyStopPrice = calculatePrice(
|
||||
entryPrice,
|
||||
config.emergencyStopPercent,
|
||||
direction
|
||||
)
|
||||
|
||||
// Create active trade object
|
||||
const activeTrade: ActiveTrade = {
|
||||
id: `test-trade-${Date.now()}`,
|
||||
positionId: openResult.transactionSignature!,
|
||||
symbol: driftSymbol,
|
||||
direction: direction,
|
||||
entryPrice,
|
||||
entryTime: Date.now(),
|
||||
positionSize: positionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
emergencyStopPrice,
|
||||
currentSize: positionSizeUSD,
|
||||
tp1Hit: false,
|
||||
slMovedToBreakeven: false,
|
||||
slMovedToProfit: false,
|
||||
realizedPnL: 0,
|
||||
unrealizedPnL: 0,
|
||||
peakPnL: 0,
|
||||
priceCheckCount: 0,
|
||||
lastPrice: entryPrice,
|
||||
lastUpdateTime: Date.now(),
|
||||
}
|
||||
|
||||
// Add to position manager for monitoring
|
||||
const positionManager = getPositionManager()
|
||||
await positionManager.addTrade(activeTrade)
|
||||
|
||||
console.log('✅ Trade added to position manager for monitoring')
|
||||
|
||||
// Create response object
|
||||
const response: TestTradeResponse = {
|
||||
success: true,
|
||||
positionId: openResult.transactionSignature,
|
||||
symbol: driftSymbol,
|
||||
direction: direction,
|
||||
entryPrice: entryPrice,
|
||||
positionSize: positionSizeUSD,
|
||||
stopLoss: stopLossPrice,
|
||||
takeProfit1: tp1Price,
|
||||
takeProfit2: tp2Price,
|
||||
softStopPrice: softStopPrice,
|
||||
hardStopPrice: hardStopPrice,
|
||||
useDualStops: config.useDualStops,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
// Place on-chain TP/SL orders so they appear in Drift UI
|
||||
let exitOrderSignatures: string[] = []
|
||||
try {
|
||||
const exitRes = await placeExitOrders({
|
||||
symbol: driftSymbol,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
tp1Price,
|
||||
tp2Price,
|
||||
stopLossPrice,
|
||||
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
||||
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
||||
direction: direction,
|
||||
// Dual stop parameters
|
||||
useDualStops: config.useDualStops,
|
||||
softStopPrice: softStopPrice,
|
||||
softStopBuffer: config.softStopBuffer,
|
||||
hardStopPrice: hardStopPrice,
|
||||
})
|
||||
|
||||
if (!exitRes.success) {
|
||||
console.error('❌ Failed to place on-chain exit orders:', exitRes.error)
|
||||
} else {
|
||||
console.log('📨 Exit orders placed on-chain:', exitRes.signatures)
|
||||
exitOrderSignatures = exitRes.signatures || []
|
||||
}
|
||||
|
||||
// Attach signatures to response when available
|
||||
if (exitRes.signatures && exitRes.signatures.length > 0) {
|
||||
;(response as any).exitOrderSignatures = exitRes.signatures
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Unexpected error placing exit orders:', err)
|
||||
}
|
||||
|
||||
// Save trade to database
|
||||
try {
|
||||
await createTrade({
|
||||
positionId: openResult.transactionSignature!,
|
||||
symbol: driftSymbol,
|
||||
direction: direction,
|
||||
entryPrice,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
leverage: config.leverage,
|
||||
stopLossPrice,
|
||||
takeProfit1Price: tp1Price,
|
||||
takeProfit2Price: tp2Price,
|
||||
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
||||
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
||||
configSnapshot: config,
|
||||
entryOrderTx: openResult.transactionSignature!,
|
||||
tp1OrderTx: exitOrderSignatures[0],
|
||||
tp2OrderTx: exitOrderSignatures[1],
|
||||
slOrderTx: config.useDualStops ? undefined : exitOrderSignatures[2],
|
||||
softStopOrderTx: config.useDualStops ? exitOrderSignatures[2] : undefined,
|
||||
hardStopOrderTx: config.useDualStops ? exitOrderSignatures[3] : undefined,
|
||||
softStopPrice,
|
||||
hardStopPrice,
|
||||
signalStrength: 'test',
|
||||
timeframe: 'manual',
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
console.log('✅ Test trade executed successfully!')
|
||||
|
||||
return NextResponse.json(response)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Test trade execution error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate price based on percentage
|
||||
*/
|
||||
function calculatePrice(
|
||||
entryPrice: number,
|
||||
percent: number,
|
||||
direction: 'long' | 'short'
|
||||
): number {
|
||||
if (direction === 'long') {
|
||||
return entryPrice * (1 + percent / 100)
|
||||
} else {
|
||||
return entryPrice * (1 - percent / 100)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user