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
# Stop trading if daily loss exceeds this amount (USD)
# 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)
MAX_TRADES_PER_HOUR=20
@@ -353,3 +353,16 @@ USE_TRAILING_STOP=true
TRAILING_STOP_PERCENT=0.3
TRAILING_STOP_ACTIVATION=0.4
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()
// Check for existing positions on the same symbol
const positionManager = await getInitializedPositionManager()
const existingTrades = Array.from(positionManager.getActiveTrades().values())
const positionManager = await getInitializedPositionManager()
await positionManager.reconcileTrade(body.symbol)
const existingTrades = Array.from(positionManager.getActiveTrades().values())
const existingPosition = existingTrades.find(trade => trade.symbol === body.symbol)
if (existingPosition) {

View File

@@ -136,6 +136,12 @@ export class PositionManager {
this.activeTrades.set(activeTrade.id, activeTrade)
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) {
@@ -207,6 +213,22 @@ export class PositionManager {
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
*/
@@ -322,31 +344,26 @@ export class PositionManager {
// Position exists - check if size changed (TP1/TP2 filled)
const positionSizeUSD = position.size * currentPrice
const trackedSizeUSD = trade.currentSize
const sizeDiffPercent = Math.abs(positionSizeUSD - trackedSizeUSD) / Math.max(trackedSizeUSD, 1) * 100
const reductionUSD = trackedSizeUSD - positionSizeUSD
const reductionPercentOfOriginal = trade.positionSize > 0
? (reductionUSD / trade.positionSize) * 100
: 0
const reductionPercentOfTracked = trackedSizeUSD > 0
? (reductionUSD / trackedSizeUSD) * 100
: 0
const sizeDiffPercent = Math.abs(positionSizeUSD - trackedSizeUSD) / trackedSizeUSD * 100
// If position size reduced significantly, TP orders likely filled
if (positionSizeUSD < trackedSizeUSD * 0.9 && sizeDiffPercent > 10) {
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)
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.currentSize = positionSizeUSD
await this.handlePostTp1Adjustments(trade, 'on-chain TP1 detection')
} else if (trade.tp1Hit && !trade.tp2Hit && reductionPercentOfTracked >= (this.config.takeProfit2SizePercent * 0.8)) {
// TP2 fired (should clear remaining runner allocation)
console.log(`🎯 TP2 detected as filled! Reduction: ${reductionPercentOfTracked.toFixed(1)}% of remaining`)
} else if (trade.tp1Hit && !trade.tp2Hit && reductionPercent >= 85) {
// TP2 fired (total should be ~95% closed, 5% runner left)
console.log(`🎯 TP2 detected as filled! Reduction: ${reductionPercent.toFixed(1)}%`)
trade.tp2Hit = true
trade.currentSize = positionSizeUSD
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 requests
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
# Configuration
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', '')
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):
"""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)
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():
"""Start the bot"""
@@ -511,6 +637,7 @@ def main():
print(f" /buySOL, /sellSOL", flush=True)
print(f" /buyBTC, /sellBTC", flush=True)
print(f" /buyETH, /sellETH", flush=True)
print(f" long sol | short btc (plain text)", flush=True)
# Create application
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("buyETH", trade_command))
application.add_handler(CommandHandler("sellETH", trade_command))
application.add_handler(MessageHandler(
filters.TEXT & (~filters.COMMAND),
manual_trade_handler,
))
# Start polling
print("\n🤖 Bot ready! Send commands to your Telegram.\n", flush=True)