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:
mindesbunister
2025-10-26 21:29:27 +01:00
parent 33821eae0c
commit d64f6d84c4
13 changed files with 2616 additions and 78 deletions

View File

@@ -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!')

View 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)
}
}

View File

@@ -32,6 +32,7 @@ export default function SettingsPage() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [restarting, setRestarting] = useState(false)
const [testing, setTesting] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
useEffect(() => {
@@ -90,6 +91,44 @@ export default function SettingsPage() {
setRestarting(false)
}
const testTrade = async () => {
if (!confirm('⚠️ This will execute a REAL trade with current settings. Continue?')) {
return
}
setTesting(true)
setMessage(null)
try {
const response = await fetch('/api/trading/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
symbol: 'SOLUSDT',
direction: 'long',
}),
})
const data = await response.json()
if (data.success) {
const dualStopsMsg = data.useDualStops
? `Dual stops: Soft $${data.softStopPrice?.toFixed(4)} | Hard $${data.hardStopPrice?.toFixed(4)}`
: `SL: $${data.stopLoss?.toFixed(4)}`
setMessage({
type: 'success',
text: `✅ 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}` })
}
} catch (error) {
setMessage({ type: 'error', text: `Test trade failed: ${error instanceof Error ? error.message : 'Unknown error'}` })
}
setTesting(false)
}
const updateSetting = (key: keyof TradingSettings, value: any) => {
if (!settings) return
setSettings({ ...settings, [key]: value })
@@ -342,26 +381,38 @@ export default function SettingsPage() {
</div>
{/* Action Buttons */}
<div className="mt-8 flex gap-4">
<div className="mt-8 space-y-4">
{/* Primary Actions */}
<div className="flex gap-4">
<button
onClick={saveSettings}
disabled={saving}
className="flex-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold py-4 px-6 rounded-lg hover:from-blue-600 hover:to-purple-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? '💾 Saving...' : '💾 Save Settings'}
</button>
<button
onClick={restartBot}
disabled={restarting}
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"
>
{restarting ? '🔄 Restarting...' : '🔄 Restart Bot'}
</button>
<button
onClick={loadSettings}
className="bg-slate-700 text-white font-bold py-4 px-6 rounded-lg hover:bg-slate-600 transition-all"
>
Reset
</button>
</div>
{/* Test Trade Button */}
<button
onClick={saveSettings}
disabled={saving}
className="flex-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-bold py-4 px-6 rounded-lg hover:from-blue-600 hover:to-purple-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
onClick={testTrade}
disabled={testing}
className="w-full bg-gradient-to-r from-orange-500 to-red-500 text-white font-bold py-4 px-6 rounded-lg hover:from-orange-600 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed border-2 border-orange-400"
>
{saving ? '💾 Saving...' : '💾 Save Settings'}
</button>
<button
onClick={restartBot}
disabled={restarting}
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"
>
{restarting ? '🔄 Restarting...' : '🔄 Restart Bot'}
</button>
<button
onClick={loadSettings}
className="bg-slate-700 text-white font-bold py-4 px-6 rounded-lg hover:bg-slate-600 transition-all"
>
Reset
{testing ? '🧪 Executing Test Trade...' : '🧪 Test Trade (REAL - SOL Long)'}
</button>
</div>