Add TP1/SL consistency check on trade restore
This commit is contained in:
15
.env
15
.env
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
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 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)
|
||||
|
||||
Reference in New Issue
Block a user