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:
33
.env
33
.env
@@ -72,6 +72,25 @@ LEVERAGE=5
|
|||||||
# Example: -1.5% on 10x = -15% account loss
|
# Example: -1.5% on 10x = -15% account loss
|
||||||
STOP_LOSS_PERCENT=-2.0
|
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
|
# Take Profit 1: Close 50% of position at this profit level
|
||||||
# Example: +0.7% on 10x = +7% account gain
|
# Example: +0.7% on 10x = +7% account gain
|
||||||
TAKE_PROFIT_1_PERCENT=0.5
|
TAKE_PROFIT_1_PERCENT=0.5
|
||||||
@@ -186,11 +205,15 @@ EMAIL_PASSWORD=your_16_character_app_password
|
|||||||
# PostgreSQL connection string
|
# PostgreSQL connection string
|
||||||
# Format: postgresql://username:password@host:port/database
|
# Format: postgresql://username:password@host:port/database
|
||||||
#
|
#
|
||||||
# Local setup:
|
# IMPORTANT: Use different URLs for different environments:
|
||||||
# 1. Install PostgreSQL: https://www.postgresql.org/download/
|
# - Docker container (runtime): trading-bot-postgres (container name)
|
||||||
# 2. Create database: createdb trading_bot_v4
|
# - Local development (Prisma CLI): localhost:5432
|
||||||
# 3. Update connection string below
|
#
|
||||||
DATABASE_URL=postgresql://postgres:password@localhost:5432/trading_bot_v4
|
# 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:
|
# Cloud PostgreSQL providers:
|
||||||
# - Supabase: https://supabase.com (free tier available)
|
# - Supabase: https://supabase.com (free tier available)
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ COPY --from=deps /app/node_modules ./node_modules
|
|||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Generate Prisma client before building
|
||||||
|
RUN npx prisma generate
|
||||||
|
|
||||||
# Build Next.js application
|
# Build Next.js application
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { openPosition, placeExitOrders } from '@/lib/drift/orders'
|
|||||||
import { normalizeTradingViewSymbol } from '@/config/trading'
|
import { normalizeTradingViewSymbol } from '@/config/trading'
|
||||||
import { getMergedConfig } from '@/config/trading'
|
import { getMergedConfig } from '@/config/trading'
|
||||||
import { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
import { getPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||||
|
import { createTrade } from '@/lib/database/trades'
|
||||||
|
|
||||||
export interface ExecuteTradeRequest {
|
export interface ExecuteTradeRequest {
|
||||||
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
||||||
@@ -135,6 +136,26 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
body.direction
|
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(
|
const tp1Price = calculatePrice(
|
||||||
entryPrice,
|
entryPrice,
|
||||||
config.takeProfit1Percent,
|
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)
|
// Place on-chain TP/SL orders so they appear in Drift UI (reduce-only LIMIT orders)
|
||||||
|
let exitOrderSignatures: string[] = []
|
||||||
try {
|
try {
|
||||||
const exitRes = await placeExitOrders({
|
const exitRes = await placeExitOrders({
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
@@ -221,12 +243,18 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
tp1SizePercent: config.takeProfit1SizePercent || 50,
|
||||||
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
tp2SizePercent: config.takeProfit2SizePercent || 100,
|
||||||
direction: body.direction,
|
direction: body.direction,
|
||||||
|
// Dual stop parameters
|
||||||
|
useDualStops: config.useDualStops,
|
||||||
|
softStopPrice: softStopPrice,
|
||||||
|
softStopBuffer: config.softStopBuffer,
|
||||||
|
hardStopPrice: hardStopPrice,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!exitRes.success) {
|
if (!exitRes.success) {
|
||||||
console.error('❌ Failed to place on-chain exit orders:', exitRes.error)
|
console.error('❌ Failed to place on-chain exit orders:', exitRes.error)
|
||||||
} else {
|
} else {
|
||||||
console.log('📨 Exit orders placed on-chain:', exitRes.signatures)
|
console.log('📨 Exit orders placed on-chain:', exitRes.signatures)
|
||||||
|
exitOrderSignatures = exitRes.signatures || []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach signatures to response when available
|
// 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)
|
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!')
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ export default function SettingsPage() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [restarting, setRestarting] = useState(false)
|
const [restarting, setRestarting] = useState(false)
|
||||||
|
const [testing, setTesting] = useState(false)
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
|
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -90,6 +91,44 @@ export default function SettingsPage() {
|
|||||||
setRestarting(false)
|
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) => {
|
const updateSetting = (key: keyof TradingSettings, value: any) => {
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
setSettings({ ...settings, [key]: value })
|
setSettings({ ...settings, [key]: value })
|
||||||
@@ -342,26 +381,38 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* 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
|
<button
|
||||||
onClick={saveSettings}
|
onClick={testTrade}
|
||||||
disabled={saving}
|
disabled={testing}
|
||||||
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"
|
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'}
|
{testing ? '🧪 Executing Test Trade...' : '🧪 Test Trade (REAL - SOL Long)'}
|
||||||
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ export interface TradingConfig {
|
|||||||
takeProfit2Percent: number // Positive number (e.g., 1.5)
|
takeProfit2Percent: number // Positive number (e.g., 1.5)
|
||||||
emergencyStopPercent: number // Hard stop (e.g., -2.0)
|
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
|
// Dynamic adjustments
|
||||||
breakEvenTriggerPercent: number // When to move SL to breakeven
|
breakEvenTriggerPercent: number // When to move SL to breakeven
|
||||||
profitLockTriggerPercent: number // When to lock in profit
|
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%)
|
takeProfit2Percent: 1.5, // +1.5% price = +15% account gain (closes 50%)
|
||||||
emergencyStopPercent: -2.0, // -2% hard stop = -20% account loss
|
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
|
// Dynamic adjustments
|
||||||
breakEvenTriggerPercent: 0.4, // Move SL to breakeven at +0.4%
|
breakEvenTriggerPercent: 0.4, // Move SL to breakeven at +0.4%
|
||||||
profitLockTriggerPercent: 1.0, // Lock profit at +1.0%
|
profitLockTriggerPercent: 1.0, // Lock profit at +1.0%
|
||||||
@@ -164,6 +176,18 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
|
|||||||
stopLossPercent: process.env.STOP_LOSS_PERCENT
|
stopLossPercent: process.env.STOP_LOSS_PERCENT
|
||||||
? parseFloat(process.env.STOP_LOSS_PERCENT)
|
? parseFloat(process.env.STOP_LOSS_PERCENT)
|
||||||
: undefined,
|
: 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
|
takeProfit1Percent: process.env.TAKE_PROFIT_1_PERCENT
|
||||||
? parseFloat(process.env.TAKE_PROFIT_1_PERCENT)
|
? parseFloat(process.env.TAKE_PROFIT_1_PERCENT)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
245
lib/database/trades.ts
Normal file
245
lib/database/trades.ts
Normal 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,11 @@ export interface PlaceExitOrdersOptions {
|
|||||||
direction: 'long' | 'short'
|
direction: 'long' | 'short'
|
||||||
useStopLimit?: boolean // Optional: use TRIGGER_LIMIT instead of TRIGGER_MARKET for SL
|
useStopLimit?: boolean // Optional: use TRIGGER_LIMIT instead of TRIGGER_MARKET for SL
|
||||||
stopLimitBuffer?: number // Optional: buffer percentage for stop-limit (default 0.5%)
|
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,64 +278,127 @@ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place Stop-Loss order
|
// Place Stop-Loss order(s)
|
||||||
// Default: TRIGGER_MARKET (guaranteed execution, RECOMMENDED for most traders)
|
// Supports three modes:
|
||||||
// Optional: TRIGGER_LIMIT with buffer (only for very liquid markets to avoid extreme wicks)
|
// 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 slUSD = options.positionSizeUSD
|
||||||
const slBaseAmount = usdToBase(slUSD, options.stopLossPrice)
|
const slBaseAmount = usdToBase(slUSD, options.stopLossPrice)
|
||||||
|
|
||||||
if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
|
if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) {
|
||||||
const useStopLimit = options.useStopLimit ?? false
|
const useDualStops = options.useDualStops ?? false
|
||||||
const stopLimitBuffer = options.stopLimitBuffer ?? 0.5 // default 0.5% buffer
|
|
||||||
|
|
||||||
if (useStopLimit) {
|
if (useDualStops && options.softStopPrice && options.hardStopPrice) {
|
||||||
// TRIGGER_LIMIT: Protects against extreme wicks but may not fill during fast moves
|
// ============== DUAL STOP SYSTEM ==============
|
||||||
const limitPriceMultiplier = options.direction === 'long'
|
console.log('🛡️🛡️ Placing DUAL STOP SYSTEM...')
|
||||||
? (1 - stopLimitBuffer / 100) // Long: limit below trigger
|
|
||||||
: (1 + stopLimitBuffer / 100) // Short: limit above trigger
|
|
||||||
|
|
||||||
const orderParams: any = {
|
// 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,
|
orderType: OrderType.TRIGGER_LIMIT,
|
||||||
marketIndex: marketConfig.driftMarketIndex,
|
marketIndex: marketConfig.driftMarketIndex,
|
||||||
direction: orderDirection,
|
direction: orderDirection,
|
||||||
baseAssetAmount: new BN(slBaseAmount),
|
baseAssetAmount: new BN(slBaseAmount),
|
||||||
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
|
triggerPrice: new BN(Math.floor(options.softStopPrice * 1e6)),
|
||||||
price: new BN(Math.floor(options.stopLossPrice * limitPriceMultiplier * 1e6)),
|
price: new BN(Math.floor(options.softStopPrice * softStopMultiplier * 1e6)),
|
||||||
triggerCondition: options.direction === 'long'
|
triggerCondition: options.direction === 'long'
|
||||||
? OrderTriggerCondition.BELOW
|
? OrderTriggerCondition.BELOW
|
||||||
: OrderTriggerCondition.ABOVE,
|
: OrderTriggerCondition.ABOVE,
|
||||||
reduceOnly: true,
|
reduceOnly: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🛡️ Placing SL as TRIGGER_LIMIT (${stopLimitBuffer}% buffer)...`)
|
console.log(` 1️⃣ Soft Stop (TRIGGER_LIMIT):`)
|
||||||
console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
|
console.log(` Trigger: $${options.softStopPrice.toFixed(4)}`)
|
||||||
console.log(` Limit: $${(options.stopLossPrice * limitPriceMultiplier).toFixed(4)}`)
|
console.log(` Limit: $${(options.softStopPrice * softStopMultiplier).toFixed(4)}`)
|
||||||
console.log(` ⚠️ May not fill during fast moves - use for liquid markets only!`)
|
console.log(` Purpose: Avoid false breakouts/wicks`)
|
||||||
|
|
||||||
const sig = await (driftClient as any).placePerpOrder(orderParams)
|
const softStopSig = await (driftClient as any).placePerpOrder(softStopParams)
|
||||||
console.log('✅ SL trigger-limit order placed:', sig)
|
console.log(` ✅ Soft stop placed: ${softStopSig}`)
|
||||||
signatures.push(sig)
|
signatures.push(softStopSig)
|
||||||
} else {
|
|
||||||
// TRIGGER_MARKET: Guaranteed execution (RECOMMENDED)
|
// 2. Hard Stop (TRIGGER_MARKET) - Guarantees exit
|
||||||
const orderParams: any = {
|
const hardStopParams: any = {
|
||||||
orderType: OrderType.TRIGGER_MARKET,
|
orderType: OrderType.TRIGGER_MARKET,
|
||||||
marketIndex: marketConfig.driftMarketIndex,
|
marketIndex: marketConfig.driftMarketIndex,
|
||||||
direction: orderDirection,
|
direction: orderDirection,
|
||||||
baseAssetAmount: new BN(slBaseAmount),
|
baseAssetAmount: new BN(slBaseAmount),
|
||||||
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
|
triggerPrice: new BN(Math.floor(options.hardStopPrice * 1e6)),
|
||||||
triggerCondition: options.direction === 'long'
|
triggerCondition: options.direction === 'long'
|
||||||
? OrderTriggerCondition.BELOW
|
? OrderTriggerCondition.BELOW
|
||||||
: OrderTriggerCondition.ABOVE,
|
: OrderTriggerCondition.ABOVE,
|
||||||
reduceOnly: true,
|
reduceOnly: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🛡️ Placing SL as TRIGGER_MARKET (guaranteed execution - RECOMMENDED)...`)
|
console.log(` 2️⃣ Hard Stop (TRIGGER_MARKET):`)
|
||||||
console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
|
console.log(` Trigger: $${options.hardStopPrice.toFixed(4)}`)
|
||||||
console.log(` ✅ Will execute at market price when triggered (may slip but WILL fill)`)
|
console.log(` Purpose: Guaranteed exit if soft stop doesn't fill`)
|
||||||
|
|
||||||
const sig = await (driftClient as any).placePerpOrder(orderParams)
|
const hardStopSig = await (driftClient as any).placePerpOrder(hardStopParams)
|
||||||
console.log('✅ SL trigger-market order placed:', sig)
|
console.log(` ✅ Hard stop placed: ${hardStopSig}`)
|
||||||
signatures.push(sig)
|
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
|
||||||
|
|
||||||
|
if (useStopLimit) {
|
||||||
|
// TRIGGER_LIMIT: For liquid markets
|
||||||
|
const limitPriceMultiplier = options.direction === 'long'
|
||||||
|
? (1 - stopLimitBuffer / 100)
|
||||||
|
: (1 + stopLimitBuffer / 100)
|
||||||
|
|
||||||
|
const orderParams: any = {
|
||||||
|
orderType: OrderType.TRIGGER_LIMIT,
|
||||||
|
marketIndex: marketConfig.driftMarketIndex,
|
||||||
|
direction: orderDirection,
|
||||||
|
baseAssetAmount: new BN(slBaseAmount),
|
||||||
|
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
|
||||||
|
price: new BN(Math.floor(options.stopLossPrice * limitPriceMultiplier * 1e6)),
|
||||||
|
triggerCondition: options.direction === 'long'
|
||||||
|
? OrderTriggerCondition.BELOW
|
||||||
|
: OrderTriggerCondition.ABOVE,
|
||||||
|
reduceOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🛡️ Placing SL as TRIGGER_LIMIT (${stopLimitBuffer}% buffer)...`)
|
||||||
|
console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
|
||||||
|
console.log(` Limit: $${(options.stopLossPrice * limitPriceMultiplier).toFixed(4)}`)
|
||||||
|
console.log(` ⚠️ May not fill during fast moves - use for liquid markets only!`)
|
||||||
|
|
||||||
|
const sig = await (driftClient as any).placePerpOrder(orderParams)
|
||||||
|
console.log('✅ SL trigger-limit order placed:', sig)
|
||||||
|
signatures.push(sig)
|
||||||
|
} else {
|
||||||
|
// TRIGGER_MARKET: Default, guaranteed execution
|
||||||
|
const orderParams: any = {
|
||||||
|
orderType: OrderType.TRIGGER_MARKET,
|
||||||
|
marketIndex: marketConfig.driftMarketIndex,
|
||||||
|
direction: orderDirection,
|
||||||
|
baseAssetAmount: new BN(slBaseAmount),
|
||||||
|
triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)),
|
||||||
|
triggerCondition: options.direction === 'long'
|
||||||
|
? OrderTriggerCondition.BELOW
|
||||||
|
: OrderTriggerCondition.ABOVE,
|
||||||
|
reduceOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🛡️ Placing SL as TRIGGER_MARKET (guaranteed execution - RECOMMENDED)...`)
|
||||||
|
console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`)
|
||||||
|
console.log(` ✅ Will execute at market price when triggered (may slip but WILL fill)`)
|
||||||
|
|
||||||
|
const sig = await (driftClient as any).placePerpOrder(orderParams)
|
||||||
|
console.log('✅ SL trigger-market order placed:', sig)
|
||||||
|
signatures.push(sig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ SL size below market min, skipping on-chain SL')
|
console.log('⚠️ SL size below market min, skipping on-chain SL')
|
||||||
|
|||||||
1543
package-lock.json
generated
1543
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@drift-labs/sdk": "^2.75.0",
|
"@drift-labs/sdk": "^2.75.0",
|
||||||
|
"@prisma/client": "^6.18.0",
|
||||||
"@pythnetwork/hermes-client": "^1.0.0",
|
"@pythnetwork/hermes-client": "^1.0.0",
|
||||||
"@pythnetwork/price-service-client": "^1.3.0",
|
"@pythnetwork/price-service-client": "^1.3.0",
|
||||||
"@solana/web3.js": "^1.91.1",
|
"@solana/web3.js": "^1.91.1",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"bs58": "^5.0.0",
|
"bs58": "^5.0.0",
|
||||||
"next": "^15.0.0",
|
"next": "^15.0.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"prisma": "^6.18.0",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
"react-dom": "^18.3.0",
|
"react-dom": "^18.3.0",
|
||||||
"tailwindcss": "^3.4.1"
|
"tailwindcss": "^3.4.1"
|
||||||
|
|||||||
123
prisma/migrations/20251026200052_init/migration.sql
Normal file
123
prisma/migrations/20251026200052_init/migration.sql
Normal 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;
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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
131
prisma/schema.prisma
Normal 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])
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user