215 lines
8.3 KiB
Python
215 lines
8.3 KiB
Python
|
|
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())
|