diff --git a/.env b/.env index 65379c5..4820901 100644 --- a/.env +++ b/.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 @@ -352,4 +352,17 @@ NEW_RELIC_LICENSE_KEY= USE_TRAILING_STOP=true TRAILING_STOP_PERCENT=0.3 TRAILING_STOP_ACTIVATION=0.4 -MIN_QUALITY_SCORE=60 \ No newline at end of file +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 \ No newline at end of file diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index 879d38a..036f3af 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -146,8 +146,9 @@ export async function POST(request: NextRequest): Promise trade.symbol === body.symbol) if (existingPosition) { diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 2599923..e65a447 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -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 { + 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 @@ -354,16 +371,16 @@ export class PositionManager { console.log( `šŸƒ Runner active: $${positionSizeUSD.toFixed(2)} with trailing buffer ${trade.runnerTrailingPercent?.toFixed(3)}%` ) - + await this.saveTradeState(trade) - + } else { // Partial fill detected but unclear which TP - just update size console.log(`āš ļø Unknown partial fill detected - updating tracked size to $${positionSizeUSD.toFixed(2)}`) trade.currentSize = positionSizeUSD await this.saveTradeState(trade) } - + // Continue monitoring the remaining position return } diff --git a/scripts/debug-open-trades.mjs b/scripts/debug-open-trades.mjs new file mode 100644 index 0000000..de77d49 --- /dev/null +++ b/scripts/debug-open-trades.mjs @@ -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) +}) diff --git a/scripts/debug-open-trades.ts b/scripts/debug-open-trades.ts new file mode 100644 index 0000000..ebe227c --- /dev/null +++ b/scripts/debug-open-trades.ts @@ -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) +}) diff --git a/scripts/show-position.ts b/scripts/show-position.ts new file mode 100644 index 0000000..b69b735 --- /dev/null +++ b/scripts/show-position.ts @@ -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) +}) diff --git a/telegram_command_bot.py b/telegram_command_bot.py index 8586ef6..b5614f8 100644 --- a/telegram_command_bot.py +++ b/telegram_command_bot.py @@ -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)