Add TP1/SL consistency check on trade restore

This commit is contained in:
mindesbunister
2025-11-06 12:18:31 +01:00
parent 7c888282ec
commit 6c7eaf5f04
7 changed files with 265 additions and 26 deletions

15
.env
View File

@@ -124,7 +124,7 @@ PROFIT_LOCK_PERCENT=0.6
# Risk limits # Risk limits
# Stop trading if daily loss exceeds this amount (USD) # Stop trading if daily loss exceeds this amount (USD)
# Example: -150 = stop trading after losing $150 in a day # Example: -150 = stop trading after losing $150 in a day
MAX_DAILY_DRAWDOWN=-50 MAX_DAILY_DRAWDOWN=-1000
# Maximum number of trades allowed per hour (prevents overtrading) # Maximum number of trades allowed per hour (prevents overtrading)
MAX_TRADES_PER_HOUR=20 MAX_TRADES_PER_HOUR=20
@@ -353,3 +353,16 @@ USE_TRAILING_STOP=true
TRAILING_STOP_PERCENT=0.3 TRAILING_STOP_PERCENT=0.3
TRAILING_STOP_ACTIVATION=0.4 TRAILING_STOP_ACTIVATION=0.4
MIN_QUALITY_SCORE=60 MIN_QUALITY_SCORE=60
SOLANA_ENABLED=true
SOLANA_POSITION_SIZE=210
SOLANA_LEVERAGE=10
ETHEREUM_ENABLED=false
ETHEREUM_POSITION_SIZE=50
ETHEREUM_LEVERAGE=1
ENABLE_POSITION_SCALING=false
MIN_SCALE_QUALITY_SCORE=75
MIN_PROFIT_FOR_SCALE=0.4
MAX_SCALE_MULTIPLIER=2
SCALE_SIZE_PERCENT=50
MIN_ADX_INCREASE=5
MAX_PRICE_POSITION_FOR_SCALE=70

View File

@@ -146,8 +146,9 @@ export async function POST(request: NextRequest): Promise<NextResponse<RiskCheck
const config = getMergedConfig() const config = getMergedConfig()
// Check for existing positions on the same symbol // Check for existing positions on the same symbol
const positionManager = await getInitializedPositionManager() const positionManager = await getInitializedPositionManager()
const existingTrades = Array.from(positionManager.getActiveTrades().values()) await positionManager.reconcileTrade(body.symbol)
const existingTrades = Array.from(positionManager.getActiveTrades().values())
const existingPosition = existingTrades.find(trade => trade.symbol === body.symbol) const existingPosition = existingTrades.find(trade => trade.symbol === body.symbol)
if (existingPosition) { if (existingPosition) {

View File

@@ -136,6 +136,12 @@ export class PositionManager {
this.activeTrades.set(activeTrade.id, activeTrade) this.activeTrades.set(activeTrade.id, activeTrade)
console.log(`✅ Restored trade: ${activeTrade.symbol} ${activeTrade.direction} at $${activeTrade.entryPrice}`) console.log(`✅ Restored trade: ${activeTrade.symbol} ${activeTrade.direction} at $${activeTrade.entryPrice}`)
// Consistency check: if TP1 hit but SL not moved to breakeven, fix it now
if (activeTrade.tp1Hit && !activeTrade.slMovedToBreakeven) {
console.log(`🔧 Detected inconsistent state: TP1 hit but SL not at breakeven. Fixing now...`)
await this.handlePostTp1Adjustments(activeTrade, 'recovery after restore')
}
} }
if (this.activeTrades.size > 0) { if (this.activeTrades.size > 0) {
@@ -207,6 +213,22 @@ export class PositionManager {
return Array.from(this.activeTrades.values()) return Array.from(this.activeTrades.values())
} }
async reconcileTrade(symbol: string): Promise<void> {
const trade = Array.from(this.activeTrades.values()).find(t => t.symbol === symbol)
if (!trade) {
return
}
try {
const driftService = getDriftService()
const marketConfig = getMarketConfig(symbol)
const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
await this.checkTradeConditions(trade, oraclePrice)
} catch (error) {
console.error(`⚠️ Failed to reconcile trade for ${symbol}:`, error)
}
}
/** /**
* Get specific trade * Get specific trade
*/ */
@@ -322,31 +344,26 @@ export class PositionManager {
// Position exists - check if size changed (TP1/TP2 filled) // Position exists - check if size changed (TP1/TP2 filled)
const positionSizeUSD = position.size * currentPrice const positionSizeUSD = position.size * currentPrice
const trackedSizeUSD = trade.currentSize const trackedSizeUSD = trade.currentSize
const sizeDiffPercent = Math.abs(positionSizeUSD - trackedSizeUSD) / Math.max(trackedSizeUSD, 1) * 100 const sizeDiffPercent = Math.abs(positionSizeUSD - trackedSizeUSD) / trackedSizeUSD * 100
const reductionUSD = trackedSizeUSD - positionSizeUSD
const reductionPercentOfOriginal = trade.positionSize > 0
? (reductionUSD / trade.positionSize) * 100
: 0
const reductionPercentOfTracked = trackedSizeUSD > 0
? (reductionUSD / trackedSizeUSD) * 100
: 0
// If position size reduced significantly, TP orders likely filled // If position size reduced significantly, TP orders likely filled
if (positionSizeUSD < trackedSizeUSD * 0.9 && sizeDiffPercent > 10) { if (positionSizeUSD < trackedSizeUSD * 0.9 && sizeDiffPercent > 10) {
console.log(`📊 Position size changed: tracking $${trackedSizeUSD.toFixed(2)} but found $${positionSizeUSD.toFixed(2)}`) console.log(`📊 Position size changed: tracking $${trackedSizeUSD.toFixed(2)} but found $${positionSizeUSD.toFixed(2)}`)
if (!trade.tp1Hit && reductionPercentOfOriginal >= (this.config.takeProfit1SizePercent * 0.8)) { // Detect which TP filled based on size reduction
const reductionPercent = ((trackedSizeUSD - positionSizeUSD) / trade.positionSize) * 100
if (!trade.tp1Hit && reductionPercent >= (this.config.takeProfit1SizePercent * 0.8)) {
// TP1 fired (should be ~75% reduction) // TP1 fired (should be ~75% reduction)
console.log(`🎯 TP1 detected as filled! Reduction: ${reductionPercentOfOriginal.toFixed(1)}% of original`) console.log(`🎯 TP1 detected as filled! Reduction: ${reductionPercent.toFixed(1)}%`)
trade.tp1Hit = true trade.tp1Hit = true
trade.currentSize = positionSizeUSD trade.currentSize = positionSizeUSD
await this.handlePostTp1Adjustments(trade, 'on-chain TP1 detection') await this.handlePostTp1Adjustments(trade, 'on-chain TP1 detection')
} else if (trade.tp1Hit && !trade.tp2Hit && reductionPercentOfTracked >= (this.config.takeProfit2SizePercent * 0.8)) { } else if (trade.tp1Hit && !trade.tp2Hit && reductionPercent >= 85) {
// TP2 fired (should clear remaining runner allocation) // TP2 fired (total should be ~95% closed, 5% runner left)
console.log(`🎯 TP2 detected as filled! Reduction: ${reductionPercentOfTracked.toFixed(1)}% of remaining`) console.log(`🎯 TP2 detected as filled! Reduction: ${reductionPercent.toFixed(1)}%`)
trade.tp2Hit = true trade.tp2Hit = true
trade.currentSize = positionSizeUSD trade.currentSize = positionSizeUSD
trade.trailingStopActive = true trade.trailingStopActive = true

View File

@@ -0,0 +1,32 @@
import { getPrismaClient } from '../lib/database/trades'
async function main() {
const prisma = getPrismaClient()
const openTrades = await prisma.trade.findMany({
where: { status: { not: 'closed' } },
orderBy: { entryTime: 'desc' },
take: 10,
})
console.log(
openTrades.map(t => ({
id: t.id,
symbol: t.symbol,
direction: t.direction,
status: t.status,
entryPrice: t.entryPrice,
exitPrice: t.exitPrice,
realizedPnL: t.realizedPnL,
exitReason: t.exitReason,
entryTime: t.entryTime,
exitTime: t.exitTime,
}))
)
process.exit(0)
}
main().catch(err => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,30 @@
import { getPrismaClient } from '../lib/database/trades'
async function main() {
const prisma = getPrismaClient()
const openTrades = await prisma.trade.findMany({
where: { status: { not: 'closed' } },
orderBy: { entryTime: 'desc' },
take: 10,
})
console.log('Open trades:', openTrades.map(t => ({
id: t.id,
symbol: t.symbol,
direction: t.direction,
status: t.status,
entryPrice: t.entryPrice,
exitPrice: t.exitPrice,
exitReason: t.exitReason,
realizedPnL: t.realizedPnL,
createdAt: t.entryTime,
exitTime: t.exitTime,
})))
process.exit(0)
}
main().catch(err => {
console.error(err)
process.exit(1)
})

15
scripts/show-position.ts Normal file
View File

@@ -0,0 +1,15 @@
import { initializeDriftService, getDriftService } from '../lib/drift/client'
import { getMarketConfig } from '../config/trading'
async function main() {
await initializeDriftService()
const service = getDriftService()
const market = getMarketConfig('SOL-PERP')
const position = await service.getPosition(market.driftMarketIndex)
console.log('Position:', position)
}
main().catch(err => {
console.error(err)
process.exit(1)
})

View File

@@ -6,7 +6,7 @@ Only responds to YOUR commands in YOUR chat
import os import os
import requests import requests
from telegram import Update from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
# Configuration # Configuration
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
@@ -15,6 +15,38 @@ TRADING_BOT_URL = os.getenv('TRADING_BOT_URL', 'http://trading-bot-v4:3000')
API_SECRET_KEY = os.getenv('API_SECRET_KEY', '') API_SECRET_KEY = os.getenv('API_SECRET_KEY', '')
ALLOWED_CHAT_ID = int(os.getenv('TELEGRAM_CHAT_ID', '579304651')) ALLOWED_CHAT_ID = int(os.getenv('TELEGRAM_CHAT_ID', '579304651'))
SYMBOL_MAP = {
'sol': {
'tradingview': 'SOLUSDT',
'label': 'SOL'
},
'eth': {
'tradingview': 'ETHUSDT',
'label': 'ETH'
},
'btc': {
'tradingview': 'BTCUSDT',
'label': 'BTC'
},
}
MANUAL_METRICS = {
'long': {
'atr': 0.45,
'adx': 32,
'rsi': 58,
'volumeRatio': 1.25,
'pricePosition': 55,
},
'short': {
'atr': 0.45,
'adx': 32,
'rsi': 42,
'volumeRatio': 1.25,
'pricePosition': 45,
},
}
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /status command - show current open positions""" """Handle /status command - show current open positions"""
@@ -496,6 +528,100 @@ async def trade_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
print(f"❌ Error: {e}", flush=True) print(f"❌ Error: {e}", flush=True)
await update.message.reply_text(f"❌ Error: {str(e)}") await update.message.reply_text(f"❌ Error: {str(e)}")
async def manual_trade_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Execute manual long/short commands sent as plain text."""
if update.message is None:
return
if update.message.chat_id != ALLOWED_CHAT_ID:
return
text = update.message.text.strip().lower()
parts = text.split()
if len(parts) != 2:
return
direction, symbol_key = parts[0], parts[1]
if direction not in ('long', 'short'):
return
symbol_info = SYMBOL_MAP.get(symbol_key)
if not symbol_info:
return
metrics = MANUAL_METRICS[direction]
payload = {
'symbol': symbol_info['tradingview'],
'direction': direction,
'timeframe': 'manual',
'signalStrength': 'manual',
'atr': metrics['atr'],
'adx': metrics['adx'],
'rsi': metrics['rsi'],
'volumeRatio': metrics['volumeRatio'],
'pricePosition': metrics['pricePosition'],
}
try:
print(f"🚀 Manual trade: {direction.upper()} {symbol_info['label']}", flush=True)
response = requests.post(
f"{TRADING_BOT_URL}/api/trading/execute",
headers={'Authorization': f'Bearer {API_SECRET_KEY}'},
json=payload,
timeout=30,
)
print(f"📥 Manual trade response: {response.status_code}", flush=True)
if not response.ok:
await update.message.reply_text(
f"❌ Execution error ({response.status_code})"
)
return
data = response.json()
if not data.get('success'):
message = data.get('message') or data.get('error') or 'Trade rejected'
await update.message.reply_text(f"{message}")
return
entry_price = data.get('entryPrice')
notional = data.get('positionSize')
leverage = data.get('leverage')
tp1 = data.get('takeProfit1')
tp2 = data.get('takeProfit2')
sl = data.get('stopLoss')
entry_text = f"${entry_price:.4f}" if entry_price is not None else 'n/a'
size_text = (
f"${notional:.2f} @ {leverage}x"
if notional is not None and leverage is not None
else 'n/a'
)
tp1_text = f"${tp1:.4f}" if tp1 is not None else 'n/a'
tp2_text = f"${tp2:.4f}" if tp2 is not None else 'n/a'
sl_text = f"${sl:.4f}" if sl is not None else 'n/a'
success_message = (
f"✅ OPENED {direction.upper()} {symbol_info['label']}\n"
f"Entry: {entry_text}\n"
f"Size: {size_text}\n"
f"TP1: {tp1_text}\nTP2: {tp2_text}\nSL: {sl_text}"
)
await update.message.reply_text(success_message)
except Exception as exc:
print(f"❌ Manual trade failed: {exc}", flush=True)
await update.message.reply_text(f"❌ Error: {exc}")
def main(): def main():
"""Start the bot""" """Start the bot"""
@@ -511,6 +637,7 @@ def main():
print(f" /buySOL, /sellSOL", flush=True) print(f" /buySOL, /sellSOL", flush=True)
print(f" /buyBTC, /sellBTC", flush=True) print(f" /buyBTC, /sellBTC", flush=True)
print(f" /buyETH, /sellETH", flush=True) print(f" /buyETH, /sellETH", flush=True)
print(f" long sol | short btc (plain text)", flush=True)
# Create application # Create application
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
@@ -527,6 +654,10 @@ def main():
application.add_handler(CommandHandler("sellBTC", trade_command)) application.add_handler(CommandHandler("sellBTC", trade_command))
application.add_handler(CommandHandler("buyETH", trade_command)) application.add_handler(CommandHandler("buyETH", trade_command))
application.add_handler(CommandHandler("sellETH", trade_command)) application.add_handler(CommandHandler("sellETH", trade_command))
application.add_handler(MessageHandler(
filters.TEXT & (~filters.COMMAND),
manual_trade_handler,
))
# Start polling # Start polling
print("\n🤖 Bot ready! Send commands to your Telegram.\n", flush=True) print("\n🤖 Bot ready! Send commands to your Telegram.\n", flush=True)