Add TP1/SL consistency check on trade restore
This commit is contained in:
17
.env
17
.env
@@ -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
|
||||||
@@ -352,4 +352,17 @@ NEW_RELIC_LICENSE_KEY=
|
|||||||
USE_TRAILING_STOP=true
|
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
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -354,16 +371,16 @@ export class PositionManager {
|
|||||||
console.log(
|
console.log(
|
||||||
`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%`
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.saveTradeState(trade)
|
await this.saveTradeState(trade)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Partial fill detected but unclear which TP - just update size
|
// Partial fill detected but unclear which TP - just update size
|
||||||
console.log(`⚠️ Unknown partial fill detected - updating tracked size to $${positionSizeUSD.toFixed(2)}`)
|
console.log(`⚠️ Unknown partial fill detected - updating tracked size to $${positionSizeUSD.toFixed(2)}`)
|
||||||
trade.currentSize = positionSizeUSD
|
trade.currentSize = positionSizeUSD
|
||||||
await this.saveTradeState(trade)
|
await this.saveTradeState(trade)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue monitoring the remaining position
|
// Continue monitoring the remaining position
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
32
scripts/debug-open-trades.mjs
Normal file
32
scripts/debug-open-trades.mjs
Normal 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)
|
||||||
|
})
|
||||||
30
scripts/debug-open-trades.ts
Normal file
30
scripts/debug-open-trades.ts
Normal 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
15
scripts/show-position.ts
Normal 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)
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user