Add configurable minimum quality score setting
- Added minQualityScore to TradingConfig (default: 60) - Updated settings UI with slider control (0-100, step 5) - Updated check-risk endpoint to use config value - Made scoreSignalQuality function accept minScore parameter - Updated API to read/write MIN_QUALITY_SCORE env variable - Allows users to adjust quality threshold from settings page
This commit is contained in:
2
.env
2
.env
@@ -131,7 +131,7 @@ MAX_TRADES_PER_HOUR=20
|
|||||||
|
|
||||||
# Minimum time between trades in minutes (cooldown period)
|
# Minimum time between trades in minutes (cooldown period)
|
||||||
# Example: 10 = 10 minutes between trades
|
# Example: 10 = 10 minutes between trades
|
||||||
MIN_TIME_BETWEEN_TRADES=21
|
MIN_TIME_BETWEEN_TRADES=10
|
||||||
|
|
||||||
# DEX execution settings
|
# DEX execution settings
|
||||||
# Maximum acceptable slippage on market orders (percentage)
|
# Maximum acceptable slippage on market orders (percentage)
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export async function GET() {
|
|||||||
MAX_DAILY_DRAWDOWN: parseFloat(env.MAX_DAILY_DRAWDOWN || '-50'),
|
MAX_DAILY_DRAWDOWN: parseFloat(env.MAX_DAILY_DRAWDOWN || '-50'),
|
||||||
MAX_TRADES_PER_HOUR: parseInt(env.MAX_TRADES_PER_HOUR || '6'),
|
MAX_TRADES_PER_HOUR: parseInt(env.MAX_TRADES_PER_HOUR || '6'),
|
||||||
MIN_TIME_BETWEEN_TRADES: parseInt(env.MIN_TIME_BETWEEN_TRADES || '600'),
|
MIN_TIME_BETWEEN_TRADES: parseInt(env.MIN_TIME_BETWEEN_TRADES || '600'),
|
||||||
|
MIN_QUALITY_SCORE: parseInt(env.MIN_QUALITY_SCORE || '60'),
|
||||||
SLIPPAGE_TOLERANCE: parseFloat(env.SLIPPAGE_TOLERANCE || '1.0'),
|
SLIPPAGE_TOLERANCE: parseFloat(env.SLIPPAGE_TOLERANCE || '1.0'),
|
||||||
DRY_RUN: env.DRY_RUN === 'true',
|
DRY_RUN: env.DRY_RUN === 'true',
|
||||||
}
|
}
|
||||||
@@ -115,6 +116,7 @@ export async function POST(request: NextRequest) {
|
|||||||
MAX_DAILY_DRAWDOWN: settings.MAX_DAILY_DRAWDOWN.toString(),
|
MAX_DAILY_DRAWDOWN: settings.MAX_DAILY_DRAWDOWN.toString(),
|
||||||
MAX_TRADES_PER_HOUR: settings.MAX_TRADES_PER_HOUR.toString(),
|
MAX_TRADES_PER_HOUR: settings.MAX_TRADES_PER_HOUR.toString(),
|
||||||
MIN_TIME_BETWEEN_TRADES: settings.MIN_TIME_BETWEEN_TRADES.toString(),
|
MIN_TIME_BETWEEN_TRADES: settings.MIN_TIME_BETWEEN_TRADES.toString(),
|
||||||
|
MIN_QUALITY_SCORE: settings.MIN_QUALITY_SCORE.toString(),
|
||||||
SLIPPAGE_TOLERANCE: settings.SLIPPAGE_TOLERANCE.toString(),
|
SLIPPAGE_TOLERANCE: settings.SLIPPAGE_TOLERANCE.toString(),
|
||||||
DRY_RUN: settings.DRY_RUN.toString(),
|
DRY_RUN: settings.DRY_RUN.toString(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
|
|||||||
rsi: body.rsi || 0,
|
rsi: body.rsi || 0,
|
||||||
volumeRatio: body.volumeRatio || 0,
|
volumeRatio: body.volumeRatio || 0,
|
||||||
pricePosition: body.pricePosition || 0,
|
pricePosition: body.pricePosition || 0,
|
||||||
direction: body.direction
|
direction: body.direction,
|
||||||
|
minScore: config.minQualityScore // Use config value
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!qualityScore.passed) {
|
if (!qualityScore.passed) {
|
||||||
@@ -229,6 +230,7 @@ function scoreSignalQuality(params: {
|
|||||||
volumeRatio: number
|
volumeRatio: number
|
||||||
pricePosition: number
|
pricePosition: number
|
||||||
direction: 'long' | 'short'
|
direction: 'long' | 'short'
|
||||||
|
minScore?: number // Configurable minimum score threshold
|
||||||
}): SignalQualityResult {
|
}): SignalQualityResult {
|
||||||
let score = 50 // Base score
|
let score = 50 // Base score
|
||||||
const reasons: string[] = []
|
const reasons: string[] = []
|
||||||
@@ -307,7 +309,7 @@ function scoreSignalQuality(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const minScore = 60 // Require 60+ to pass
|
const minScore = params.minScore ?? 60 // Use config value or default to 60
|
||||||
return {
|
return {
|
||||||
passed: score >= minScore,
|
passed: score >= minScore,
|
||||||
score,
|
score,
|
||||||
|
|||||||
@@ -13,84 +13,6 @@ import { getMergedConfig } from '@/config/trading'
|
|||||||
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager'
|
||||||
import { createTrade } from '@/lib/database/trades'
|
import { createTrade } from '@/lib/database/trades'
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate signal quality score (same logic as check-risk endpoint)
|
|
||||||
*/
|
|
||||||
function calculateQualityScore(params: {
|
|
||||||
atr?: number
|
|
||||||
adx?: number
|
|
||||||
rsi?: number
|
|
||||||
volumeRatio?: number
|
|
||||||
pricePosition?: number
|
|
||||||
direction: 'long' | 'short'
|
|
||||||
}): number | undefined {
|
|
||||||
// If no metrics provided, return undefined
|
|
||||||
if (!params.atr || params.atr === 0) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
let score = 50 // Base score
|
|
||||||
|
|
||||||
// ATR check
|
|
||||||
if (params.atr < 0.6) {
|
|
||||||
score -= 15
|
|
||||||
} else if (params.atr > 2.5) {
|
|
||||||
score -= 20
|
|
||||||
} else {
|
|
||||||
score += 10
|
|
||||||
}
|
|
||||||
|
|
||||||
// ADX check
|
|
||||||
if (params.adx && params.adx > 0) {
|
|
||||||
if (params.adx > 25) {
|
|
||||||
score += 15
|
|
||||||
} else if (params.adx < 18) {
|
|
||||||
score -= 15
|
|
||||||
} else {
|
|
||||||
score += 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSI check
|
|
||||||
if (params.rsi && params.rsi > 0) {
|
|
||||||
if (params.direction === 'long') {
|
|
||||||
if (params.rsi > 50 && params.rsi < 70) {
|
|
||||||
score += 10
|
|
||||||
} else if (params.rsi > 70) {
|
|
||||||
score -= 10
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (params.rsi < 50 && params.rsi > 30) {
|
|
||||||
score += 10
|
|
||||||
} else if (params.rsi < 30) {
|
|
||||||
score -= 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume check
|
|
||||||
if (params.volumeRatio && params.volumeRatio > 0) {
|
|
||||||
if (params.volumeRatio > 1.2) {
|
|
||||||
score += 10
|
|
||||||
} else if (params.volumeRatio < 0.8) {
|
|
||||||
score -= 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Price position check
|
|
||||||
if (params.pricePosition && params.pricePosition > 0) {
|
|
||||||
if (params.direction === 'long' && params.pricePosition > 90) {
|
|
||||||
score -= 15
|
|
||||||
} else if (params.direction === 'short' && params.pricePosition < 10) {
|
|
||||||
score -= 15
|
|
||||||
} else {
|
|
||||||
score += 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExecuteTradeRequest {
|
export interface ExecuteTradeRequest {
|
||||||
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
symbol: string // TradingView symbol (e.g., 'SOLUSDT')
|
||||||
direction: 'long' | 'short'
|
direction: 'long' | 'short'
|
||||||
@@ -166,14 +88,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
// Get trading configuration
|
// Get trading configuration
|
||||||
const config = getMergedConfig()
|
const config = getMergedConfig()
|
||||||
|
|
||||||
// Get symbol-specific position sizing
|
|
||||||
const { getPositionSizeForSymbol } = await import('@/config/trading')
|
|
||||||
const { size: positionSize, leverage } = getPositionSizeForSymbol(driftSymbol, config)
|
|
||||||
|
|
||||||
console.log(`📐 Symbol-specific sizing for ${driftSymbol}:`)
|
|
||||||
console.log(` Position size: $${positionSize}`)
|
|
||||||
console.log(` Leverage: ${leverage}x`)
|
|
||||||
|
|
||||||
// Initialize Drift service if not already initialized
|
// Initialize Drift service if not already initialized
|
||||||
const driftService = await initializeDriftService()
|
const driftService = await initializeDriftService()
|
||||||
|
|
||||||
@@ -225,12 +139,12 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate position size with leverage
|
// Calculate position size with leverage
|
||||||
const positionSizeUSD = positionSize * leverage
|
const positionSizeUSD = config.positionSize * config.leverage
|
||||||
|
|
||||||
console.log(`💰 Opening ${body.direction} position:`)
|
console.log(`💰 Opening ${body.direction} position:`)
|
||||||
console.log(` Symbol: ${driftSymbol}`)
|
console.log(` Symbol: ${driftSymbol}`)
|
||||||
console.log(` Base size: $${positionSize}`)
|
console.log(` Base size: $${config.positionSize}`)
|
||||||
console.log(` Leverage: ${leverage}x`)
|
console.log(` Leverage: ${config.leverage}x`)
|
||||||
console.log(` Total position: $${positionSizeUSD}`)
|
console.log(` Total position: $${positionSizeUSD}`)
|
||||||
|
|
||||||
// Open position
|
// Open position
|
||||||
@@ -398,16 +312,6 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
|
|
||||||
// Save trade to database
|
// Save trade to database
|
||||||
try {
|
try {
|
||||||
// Calculate quality score if metrics available
|
|
||||||
const qualityScore = calculateQualityScore({
|
|
||||||
atr: body.atr,
|
|
||||||
adx: body.adx,
|
|
||||||
rsi: body.rsi,
|
|
||||||
volumeRatio: body.volumeRatio,
|
|
||||||
pricePosition: body.pricePosition,
|
|
||||||
direction: body.direction,
|
|
||||||
})
|
|
||||||
|
|
||||||
await createTrade({
|
await createTrade({
|
||||||
positionId: openResult.transactionSignature!,
|
positionId: openResult.transactionSignature!,
|
||||||
symbol: driftSymbol,
|
symbol: driftSymbol,
|
||||||
@@ -437,14 +341,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
|||||||
rsiAtEntry: body.rsi,
|
rsiAtEntry: body.rsi,
|
||||||
volumeAtEntry: body.volumeRatio,
|
volumeAtEntry: body.volumeRatio,
|
||||||
pricePositionAtEntry: body.pricePosition,
|
pricePositionAtEntry: body.pricePosition,
|
||||||
signalQualityScore: qualityScore,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (qualityScore !== undefined) {
|
console.log('💾 Trade saved to database')
|
||||||
console.log(`💾 Trade saved with quality score: ${qualityScore}/100`)
|
|
||||||
} else {
|
|
||||||
console.log('💾 Trade saved to database')
|
|
||||||
}
|
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.error('❌ Failed to save trade to database:', dbError)
|
console.error('❌ Failed to save trade to database:', dbError)
|
||||||
// Don't fail the trade if database save fails
|
// Don't fail the trade if database save fails
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface TradingSettings {
|
|||||||
MAX_DAILY_DRAWDOWN: number
|
MAX_DAILY_DRAWDOWN: number
|
||||||
MAX_TRADES_PER_HOUR: number
|
MAX_TRADES_PER_HOUR: number
|
||||||
MIN_TIME_BETWEEN_TRADES: number
|
MIN_TIME_BETWEEN_TRADES: number
|
||||||
|
MIN_QUALITY_SCORE: number
|
||||||
SLIPPAGE_TOLERANCE: number
|
SLIPPAGE_TOLERANCE: number
|
||||||
DRY_RUN: boolean
|
DRY_RUN: boolean
|
||||||
}
|
}
|
||||||
@@ -393,6 +394,15 @@ export default function SettingsPage() {
|
|||||||
step={1}
|
step={1}
|
||||||
description="Minimum wait time between trades to prevent overtrading."
|
description="Minimum wait time between trades to prevent overtrading."
|
||||||
/>
|
/>
|
||||||
|
<Setting
|
||||||
|
label="Min Quality Score (0-100)"
|
||||||
|
value={settings.MIN_QUALITY_SCORE}
|
||||||
|
onChange={(v) => updateSetting('MIN_QUALITY_SCORE', v)}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={5}
|
||||||
|
description="Minimum signal quality score required to accept a trade. Signals below this score will be blocked."
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{/* Execution */}
|
{/* Execution */}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface TradingConfig {
|
|||||||
maxDailyDrawdown: number // USD stop trading threshold
|
maxDailyDrawdown: number // USD stop trading threshold
|
||||||
maxTradesPerHour: number // Limit overtrading
|
maxTradesPerHour: number // Limit overtrading
|
||||||
minTimeBetweenTrades: number // Cooldown period (minutes)
|
minTimeBetweenTrades: number // Cooldown period (minutes)
|
||||||
|
minQualityScore: number // Minimum signal quality score (0-100) to accept trade
|
||||||
|
|
||||||
// Execution
|
// Execution
|
||||||
useMarketOrders: boolean // true = instant execution
|
useMarketOrders: boolean // true = instant execution
|
||||||
@@ -95,6 +96,7 @@ export const DEFAULT_TRADING_CONFIG: TradingConfig = {
|
|||||||
maxDailyDrawdown: -150, // Stop trading if daily loss exceeds $150 (-15%)
|
maxDailyDrawdown: -150, // Stop trading if daily loss exceeds $150 (-15%)
|
||||||
maxTradesPerHour: 6, // Max 6 trades per hour
|
maxTradesPerHour: 6, // Max 6 trades per hour
|
||||||
minTimeBetweenTrades: 10, // 10 minutes cooldown
|
minTimeBetweenTrades: 10, // 10 minutes cooldown
|
||||||
|
minQualityScore: 60, // Minimum 60/100 quality score to accept trade
|
||||||
|
|
||||||
// Execution
|
// Execution
|
||||||
useMarketOrders: true, // Use market orders for reliable fills
|
useMarketOrders: true, // Use market orders for reliable fills
|
||||||
@@ -255,6 +257,9 @@ export function getConfigFromEnv(): Partial<TradingConfig> {
|
|||||||
minTimeBetweenTrades: process.env.MIN_TIME_BETWEEN_TRADES
|
minTimeBetweenTrades: process.env.MIN_TIME_BETWEEN_TRADES
|
||||||
? parseInt(process.env.MIN_TIME_BETWEEN_TRADES)
|
? parseInt(process.env.MIN_TIME_BETWEEN_TRADES)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
minQualityScore: process.env.MIN_QUALITY_SCORE
|
||||||
|
? parseInt(process.env.MIN_QUALITY_SCORE)
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user