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

33
.env
View File

@@ -72,6 +72,25 @@ LEVERAGE=5
# Example: -1.5% on 10x = -15% account loss
STOP_LOSS_PERCENT=-2.0
# ================================
# DUAL STOP SYSTEM (Advanced)
# ================================
# Enable dual stop system to avoid wicks while guaranteeing exit
# When enabled, places TWO stop orders:
# 1. Soft Stop (TRIGGER_LIMIT) - Avoids false breakouts/wicks
# 2. Hard Stop (TRIGGER_MARKET) - Guarantees exit if price keeps falling
USE_DUAL_STOPS=true
# Soft Stop (Primary, Stop-Limit)
# Triggers first, tries to avoid wicks
SOFT_STOP_PERCENT=-1.5
SOFT_STOP_BUFFER=0.4 # Buffer between trigger and limit (0.4% = limit at -1.9%)
# Hard Stop (Backup, Stop-Market)
# Only triggers if soft stop doesn't fill
# Guarantees exit during strong breakdowns
HARD_STOP_PERCENT=-2.5
# Take Profit 1: Close 50% of position at this profit level
# Example: +0.7% on 10x = +7% account gain
TAKE_PROFIT_1_PERCENT=0.5
@@ -186,11 +205,15 @@ EMAIL_PASSWORD=your_16_character_app_password
# PostgreSQL connection string
# Format: postgresql://username:password@host:port/database
#
# Local setup:
# 1. Install PostgreSQL: https://www.postgresql.org/download/
# 2. Create database: createdb trading_bot_v4
# 3. Update connection string below
DATABASE_URL=postgresql://postgres:password@localhost:5432/trading_bot_v4
# IMPORTANT: Use different URLs for different environments:
# - Docker container (runtime): trading-bot-postgres (container name)
# - Local development (Prisma CLI): localhost:5432
#
# The URL below is for Docker runtime. For Prisma migrations from host:
# DATABASE_URL="postgresql://postgres:postgres@localhost:5432/trading_bot_v4" npx prisma migrate dev
#
# PostgreSQL Database (for trade history and analytics)
DATABASE_URL=postgresql://postgres:postgres@trading-bot-postgres:5432/trading_bot_v4
# Cloud PostgreSQL providers:
# - Supabase: https://supabase.com (free tier available)

View File

@@ -35,6 +35,9 @@ COPY --from=deps /app/node_modules ./node_modules
# Copy source code
COPY . .
# Generate Prisma client before building
RUN npx prisma generate
# Build Next.js application
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_ENV production

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,7 +381,9 @@ 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}
@@ -365,6 +406,16 @@ export default function SettingsPage() {
</button>
</div>
{/* Test Trade Button */}
<button
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"
>
{testing ? '🧪 Executing Test Trade...' : '🧪 Test Trade (REAL - SOL Long)'}
</button>
</div>
<div className="mt-4 text-center text-slate-400 text-sm">
💡 Save settings first, then click Restart Bot to apply changes
</div>

View File

@@ -15,6 +15,12 @@ export interface TradingConfig {
takeProfit2Percent: number // Positive number (e.g., 1.5)
emergencyStopPercent: number // Hard stop (e.g., -2.0)
// Dual Stop System (Advanced)
useDualStops: boolean // Enable dual stop system
softStopPercent: number // Soft stop trigger (e.g., -1.5)
softStopBuffer: number // Buffer for soft stop limit (e.g., 0.4)
hardStopPercent: number // Hard stop trigger (e.g., -2.5)
// Dynamic adjustments
breakEvenTriggerPercent: number // When to move SL to breakeven
profitLockTriggerPercent: number // When to lock in profit
@@ -57,6 +63,12 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = {
takeProfit2Percent: 1.5, // +1.5% price = +15% account gain (closes 50%)
emergencyStopPercent: -2.0, // -2% hard stop = -20% account loss
// Dual Stop System
useDualStops: false, // Disabled by default
softStopPercent: -1.5, // Soft stop (TRIGGER_LIMIT)
softStopBuffer: 0.4, // 0.4% buffer (limit at -1.9%)
hardStopPercent: -2.5, // Hard stop (TRIGGER_MARKET)
// Dynamic adjustments
breakEvenTriggerPercent: 0.4, // Move SL to breakeven at +0.4%
profitLockTriggerPercent: 1.0, // Lock profit at +1.0%
@@ -164,6 +176,18 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
stopLossPercent: process.env.STOP_LOSS_PERCENT
? parseFloat(process.env.STOP_LOSS_PERCENT)
: undefined,
useDualStops: process.env.USE_DUAL_STOPS
? process.env.USE_DUAL_STOPS === 'true'
: undefined,
softStopPercent: process.env.SOFT_STOP_PERCENT
? parseFloat(process.env.SOFT_STOP_PERCENT)
: undefined,
softStopBuffer: process.env.SOFT_STOP_BUFFER
? parseFloat(process.env.SOFT_STOP_BUFFER)
: undefined,
hardStopPercent: process.env.HARD_STOP_PERCENT
? parseFloat(process.env.HARD_STOP_PERCENT)
: undefined,
takeProfit1Percent: process.env.TAKE_PROFIT_1_PERCENT
? parseFloat(process.env.TAKE_PROFIT_1_PERCENT)
: undefined,

245
lib/database/trades.ts Normal file
View File

@@ -0,0 +1,245 @@
/**
* Database Service for Trade Tracking and Analytics
*/
import { PrismaClient } from '@prisma/client'
// Singleton Prisma client
let prisma: PrismaClient | null = null
export function getPrismaClient(): PrismaClient {
if (!prisma) {
prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
console.log('✅ Prisma client initialized')
}
return prisma
}
export interface CreateTradeParams {
positionId: string
symbol: string
direction: 'long' | 'short'
entryPrice: number
entrySlippage?: number
positionSizeUSD: number
leverage: number
stopLossPrice: number
softStopPrice?: number
hardStopPrice?: number
takeProfit1Price: number
takeProfit2Price: number
tp1SizePercent: number
tp2SizePercent: number
entryOrderTx: string
tp1OrderTx?: string
tp2OrderTx?: string
slOrderTx?: string
softStopOrderTx?: string
hardStopOrderTx?: string
configSnapshot: any
signalSource?: string
signalStrength?: string
timeframe?: string
}
export interface UpdateTradeExitParams {
positionId: string
exitPrice: number
exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency'
realizedPnL: number
exitOrderTx: string
holdTimeSeconds: number
maxDrawdown?: number
maxGain?: number
}
/**
* Create a new trade record
*/
export async function createTrade(params: CreateTradeParams) {
const prisma = getPrismaClient()
try {
const trade = await prisma.trade.create({
data: {
positionId: params.positionId,
symbol: params.symbol,
direction: params.direction,
entryPrice: params.entryPrice,
entryTime: new Date(),
entrySlippage: params.entrySlippage,
positionSizeUSD: params.positionSizeUSD,
leverage: params.leverage,
stopLossPrice: params.stopLossPrice,
softStopPrice: params.softStopPrice,
hardStopPrice: params.hardStopPrice,
takeProfit1Price: params.takeProfit1Price,
takeProfit2Price: params.takeProfit2Price,
tp1SizePercent: params.tp1SizePercent,
tp2SizePercent: params.tp2SizePercent,
entryOrderTx: params.entryOrderTx,
tp1OrderTx: params.tp1OrderTx,
tp2OrderTx: params.tp2OrderTx,
slOrderTx: params.slOrderTx,
softStopOrderTx: params.softStopOrderTx,
hardStopOrderTx: params.hardStopOrderTx,
configSnapshot: params.configSnapshot,
signalSource: params.signalSource,
signalStrength: params.signalStrength,
timeframe: params.timeframe,
status: 'open',
},
})
console.log(`📊 Trade record created: ${trade.id}`)
return trade
} catch (error) {
console.error('❌ Failed to create trade record:', error)
throw error
}
}
/**
* Update trade when position exits
*/
export async function updateTradeExit(params: UpdateTradeExitParams) {
const prisma = getPrismaClient()
try {
// First fetch the trade to get positionSizeUSD
const existingTrade = await prisma.trade.findUnique({
where: { positionId: params.positionId },
select: { positionSizeUSD: true },
})
if (!existingTrade) {
throw new Error(`Trade not found: ${params.positionId}`)
}
const trade = await prisma.trade.update({
where: { positionId: params.positionId },
data: {
exitPrice: params.exitPrice,
exitTime: new Date(),
exitReason: params.exitReason,
realizedPnL: params.realizedPnL,
realizedPnLPercent: (params.realizedPnL / existingTrade.positionSizeUSD) * 100,
exitOrderTx: params.exitOrderTx,
holdTimeSeconds: params.holdTimeSeconds,
maxDrawdown: params.maxDrawdown,
maxGain: params.maxGain,
status: 'closed',
},
})
console.log(`📊 Trade closed: ${trade.id} | P&L: $${params.realizedPnL.toFixed(2)}`)
return trade
} catch (error) {
console.error('❌ Failed to update trade exit:', error)
throw error
}
}
/**
* Add price update for a trade (for tracking max gain/drawdown)
*/
export async function addPriceUpdate(
tradeId: string,
price: number,
pnl: number,
pnlPercent: number
) {
const prisma = getPrismaClient()
try {
await prisma.priceUpdate.create({
data: {
tradeId,
price,
pnl,
pnlPercent,
},
})
} catch (error) {
console.error('❌ Failed to add price update:', error)
// Don't throw - price updates are non-critical
}
}
/**
* Log system event
*/
export async function logSystemEvent(
eventType: string,
message: string,
details?: any
) {
const prisma = getPrismaClient()
try {
await prisma.systemEvent.create({
data: {
eventType,
message,
details: details ? JSON.parse(JSON.stringify(details)) : null,
},
})
} catch (error) {
console.error('❌ Failed to log system event:', error)
}
}
/**
* Get trade statistics
*/
export async function getTradeStats(days: number = 30) {
const prisma = getPrismaClient()
const since = new Date()
since.setDate(since.getDate() - days)
const trades = await prisma.trade.findMany({
where: {
createdAt: { gte: since },
status: 'closed',
},
})
const winning = trades.filter((t) => (t.realizedPnL ?? 0) > 0)
const losing = trades.filter((t) => (t.realizedPnL ?? 0) < 0)
const totalPnL = trades.reduce((sum, t) => sum + (t.realizedPnL ?? 0), 0)
const winRate = trades.length > 0 ? (winning.length / trades.length) * 100 : 0
const avgWin = winning.length > 0
? winning.reduce((sum, t) => sum + (t.realizedPnL ?? 0), 0) / winning.length
: 0
const avgLoss = losing.length > 0
? losing.reduce((sum, t) => sum + (t.realizedPnL ?? 0), 0) / losing.length
: 0
return {
totalTrades: trades.length,
winningTrades: winning.length,
losingTrades: losing.length,
winRate: winRate.toFixed(2),
totalPnL: totalPnL.toFixed(2),
avgWin: avgWin.toFixed(2),
avgLoss: avgLoss.toFixed(2),
profitFactor: avgLoss !== 0 ? (avgWin / Math.abs(avgLoss)).toFixed(2) : 'N/A',
}
}
/**
* Disconnect Prisma client (for graceful shutdown)
*/
export async function disconnectPrisma() {
if (prisma) {
await prisma.$disconnect()
prisma = null
console.log('✅ Prisma client disconnected')
}
}

View File

@@ -63,6 +63,11 @@ export interface PlaceExitOrdersOptions {
direction: 'long' | 'short'
useStopLimit?: boolean // Optional: use TRIGGER_LIMIT instead of TRIGGER_MARKET for SL
stopLimitBuffer?: number // Optional: buffer percentage for stop-limit (default 0.5%)
// Dual Stop System
useDualStops?: boolean // Enable dual stop system
softStopPrice?: number // Soft stop trigger price (TRIGGER_LIMIT)
softStopBuffer?: number // Buffer for soft stop limit price
hardStopPrice?: number // Hard stop trigger price (TRIGGER_MARKET)
}
/**
@@ -273,21 +278,83 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
}
}
// Place Stop-Loss order
// Default: TRIGGER_MARKET (guaranteed execution, RECOMMENDED for most traders)
// Optional: TRIGGER_LIMIT with buffer (only for very liquid markets to avoid extreme wicks)
// Place Stop-Loss order(s)
// Supports three modes:
// 1. Dual Stop System (soft stop-limit + hard stop-market)
// 2. Single TRIGGER_LIMIT (for liquid markets)
// 3. Single TRIGGER_MARKET (default, guaranteed execution)
const slUSD = options.positionSizeUSD
const slBaseAmount = usdToBase(slUSD, options.stopLossPrice)
if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
const useDualStops = options.useDualStops ?? false
if (useDualStops && options.softStopPrice && options.hardStopPrice) {
// ============== DUAL STOP SYSTEM ==============
console.log('🛡️🛡️ Placing DUAL STOP SYSTEM...')
// 1. Soft Stop (TRIGGER_LIMIT) - Avoids wicks
const softStopBuffer = options.softStopBuffer ?? 0.4
const softStopMultiplier = options.direction === 'long'
? (1 - softStopBuffer / 100)
: (1 + softStopBuffer / 100)
const softStopParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.softStopPrice * 1e6)),
price: new BN(Math.floor(options.softStopPrice * softStopMultiplier * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
console.log(` 1⃣ Soft Stop (TRIGGER_LIMIT):`)
console.log(` Trigger: $${options.softStopPrice.toFixed(4)}`)
console.log(` Limit: $${(options.softStopPrice * softStopMultiplier).toFixed(4)}`)
console.log(` Purpose: Avoid false breakouts/wicks`)
const softStopSig = await (driftClient as any).placePerpOrder(softStopParams)
console.log(` ✅ Soft stop placed: ${softStopSig}`)
signatures.push(softStopSig)
// 2. Hard Stop (TRIGGER_MARKET) - Guarantees exit
const hardStopParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
direction: orderDirection,
baseAssetAmount: new BN(slBaseAmount),
triggerPrice: new BN(Math.floor(options.hardStopPrice * 1e6)),
triggerCondition: options.direction === 'long'
? OrderTriggerCondition.BELOW
: OrderTriggerCondition.ABOVE,
reduceOnly: true,
}
console.log(` 2⃣ Hard Stop (TRIGGER_MARKET):`)
console.log(` Trigger: $${options.hardStopPrice.toFixed(4)}`)
console.log(` Purpose: Guaranteed exit if soft stop doesn't fill`)
const hardStopSig = await (driftClient as any).placePerpOrder(hardStopParams)
console.log(` ✅ Hard stop placed: ${hardStopSig}`)
signatures.push(hardStopSig)
console.log(`🎯 Dual stop system active: Soft @ $${options.softStopPrice.toFixed(2)} | Hard @ $${options.hardStopPrice.toFixed(2)}`)
} else {
// ============== SINGLE STOP SYSTEM ==============
const useStopLimit = options.useStopLimit ?? false
const stopLimitBuffer = options.stopLimitBuffer ?? 0.5 // default 0.5% buffer
const stopLimitBuffer = options.stopLimitBuffer ?? 0.5
if (useStopLimit) {
// TRIGGER_LIMIT: Protects against extreme wicks but may not fill during fast moves
// TRIGGER_LIMIT: For liquid markets
const limitPriceMultiplier = options.direction === 'long'
? (1 - stopLimitBuffer / 100) // Long: limit below trigger
: (1 + stopLimitBuffer / 100) // Short: limit above trigger
? (1 - stopLimitBuffer / 100)
: (1 + stopLimitBuffer / 100)
const orderParams: any = {
orderType: OrderType.TRIGGER_LIMIT,
@@ -311,7 +378,7 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
console.log('✅ SL trigger-limit order placed:', sig)
signatures.push(sig)
} else {
// TRIGGER_MARKET: Guaranteed execution (RECOMMENDED)
// TRIGGER_MARKET: Default, guaranteed execution
const orderParams: any = {
orderType: OrderType.TRIGGER_MARKET,
marketIndex: marketConfig.driftMarketIndex,
@@ -332,6 +399,7 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
console.log('✅ SL trigger-market order placed:', sig)
signatures.push(sig)
}
}
} else {
console.log('⚠️ SL size below market min, skipping on-chain SL')
}

1543
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@drift-labs/sdk": "^2.75.0",
"@prisma/client": "^6.18.0",
"@pythnetwork/hermes-client": "^1.0.0",
"@pythnetwork/price-service-client": "^1.3.0",
"@solana/web3.js": "^1.91.1",
@@ -18,6 +19,7 @@
"bs58": "^5.0.0",
"next": "^15.0.0",
"postcss": "^8.5.6",
"prisma": "^6.18.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"tailwindcss": "^3.4.1"

View File

@@ -0,0 +1,123 @@
-- CreateTable
CREATE TABLE "Trade" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"positionId" TEXT NOT NULL,
"symbol" TEXT NOT NULL,
"direction" TEXT NOT NULL,
"entryPrice" DOUBLE PRECISION NOT NULL,
"entryTime" TIMESTAMP(3) NOT NULL,
"entrySlippage" DOUBLE PRECISION,
"positionSizeUSD" DOUBLE PRECISION NOT NULL,
"leverage" DOUBLE PRECISION NOT NULL,
"stopLossPrice" DOUBLE PRECISION NOT NULL,
"softStopPrice" DOUBLE PRECISION,
"hardStopPrice" DOUBLE PRECISION,
"takeProfit1Price" DOUBLE PRECISION NOT NULL,
"takeProfit2Price" DOUBLE PRECISION NOT NULL,
"tp1SizePercent" DOUBLE PRECISION NOT NULL,
"tp2SizePercent" DOUBLE PRECISION NOT NULL,
"exitPrice" DOUBLE PRECISION,
"exitTime" TIMESTAMP(3),
"exitReason" TEXT,
"realizedPnL" DOUBLE PRECISION,
"realizedPnLPercent" DOUBLE PRECISION,
"holdTimeSeconds" INTEGER,
"maxDrawdown" DOUBLE PRECISION,
"maxGain" DOUBLE PRECISION,
"entryOrderTx" TEXT NOT NULL,
"tp1OrderTx" TEXT,
"tp2OrderTx" TEXT,
"slOrderTx" TEXT,
"softStopOrderTx" TEXT,
"hardStopOrderTx" TEXT,
"exitOrderTx" TEXT,
"configSnapshot" JSONB NOT NULL,
"signalSource" TEXT,
"signalStrength" TEXT,
"timeframe" TEXT,
"status" TEXT NOT NULL DEFAULT 'open',
CONSTRAINT "Trade_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PriceUpdate" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"tradeId" TEXT NOT NULL,
"price" DOUBLE PRECISION NOT NULL,
"pnl" DOUBLE PRECISION NOT NULL,
"pnlPercent" DOUBLE PRECISION NOT NULL,
CONSTRAINT "PriceUpdate_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SystemEvent" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"eventType" TEXT NOT NULL,
"message" TEXT NOT NULL,
"details" JSONB,
CONSTRAINT "SystemEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DailyStats" (
"id" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL,
"tradesCount" INTEGER NOT NULL,
"winningTrades" INTEGER NOT NULL,
"losingTrades" INTEGER NOT NULL,
"totalPnL" DOUBLE PRECISION NOT NULL,
"totalPnLPercent" DOUBLE PRECISION NOT NULL,
"winRate" DOUBLE PRECISION NOT NULL,
"avgWin" DOUBLE PRECISION NOT NULL,
"avgLoss" DOUBLE PRECISION NOT NULL,
"profitFactor" DOUBLE PRECISION NOT NULL,
"maxDrawdown" DOUBLE PRECISION NOT NULL,
"sharpeRatio" DOUBLE PRECISION,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DailyStats_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Trade_positionId_key" ON "Trade"("positionId");
-- CreateIndex
CREATE INDEX "Trade_symbol_idx" ON "Trade"("symbol");
-- CreateIndex
CREATE INDEX "Trade_createdAt_idx" ON "Trade"("createdAt");
-- CreateIndex
CREATE INDEX "Trade_status_idx" ON "Trade"("status");
-- CreateIndex
CREATE INDEX "Trade_exitReason_idx" ON "Trade"("exitReason");
-- CreateIndex
CREATE INDEX "PriceUpdate_tradeId_idx" ON "PriceUpdate"("tradeId");
-- CreateIndex
CREATE INDEX "PriceUpdate_createdAt_idx" ON "PriceUpdate"("createdAt");
-- CreateIndex
CREATE INDEX "SystemEvent_eventType_idx" ON "SystemEvent"("eventType");
-- CreateIndex
CREATE INDEX "SystemEvent_createdAt_idx" ON "SystemEvent"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "DailyStats_date_key" ON "DailyStats"("date");
-- CreateIndex
CREATE INDEX "DailyStats_date_idx" ON "DailyStats"("date");
-- AddForeignKey
ALTER TABLE "PriceUpdate" ADD CONSTRAINT "PriceUpdate_tradeId_fkey" FOREIGN KEY ("tradeId") REFERENCES "Trade"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

131
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,131 @@
// Prisma Schema for Trading Bot v4
// Database: PostgreSQL
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Trade records for analysis and performance tracking
model Trade {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Trade identification
positionId String @unique // Transaction signature from entry order
symbol String // e.g., "SOL-PERP"
direction String // "long" or "short"
// Entry details
entryPrice Float
entryTime DateTime
entrySlippage Float?
positionSizeUSD Float
leverage Float
// Exit targets (planned)
stopLossPrice Float
softStopPrice Float? // Dual stop: soft stop-limit trigger
hardStopPrice Float? // Dual stop: hard stop-market trigger
takeProfit1Price Float
takeProfit2Price Float
tp1SizePercent Float
tp2SizePercent Float
// Exit details (actual)
exitPrice Float?
exitTime DateTime?
exitReason String? // "TP1", "TP2", "SL", "SOFT_SL", "HARD_SL", "manual", "emergency"
// Performance metrics
realizedPnL Float?
realizedPnLPercent Float?
holdTimeSeconds Int?
maxDrawdown Float? // Peak to valley during trade
maxGain Float? // Peak gain reached
// Order signatures
entryOrderTx String
tp1OrderTx String?
tp2OrderTx String?
slOrderTx String?
softStopOrderTx String? // Dual stop: soft stop tx
hardStopOrderTx String? // Dual stop: hard stop tx
exitOrderTx String?
// Configuration snapshot
configSnapshot Json // Store settings used for this trade
// Signal data
signalSource String? // "tradingview", "manual", etc.
signalStrength String? // "strong", "moderate", "weak"
timeframe String? // "5", "15", "60"
// Status
status String @default("open") // "open", "closed", "failed"
// Relations
priceUpdates PriceUpdate[]
@@index([symbol])
@@index([createdAt])
@@index([status])
@@index([exitReason])
}
// Real-time price updates during trade (for analysis)
model PriceUpdate {
id String @id @default(cuid())
createdAt DateTime @default(now())
tradeId String
trade Trade @relation(fields: [tradeId], references: [id], onDelete: Cascade)
price Float
pnl Float
pnlPercent Float
@@index([tradeId])
@@index([createdAt])
}
// System events and errors
model SystemEvent {
id String @id @default(cuid())
createdAt DateTime @default(now())
eventType String // "error", "warning", "info", "trade_executed", etc.
message String
details Json?
@@index([eventType])
@@index([createdAt])
}
// Performance analytics (daily aggregates)
model DailyStats {
id String @id @default(cuid())
date DateTime @unique
tradesCount Int
winningTrades Int
losingTrades Int
totalPnL Float
totalPnLPercent Float
winRate Float
avgWin Float
avgLoss Float
profitFactor Float
maxDrawdown Float
sharpeRatio Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([date])
}