#!/usr/bin/env python3 import asyncio import logging import sys import os from pathlib import Path from typing import List, Dict, Optional # Add project root to sys.path project_root = Path(__file__).parent sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root / 'trading_system')) sys.path.insert(0, str(project_root / 'backend')) # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger("UpdateSL") # Import system modules try: from trading_system.binance_client import BinanceClient from trading_system.risk_manager import RiskManager from trading_system import config from backend.database.connection import Database from backend.security.crypto import decrypt_str # Ensure config is loaded (defaults updated to 0.5) from trading_system.config import _init_config_manager _init_config_manager() except ImportError as e: logger.error(f"Import failed: {e}") sys.exit(1) async def process_account(account_id, name, api_key, api_secret): logger.info(f"=== Processing Account {account_id}: {name} ===") if not api_key or not api_secret: logger.warning(f"Account {name} missing keys. Skipping.") return # Initialize Client with explicit keys client = BinanceClient(api_key=api_key, api_secret=api_secret) # Connect to Binance try: # Debug proxy settings logger.info(f"Proxy Settings - HTTP: {os.environ.get('HTTP_PROXY')}, HTTPS: {os.environ.get('HTTPS_PROXY')}") # Increase timeout to 60s and retries await client.connect(timeout=60, retries=5) except Exception as e: logger.error(f"Failed to connect to Binance for account {name}: {e}") return # Initialize RiskManager # Note: RiskManager uses global config for ATR settings. # Since we updated config.py default ATR_STOP_LOSS_MULTIPLIER to 0.5, # and account 1 DB config is also 0.5, this is safe. risk_manager = RiskManager(client) try: atr_multiplier = float(config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 0.5)) logger.info(f"Target ATR Multiplier: {atr_multiplier}") # Get Open Positions try: positions = await client.get_open_positions() logger.info(f"Found {len(positions)} open positions.") except Exception as e: logger.error(f"Failed to get positions: {e}") return for position in positions: symbol = position['symbol'] amt = float(position['positionAmt']) if amt == 0: continue # Determine side from amount if amt > 0: real_side = 'BUY' else: real_side = 'SELL' entry_price = float(position['entryPrice']) mark_price = float(position['markPrice']) leverage = int(position['leverage']) quantity = abs(amt) logger.info(f"Processing {symbol} ({real_side}): Entry={entry_price}, Mark={mark_price}, Amt={amt}") # Fetch Klines for ATR try: # Get 1h klines for ATR calculation klines = await client.get_klines(symbol, '1h', limit=100) if not klines: logger.warning(f"{symbol} - Failed to get klines, skipping.") continue new_stop_loss = risk_manager.get_stop_loss_price( entry_price=entry_price, side=real_side, quantity=quantity, leverage=leverage, stop_loss_pct=None, # Force ATR usage klines=klines ) if not new_stop_loss: logger.warning(f"{symbol} - Could not calculate new stop loss.") continue logger.info(f"{symbol} - New ATR Stop Loss Price: {new_stop_loss}") # Check if triggered triggered = False if real_side == 'BUY': if mark_price <= new_stop_loss: triggered = True else: # SELL if mark_price >= new_stop_loss: triggered = True if triggered: logger.warning(f"{symbol} - Price already beyond new stop loss! Executing MARKET CLOSE.") try: await client.place_order( symbol=symbol, side='SELL' if real_side == 'BUY' else 'BUY', type='MARKET', quantity=quantity, reduceOnly=True ) logger.info(f"{symbol} - Market Close Order Placed.") await client.cancel_all_orders(symbol) logger.info(f"{symbol} - Cancelled all open orders.") except Exception as e: logger.error(f"{symbol} - Failed to close position: {e}") else: # Update Stop Loss Order logger.info(f"{symbol} - Updating Stop Loss Order to {new_stop_loss}") # Cancel existing STOP_MARKET orders try: open_orders = await client.get_open_orders(symbol) for order in open_orders: if order['type'] == 'STOP_MARKET': await client.cancel_order(symbol, order['orderId']) logger.info(f"{symbol} - Cancelled old STOP_MARKET order {order['orderId']}") except Exception as e: logger.error(f"{symbol} - Failed to cancel orders: {e}") # Place new STOP_MARKET order try: stop_side = 'SELL' if real_side == 'BUY' else 'BUY' await client.place_order( symbol=symbol, side=stop_side, type='STOP_MARKET', stopPrice=new_stop_loss, quantity=quantity, reduceOnly=True, workingType='MARK_PRICE' ) logger.info(f"{symbol} - Placed new STOP_MARKET order at {new_stop_loss}") except Exception as e: logger.error(f"{symbol} - Failed to place new stop loss: {e}") except Exception as e: logger.error(f"Error processing {symbol}: {e}") import traceback traceback.print_exc() finally: await client.close() async def main(): logger.info("Starting Multi-Account Stop Loss Update Script...") db = Database() with db.get_connection() as conn: with conn.cursor() as cursor: # Get active accounts cursor.execute("SELECT id, name, api_key_enc, api_secret_enc FROM accounts WHERE status='active'") accounts = cursor.fetchall() logger.info(f"Found {len(accounts)} active accounts.") for acc in accounts: # Assuming fetchall returns dicts because Database uses DictCursor usually, # or we check type. backend/database/connection.py ensures DictCursor if possible? # Let's handle both dict and tuple/list just in case, but based on previous tools, it returns dicts. # But wait, previous `check_atr_db` output showed dicts. acc_id = acc['id'] name = acc['name'] api_key_enc = acc['api_key_enc'] api_secret_enc = acc['api_secret_enc'] api_key = decrypt_str(api_key_enc) api_secret = decrypt_str(api_secret_enc) await process_account(acc_id, name, api_key, api_secret) logger.info("All accounts processed.") if __name__ == "__main__": loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(main())