critical: Fix Smart Validation Queue blockReason mismatch (Bug #84)

Root Cause: check-risk endpoint passes blockReason='SMART_VALIDATION_QUEUED'
but addSignal() only accepted 'QUALITY_SCORE_TOO_LOW' → signals blocked but never queued

Impact: Quality 85 LONG signal at 08:40:03 saved to database but never monitored
User missed validation opportunity when price moved favorably

Fix: Accept both blockReason variants in addSignal() validation check

Evidence:
- Database record cmj41pdqu0101pf07mith5s4c has blockReason='SMART_VALIDATION_QUEUED'
- No logs showing addSignal() execution (would log ' Smart validation queued')
- check-risk code line 451 passes 'SMART_VALIDATION_QUEUED'
- addSignal() line 76 rejected signals != 'QUALITY_SCORE_TOO_LOW'

Result: Quality 50-89 signals will now be properly queued for validation
This commit is contained in:
mindesbunister
2025-12-13 17:24:38 +01:00
parent 12b4c7cafc
commit 5d5868d802
7 changed files with 575 additions and 69 deletions

View File

@@ -19,6 +19,12 @@ services:
- TELEGRAM_CHAT_ID=579304651
- TRADING_BOT_URL=${TRADING_BOT_URL:-http://trading-bot-v4:3000}
- API_SECRET_KEY=${API_SECRET_KEY}
healthcheck:
test: ["CMD", "python3", "-c", "import sys; sys.exit(0)"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- traderv4_trading-net

View File

@@ -28,6 +28,65 @@ export interface HealthCheckResult {
driftPositions: number
unprotectedPositions: number
}
autoSyncTriggered?: boolean
}
// Cooldown to prevent sync spam (5 minutes)
let lastAutoSyncTime = 0
const AUTO_SYNC_COOLDOWN_MS = 5 * 60 * 1000
/**
* Automatically trigger position sync when untracked positions detected
*
* CRITICAL: This prevents the $1,000 loss scenario where Telegram bot
* opens positions but Position Manager never tracks them.
*
* Cooldown: 5 minutes between auto-syncs to prevent spam
*/
async function autoSyncUntrackedPositions(): Promise<boolean> {
const now = Date.now()
// Check cooldown
if (now - lastAutoSyncTime < AUTO_SYNC_COOLDOWN_MS) {
const remainingSeconds = Math.ceil((AUTO_SYNC_COOLDOWN_MS - (now - lastAutoSyncTime)) / 1000)
console.log(`⏳ Auto-sync cooldown active (${remainingSeconds}s remaining)`)
return false
}
try {
console.log('🔄 AUTO-SYNC TRIGGERED: Untracked positions detected, syncing with Drift...')
// Call the sync endpoint internally
const pm = await getInitializedPositionManager()
const driftService = getDriftService()
const driftPositions = await driftService.getAllPositions()
console.log(`📊 Found ${driftPositions.length} positions on Drift`)
let added = 0
for (const driftPos of driftPositions) {
const isTracked = Array.from((pm as any).activeTrades.values()).some(
(t: any) => t.symbol === driftPos.symbol
)
if (!isTracked && Math.abs(driftPos.size) > 0) {
console.log(` Auto-adding ${driftPos.symbol} to Position Manager`)
// Import sync logic
const { syncSinglePosition } = await import('../trading/sync-helper')
await syncSinglePosition(driftPos, pm)
added++
}
}
lastAutoSyncTime = now
console.log(`✅ AUTO-SYNC COMPLETE: Added ${added} position(s) to monitoring`)
return true
} catch (error) {
console.error('❌ Auto-sync failed:', error)
return false
}
}
/**
@@ -95,24 +154,42 @@ export async function checkPositionManagerHealth(): Promise<HealthCheckResult> {
if (pmActiveTrades !== driftPositions) {
warnings.push(`⚠️ WARNING: Position Manager has ${pmActiveTrades} trades, Drift has ${driftPositions} positions`)
warnings.push(` Possible untracked position or external closure`)
// AUTO-SYNC: If Drift has MORE positions than PM, sync automatically
if (driftPositions > pmActiveTrades) {
console.log(`🚨 UNTRACKED POSITIONS DETECTED: Drift has ${driftPositions}, PM has ${pmActiveTrades}`)
const synced = await autoSyncUntrackedPositions()
if (synced) {
warnings.push(`✅ AUTO-SYNC: Triggered position sync to restore protection`)
// Re-check PM state after sync
pmActiveTrades = (pm as any).activeTrades?.size || 0
}
}
}
// Check for unprotected positions (no SL/TP orders)
// Check for unprotected positions
// NOTE: Synced/placeholder positions (signalSource='autosync') have NULL signatures in DB
// but orders exist on Drift. Position Manager monitoring provides backup protection.
let unprotectedPositions = 0
for (const trade of dbTrades) {
if (!trade.slOrderTx && !trade.softStopOrderTx && !trade.hardStopOrderTx) {
const hasDbSignatures = !!(trade.slOrderTx || trade.softStopOrderTx || trade.hardStopOrderTx)
const isSyncedPosition = trade.signalSource === 'autosync' || trade.timeframe === 'sync'
if (!hasDbSignatures && !isSyncedPosition) {
// This is NOT a synced position but has no SL orders - CRITICAL
unprotectedPositions++
issues.push(`❌ CRITICAL: Position ${trade.symbol} (${trade.id}) has NO STOP LOSS ORDERS!`)
issues.push(` Entry: $${trade.entryPrice}, Size: $${trade.positionSizeUSD}`)
issues.push(` This is the silent SL placement failure bug`)
}
if (!trade.tp1OrderTx) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP1 order`)
if (!trade.tp1OrderTx && !isSyncedPosition) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP1 order (not synced)`)
}
if (!trade.tp2OrderTx) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP2 order`)
if (!trade.tp2OrderTx && !isSyncedPosition) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP2 order (not synced)`)
}
}

View File

@@ -811,38 +811,109 @@ export class PositionManager {
trade.slMovedToBreakeven = true
logger.log(`🛡️ Stop loss moved to: $${trade.stopLossPrice.toFixed(4)}`)
// CRITICAL: Update on-chain orders to reflect new SL at breakeven
try {
const { cancelAllOrders, placeExitOrders } = await import('../drift/orders')
// CRITICAL FIX (Dec 12, 2025): Check if we have order signatures
// Auto-synced positions may have NULL signatures, need fallback
const { updateTradeState } = await import('../database/trades')
const dbTrade = await this.prisma.trade.findUnique({
where: { id: trade.id },
select: { slOrderTx: true, softStopOrderTx: true, hardStopOrderTx: true }
})
const hasOrderSignatures = dbTrade && (
dbTrade.slOrderTx || dbTrade.softStopOrderTx || dbTrade.hardStopOrderTx
)
if (!hasOrderSignatures) {
logger.log(`⚠️ No order signatures found - auto-synced position detected`)
logger.log(`🔧 FALLBACK: Placing fresh SL order at breakeven $${trade.stopLossPrice.toFixed(4)}`)
logger.log(`🔄 Cancelling old exit orders...`)
const cancelResult = await cancelAllOrders(trade.symbol)
if (cancelResult.success) {
logger.log(`✅ Cancelled ${cancelResult.cancelledCount} old orders`)
// Place fresh SL order without trying to cancel (no signatures to cancel)
const { placeExitOrders } = await import('../drift/orders')
try {
const placeResult = await placeExitOrders({
symbol: trade.symbol,
positionSizeUSD: trade.currentSize,
entryPrice: trade.entryPrice,
tp1Price: trade.tp2Price || trade.entryPrice * (trade.direction === 'long' ? 1.02 : 0.98),
tp2Price: trade.tp2Price || trade.entryPrice * (trade.direction === 'long' ? 1.04 : 0.96),
stopLossPrice: trade.stopLossPrice,
tp1SizePercent: 0, // Already hit, don't place TP1
tp2SizePercent: trade.tp2SizePercent || 0,
direction: trade.direction,
useDualStops: this.config.useDualStops,
softStopPrice: this.config.useDualStops ? trade.stopLossPrice : undefined,
hardStopPrice: this.config.useDualStops
? (trade.direction === 'long' ? trade.stopLossPrice * 0.99 : trade.stopLossPrice * 1.01)
: undefined,
})
if (placeResult.success && placeResult.signatures) {
logger.log(`✅ Fresh SL order placed successfully`)
// Update database with new order signatures
const updateData: any = {
stopLossPrice: trade.stopLossPrice,
}
if (this.config.useDualStops && placeResult.signatures.length >= 2) {
updateData.softStopOrderTx = placeResult.signatures[0]
updateData.hardStopOrderTx = placeResult.signatures[1]
logger.log(`💾 Recorded dual SL signatures: soft=${placeResult.signatures[0].slice(0,8)}... hard=${placeResult.signatures[1].slice(0,8)}...`)
} else if (placeResult.signatures.length >= 1) {
updateData.slOrderTx = placeResult.signatures[0]
logger.log(`💾 Recorded SL signature: ${placeResult.signatures[0].slice(0,8)}...`)
}
await updateTradeState({
id: trade.id,
...updateData
})
logger.log(`✅ Database updated with new SL order signatures`)
} else {
console.error(`❌ Failed to place fresh SL order:`, placeResult.error)
logger.log(`⚠️ CRITICAL: Runner has NO STOP LOSS PROTECTION - manual intervention needed`)
}
} catch (placeError) {
console.error(`❌ Error placing fresh SL order:`, placeError)
logger.log(`⚠️ CRITICAL: Runner has NO STOP LOSS PROTECTION - manual intervention needed`)
}
} else {
// Normal flow: Has order signatures, can cancel and replace
logger.log(`✅ Order signatures found - normal order update flow`)
logger.log(`🛡️ Placing new exit orders with SL at breakeven...`)
const orderResult = await placeExitOrders({
symbol: trade.symbol,
direction: trade.direction,
entryPrice: trade.entryPrice,
positionSizeUSD: trade.currentSize, // Runner size
stopLossPrice: trade.stopLossPrice, // At breakeven now
tp1Price: trade.tp2Price, // TP2 becomes new TP1 for runner
tp2Price: 0, // No TP2 for runner
tp1SizePercent: 0, // Close 0% at TP2 (activates trailing)
tp2SizePercent: 0, // No TP2
softStopPrice: 0,
hardStopPrice: 0,
})
if (orderResult.success) {
logger.log(`✅ Exit orders updated with SL at breakeven`)
} else {
console.error(`❌ Failed to update exit orders:`, orderResult.error)
try {
const { cancelAllOrders, placeExitOrders } = await import('../drift/orders')
logger.log(`🔄 Cancelling old exit orders...`)
const cancelResult = await cancelAllOrders(trade.symbol)
if (cancelResult.success) {
logger.log(`✅ Cancelled ${cancelResult.cancelledCount} old orders`)
}
logger.log(`🛡️ Placing new exit orders with SL at breakeven...`)
const orderResult = await placeExitOrders({
symbol: trade.symbol,
direction: trade.direction,
entryPrice: trade.entryPrice,
positionSizeUSD: trade.currentSize, // Runner size
stopLossPrice: trade.stopLossPrice, // At breakeven now
tp1Price: trade.tp2Price, // TP2 becomes new TP1 for runner
tp2Price: 0, // No TP2 for runner
tp1SizePercent: 0, // Close 0% at TP2 (activates trailing)
tp2SizePercent: 0, // No TP2
softStopPrice: 0,
hardStopPrice: 0,
})
if (orderResult.success) {
logger.log(`✅ Exit orders updated with SL at breakeven`)
} else {
console.error(`❌ Failed to update exit orders:`, orderResult.error)
}
} catch (error) {
console.error(`❌ Failed to update on-chain orders after TP1:`, error)
}
} catch (error) {
console.error(`❌ Failed to update on-chain orders after TP1:`, error)
}
await this.saveTradeState(trade)

View File

@@ -73,7 +73,9 @@ class SmartValidationQueue {
const config = getMergedConfig()
// Only queue signals blocked for quality (not cooldown, rate limits, etc.)
if (params.blockReason !== 'QUALITY_SCORE_TOO_LOW') {
// CRITICAL FIX (Dec 13, 2025): Accept both QUALITY_SCORE_TOO_LOW and SMART_VALIDATION_QUEUED
// Bug: check-risk sends 'SMART_VALIDATION_QUEUED' but addSignal() only accepted 'QUALITY_SCORE_TOO_LOW'
if (params.blockReason !== 'QUALITY_SCORE_TOO_LOW' && params.blockReason !== 'SMART_VALIDATION_QUEUED') {
return null
}

266
lib/trading/sync-helper.ts Normal file
View File

@@ -0,0 +1,266 @@
/**
* Position Sync Helper
*
* Extracted from sync-positions endpoint to allow internal reuse
* by health monitor for automatic position synchronization.
*
* Created: Dec 12, 2025
* Enhanced: Dec 12, 2025 - Added order signature discovery for synced positions
*/
import { getDriftService } from '../drift/client'
import { getPrismaClient } from '../database/trades'
import { getMergedConfig } from '@/config/trading'
import { OrderType, OrderTriggerCondition } from '@drift-labs/sdk'
/**
* Query Drift for existing orders on a position and extract signatures
* CRITICAL FIX (Dec 12, 2025): Auto-synced positions need order signatures
* so Position Manager can update orders after TP1/TP2 events
*/
async function discoverExistingOrders(symbol: string, marketIndex: number): Promise<{
tp1OrderTx?: string
tp2OrderTx?: string
slOrderTx?: string
softStopOrderTx?: string
hardStopOrderTx?: string
}> {
try {
const driftService = getDriftService()
const driftClient = driftService.getClient()
// Get all open orders for this user
const orders = driftClient.getOpenOrders()
// Filter for orders on this market that are reduce-only
const marketOrders = orders.filter(order =>
order.marketIndex === marketIndex &&
order.reduceOnly === true
)
const signatures: any = {}
for (const order of marketOrders) {
// Try to extract transaction signature from order reference
// Note: Drift SDK may not expose TX signatures directly on orders
// This is a best-effort attempt to recover order references
const orderRefStr = order.orderId?.toString() || ''
if (order.orderType === OrderType.LIMIT) {
// TP1 or TP2 (LIMIT orders)
// Higher trigger = TP for long, lower trigger = TP for short
if (!signatures.tp1OrderTx) {
signatures.tp1OrderTx = orderRefStr
console.log(`🔍 Found potential TP1 order: ${orderRefStr.slice(0, 8)}...`)
} else if (!signatures.tp2OrderTx) {
signatures.tp2OrderTx = orderRefStr
console.log(`🔍 Found potential TP2 order: ${orderRefStr.slice(0, 8)}...`)
}
} else if (order.orderType === OrderType.TRIGGER_MARKET) {
// Hard stop loss (TRIGGER_MARKET)
if (!signatures.slOrderTx && !signatures.hardStopOrderTx) {
signatures.hardStopOrderTx = orderRefStr
console.log(`🔍 Found hard SL order: ${orderRefStr.slice(0, 8)}...`)
}
} else if (order.orderType === OrderType.TRIGGER_LIMIT) {
// Soft stop loss (TRIGGER_LIMIT)
if (!signatures.softStopOrderTx) {
signatures.softStopOrderTx = orderRefStr
console.log(`🔍 Found soft SL order: ${orderRefStr.slice(0, 8)}...`)
}
}
}
// Log what we found
const foundCount = Object.keys(signatures).filter(k => signatures[k]).length
if (foundCount > 0) {
console.log(`✅ Discovered ${foundCount} existing order(s) for ${symbol}`)
} else {
console.warn(`⚠️ No existing orders found for ${symbol} - PM won't be able to update orders after events`)
}
return signatures
} catch (error) {
console.error(`❌ Error discovering orders for ${symbol}:`, error)
return {} // Return empty object on error (fail gracefully)
}
}
/**
* Sync a single Drift position to Position Manager
*/
export async function syncSinglePosition(driftPos: any, positionManager: any): Promise<void> {
const driftService = getDriftService()
const prisma = getPrismaClient()
const config = getMergedConfig()
try {
// Get current oracle price for this market
const currentPrice = await driftService.getOraclePrice(driftPos.marketIndex)
// Calculate targets based on current config
const direction = driftPos.side
const entryPrice = driftPos.entryPrice
// Calculate TP/SL prices
const calculatePrice = (entry: number, percent: number, dir: 'long' | 'short') => {
if (dir === 'long') {
return entry * (1 + percent / 100)
} else {
return entry * (1 - percent / 100)
}
}
const stopLossPrice = calculatePrice(entryPrice, config.stopLossPercent, direction)
const tp1Price = calculatePrice(entryPrice, config.takeProfit1Percent, direction)
const tp2Price = calculatePrice(entryPrice, config.takeProfit2Percent, direction)
const emergencyStopPrice = calculatePrice(entryPrice, config.emergencyStopPercent, direction)
// Calculate position size in USD (Drift size is tokens)
const positionSizeUSD = Math.abs(driftPos.size) * currentPrice
// Try to find an existing open trade in the database for this symbol
const existingTrade = await prisma.trade.findFirst({
where: {
symbol: driftPos.symbol,
status: 'open',
},
orderBy: { entryTime: 'desc' },
})
const normalizeDirection = (dir: string): 'long' | 'short' =>
dir === 'long' ? 'long' : 'short'
const buildActiveTradeFromDb = (dbTrade: any): any => {
const pmState = (dbTrade.configSnapshot as any)?.positionManagerState
return {
id: dbTrade.id,
positionId: dbTrade.positionId,
symbol: dbTrade.symbol,
direction: normalizeDirection(dbTrade.direction),
entryPrice: dbTrade.entryPrice,
entryTime: dbTrade.entryTime.getTime(),
positionSize: dbTrade.positionSizeUSD,
leverage: dbTrade.leverage,
stopLossPrice: pmState?.stopLossPrice ?? dbTrade.stopLossPrice,
tp1Price: dbTrade.takeProfit1Price,
tp2Price: dbTrade.takeProfit2Price,
emergencyStopPrice: dbTrade.stopLossPrice * (dbTrade.direction === 'long' ? 0.98 : 1.02),
currentSize: pmState?.currentSize ?? dbTrade.positionSizeUSD,
originalPositionSize: dbTrade.positionSizeUSD,
takeProfitPrice1: dbTrade.takeProfit1Price,
takeProfitPrice2: dbTrade.takeProfit2Price,
tp1Hit: pmState?.tp1Hit ?? false,
tp2Hit: pmState?.tp2Hit ?? false,
slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false,
slMovedToProfit: pmState?.slMovedToProfit ?? false,
trailingStopActive: pmState?.trailingStopActive ?? false,
trailingStopDistance: pmState?.trailingStopDistance,
realizedPnL: pmState?.realizedPnL ?? 0,
unrealizedPnL: pmState?.unrealizedPnL ?? 0,
peakPnL: pmState?.peakPnL ?? 0,
peakPrice: pmState?.peakPrice ?? dbTrade.entryPrice,
maxFavorableExcursion: pmState?.maxFavorableExcursion ?? 0,
maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0,
maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice,
maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice,
originalAdx: dbTrade.adxAtEntry,
timesScaled: pmState?.timesScaled ?? 0,
totalScaleAdded: pmState?.totalScaleAdded ?? 0,
atrAtEntry: dbTrade.atrAtEntry,
// TP2 runner configuration (CRITICAL FIX - Dec 12, 2025)
tp1SizePercent: pmState?.tp1SizePercent ?? dbTrade.tp1SizePercent ?? config.takeProfit1SizePercent,
tp2SizePercent: pmState?.tp2SizePercent ?? dbTrade.tp2SizePercent ?? config.takeProfit2SizePercent,
runnerTrailingPercent: pmState?.runnerTrailingPercent,
priceCheckCount: 0,
lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice,
lastUpdateTime: Date.now(),
}
}
let activeTrade
if (existingTrade) {
console.log(`🔗 Found existing open trade in DB for ${driftPos.symbol}, attaching to Position Manager`)
activeTrade = buildActiveTradeFromDb(existingTrade)
} else {
console.warn(`⚠️ No open DB trade found for ${driftPos.symbol}. Creating synced placeholder to restore protection.`)
const now = new Date()
const syntheticPositionId = `autosync-${now.getTime()}-${driftPos.marketIndex}`
// CRITICAL FIX (Dec 12, 2025): Query Drift for existing orders
// Auto-synced positions need order signatures so PM can update orders after TP1/TP2 events
console.log(`🔍 Querying Drift for existing orders on ${driftPos.symbol}...`)
const existingOrders = await discoverExistingOrders(driftPos.symbol, driftPos.marketIndex)
const placeholderTrade = await prisma.trade.create({
data: {
positionId: syntheticPositionId,
symbol: driftPos.symbol,
direction,
entryPrice,
entryTime: now,
positionSizeUSD,
collateralUSD: positionSizeUSD / config.leverage,
leverage: config.leverage,
stopLossPrice,
takeProfit1Price: tp1Price,
takeProfit2Price: tp2Price,
tp1SizePercent: config.takeProfit1SizePercent,
tp2SizePercent:
config.useTp2AsTriggerOnly && (config.takeProfit2SizePercent ?? 0) <= 0
? 0
: (config.takeProfit2SizePercent ?? 0),
status: 'open',
signalSource: 'autosync',
timeframe: 'sync',
// CRITICAL FIX (Dec 12, 2025): Record discovered order signatures
tp1OrderTx: existingOrders.tp1OrderTx || null,
tp2OrderTx: existingOrders.tp2OrderTx || null,
slOrderTx: existingOrders.slOrderTx || null,
softStopOrderTx: existingOrders.softStopOrderTx || null,
hardStopOrderTx: existingOrders.hardStopOrderTx || null,
configSnapshot: {
source: 'health-monitor-autosync',
syncedAt: now.toISOString(),
discoveredOrders: existingOrders, // Store for debugging
positionManagerState: {
currentSize: positionSizeUSD,
tp1Hit: false,
tp2Hit: false,
slMovedToBreakeven: false,
slMovedToProfit: false,
trailingStopActive: false,
stopLossPrice,
realizedPnL: 0,
unrealizedPnL: driftPos.unrealizedPnL ?? 0,
peakPnL: driftPos.unrealizedPnL ?? 0,
peakPrice: entryPrice,
lastPrice: currentPrice,
maxFavorableExcursion: 0,
maxAdverseExcursion: 0,
maxFavorablePrice: entryPrice,
maxAdversePrice: entryPrice,
lastUpdate: now.toISOString(),
// TP2 runner configuration (CRITICAL FIX - Dec 12, 2025)
tp1SizePercent: config.takeProfit1SizePercent,
tp2SizePercent: config.takeProfit2SizePercent,
trailingStopDistance: undefined,
runnerTrailingPercent: undefined,
},
},
entryOrderTx: syntheticPositionId,
},
})
activeTrade = buildActiveTradeFromDb(placeholderTrade)
}
await positionManager.addTrade(activeTrade)
console.log(`✅ Synced ${driftPos.symbol} to Position Manager`)
} catch (error) {
console.error(`❌ Failed to sync ${driftPos.symbol}:`, error)
throw error
}
}

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env ts-node
/**
* Place fresh TP1 + SL orders for existing position
* Does NOT close or modify the position itself
*/
import { initializeDriftService } from '../lib/drift/client'
import { placeExitOrders } from '../lib/drift/orders'
import { getPythClient } from '../lib/pyth/client'
async function placeExitOrdersForPosition() {
try {
console.log('🔧 Initializing services...')
// Initialize Drift
const driftService = await initializeDriftService()
const driftClient = driftService.getDriftClient()
// Get current position
const position = await driftService.getPosition(0) // SOL-PERP market index
if (!position || position.baseAssetAmount.toString() === '0') {
console.error('❌ No open SOL-PERP position found')
process.exit(1)
}
console.log(`✅ Found position: ${position.baseAssetAmount.toString()} base units`)
// Get current price
const pythClient = getPythClient()
const currentPrice = await pythClient.getPrice('SOL-PERP')
console.log(`💰 Current SOL price: $${currentPrice}`)
// Position details from database
const entryPrice = 131.7993991266376
const positionSizeUSD = 903.1306122
const tp1Price = 132.8537943196507
const stopLossPrice = 129.822408139738
const tp1SizePercent = 60 // Close 60% at TP1
console.log(`\n📊 Position Configuration:`)
console.log(` Entry: $${entryPrice}`)
console.log(` Size: $${positionSizeUSD} (${(positionSizeUSD / currentPrice).toFixed(2)} SOL)`)
console.log(` TP1: $${tp1Price} (+${((tp1Price / entryPrice - 1) * 100).toFixed(2)}%)`)
console.log(` SL: $${stopLossPrice} (${((stopLossPrice / entryPrice - 1) * 100).toFixed(2)}%)`)
console.log(` TP1 will close: ${tp1SizePercent}%`)
console.log(` Runner: ${100 - tp1SizePercent}% (software-monitored, no on-chain TP2)`)
// Place orders
console.log(`\n🔨 Placing exit orders...`)
const result = await placeExitOrders({
symbol: 'SOL-PERP',
direction: 'long',
entryPrice: entryPrice,
positionSizeUSD: positionSizeUSD,
tp1Price: tp1Price,
tp1SizePercent: tp1SizePercent,
tp2Price: 134.171788310917, // Not used (tp2SizePercent = 0)
tp2SizePercent: 0, // No TP2 order (runner only)
stopLossPrice: stopLossPrice,
useDualStops: false, // Single SL only
})
if (result.success) {
console.log(`\n✅ Orders placed successfully!`)
console.log(` Signatures:`, result.signatures)
console.log(`\n📝 Update database with these signatures:`)
console.log(` tp1OrderTx: ${result.signatures[0]}`)
console.log(` slOrderTx: ${result.signatures[1]}`)
console.log(` tp2OrderTx: NULL (runner-only, no on-chain order)`)
} else {
console.error(`\n❌ Failed to place orders:`, result.error)
process.exit(1)
}
} catch (error) {
console.error('❌ Error:', error)
process.exit(1)
}
}
placeExitOrdersForPosition()

View File

@@ -745,14 +745,14 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
data_age_text = f" ({data_age}s old)" if data_age else ""
message = (
f"🛑 *Analytics suggest NOT entering {direction.upper()} {symbol_info['label']}*\n\n"
f"*Reason:* {analytics.get('reason', 'Unknown')}\n"
f"*Score:* {analytics.get('score', 0)}/100\n"
f"*Data:* {data_icon} {data_source}{data_age_text}\n\n"
f"Use `{text} --force` to override"
f"🛑 Analytics suggest NOT entering {direction.upper()} {symbol_info['label']}\n\n"
f"Reason: {analytics.get('reason', 'Unknown')}\n"
f"Score: {analytics.get('score', 0)}/100\n"
f"Data: {data_icon} {data_source}{data_age_text}\n\n"
f"Use '{text} --force' to override"
)
await update.message.reply_text(message, parse_mode='Markdown')
await update.message.reply_text(message)
print(f"❌ Trade blocked by analytics (score: {analytics.get('score')})", flush=True)
return
@@ -762,12 +762,12 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
data_age_text = f" ({data_age}s old)" if data_age else ""
confirm_message = (
f"*Analytics check passed ({analytics.get('score')}/100)*\n"
f"✅ Analytics check passed ({analytics.get('score')}/100)\n"
f"Data: {data_source}{data_age_text}\n"
f"Proceeding with {direction.upper()} {symbol_info['label']}..."
)
await update.message.reply_text(confirm_message, parse_mode='Markdown')
await update.message.reply_text(confirm_message)
print(f"✅ Analytics passed (score: {analytics.get('score')})", flush=True)
else:
# Analytics endpoint failed - proceed with trade (fail-open)
@@ -783,9 +783,8 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
# Send waiting message to user
await update.message.reply_text(
f"*Waiting for next 1-minute datapoint...*\n"
f"Will execute with fresh ATR (max 90s)",
parse_mode='Markdown'
f"⏳ Waiting for next 1-minute datapoint...\n"
f"Will execute with fresh ATR (max 90s)"
)
# Poll for fresh data (new timestamp = new datapoint arrived)
@@ -813,29 +812,26 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
print(f"✅ Using fresh metrics: ATR={metrics['atr']:.4f}, ADX={metrics['adx']:.1f}, RSI={metrics['rsi']:.1f} ({data_age}s old)", flush=True)
await update.message.reply_text(
f"*Fresh data received*\n"
f"✅ Fresh data received\n"
f"ATR: {metrics['atr']:.4f} | ADX: {metrics['adx']:.1f} | RSI: {metrics['rsi']:.1f}\n"
f"Executing {direction.upper()} {symbol_info['label']}...",
parse_mode='Markdown'
f"Executing {direction.upper()} {symbol_info['label']}..."
)
else:
print(f"⚠️ Fresh data invalid (ATR={fresh_atr}), using preset metrics", flush=True)
await update.message.reply_text(
f"⚠️ *Fresh data invalid*\n"
f"⚠️ Fresh data invalid\n"
f"Using preset ATR: {metrics['atr']}\n"
f"Executing {direction.upper()} {symbol_info['label']}...",
parse_mode='Markdown'
f"Executing {direction.upper()} {symbol_info['label']}..."
)
else:
# Timeout - fallback to preset with warning
print(f"⚠️ Timeout waiting for fresh data - using preset metrics: ATR={metrics['atr']}", flush=True)
await update.message.reply_text(
f"⚠️ *Timeout waiting for fresh data*\n"
f"⚠️ Timeout waiting for fresh data\n"
f"Using preset ATR: {metrics['atr']}\n"
f"Executing {direction.upper()} {symbol_info['label']}...",
parse_mode='Markdown'
f"Executing {direction.upper()} {symbol_info['label']}..."
)
# Execute the trade with fresh or fallback metrics
@@ -865,17 +861,22 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
# Parse JSON even for error responses to get detailed error messages
try:
print(f"🔍 Parsing JSON response...", flush=True)
data = response.json()
except Exception:
print(f"✅ JSON parsed successfully", flush=True)
except Exception as e:
print(f"❌ JSON parse error: {e}", flush=True)
await update.message.reply_text(f"❌ Execution error ({response.status_code})")
return
if not data.get('success'):
# CRITICAL: Show detailed error message (may contain "CLOSE POSITION MANUALLY")
message = data.get('message') or data.get('error') or 'Trade rejected'
print(f"❌ Trade failed: {message}", flush=True)
await update.message.reply_text(f"{message}")
return
print(f"✅ Trade success, extracting data...", flush=True)
entry_price = data.get('entryPrice')
notional = data.get('positionSize')
leverage = data.get('leverage')
@@ -902,12 +903,15 @@ async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYP
f"TP1: {tp1_text}\nTP2: {tp2_text}\nSL: {sl_text}"
)
print(f"📤 Sending success message to user...", flush=True)
await update.message.reply_text(success_message)
print(f"✅ Success message sent!", flush=True)
except Exception as exc:
print(f"❌ Manual trade failed: {exc}", flush=True)
await update.message.reply_text(f"❌ Error: {exc}")
async def main():
"""Start the bot"""
@@ -929,7 +933,7 @@ async def main():
# Create application
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
# Add command handlers
# Add handlers
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("status", status_command))
application.add_handler(CommandHandler("close", close_command))
@@ -951,10 +955,7 @@ async def main():
manual_trade_handler,
))
# Initialize the application first
await application.initialize()
# Register bot commands for autocomplete (works in Telegram AND Matrix bridges)
# Set bot commands for autocomplete
commands = [
BotCommand("help", "Show all available commands"),
BotCommand("status", "Show open positions"),
@@ -970,23 +971,24 @@ async def main():
BotCommand("sellfart", "Sell FARTCOIN (shortcut)"),
]
await application.bot.set_my_commands(commands)
print("✅ Bot commands registered for autocomplete (Telegram + Matrix)", flush=True)
print("✅ Bot commands registered for autocomplete", flush=True)
# Start polling
print("\n🤖 Bot ready! Send commands to your Telegram.\n", flush=True)
# Start the bot with proper async pattern
await application.initialize()
await application.start()
await application.updater.start_polling(allowed_updates=Update.ALL_TYPES)
# Run until stopped
# Keep running until stopped
try:
await asyncio.Event().wait()
except (KeyboardInterrupt, SystemExit):
pass
# Cleanup
await application.updater.stop()
await application.stop()
await application.shutdown()
finally:
await application.updater.stop()
await application.stop()
await application.shutdown()
if __name__ == '__main__':
asyncio.run(main())