3032 lines
132 KiB
Python
3032 lines
132 KiB
Python
|
|
import os
|
||
|
|
import time
|
||
|
|
import threading
|
||
|
|
import logging
|
||
|
|
import pandas as pd
|
||
|
|
import numpy as np
|
||
|
|
import requests
|
||
|
|
import json
|
||
|
|
import hashlib
|
||
|
|
import hmac
|
||
|
|
import base64
|
||
|
|
import schedule
|
||
|
|
import okx.Account as Account
|
||
|
|
import okx.MarketData as MarketData
|
||
|
|
import okx.Trade as Trade
|
||
|
|
from datetime import datetime, timezone, timedelta
|
||
|
|
from dotenv import load_dotenv
|
||
|
|
from logging.handlers import RotatingFileHandler
|
||
|
|
from database_manager import DatabaseManager
|
||
|
|
import atexit
|
||
|
|
import signal
|
||
|
|
|
||
|
|
tdmode = 'cross'
|
||
|
|
|
||
|
|
# Load environment variables
|
||
|
|
load_dotenv()
|
||
|
|
|
||
|
|
def setup_logging():
|
||
|
|
"""Configure logging system"""
|
||
|
|
# Set environment variable to ensure UTF-8 encoding
|
||
|
|
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||
|
|
|
||
|
|
# Create formatter
|
||
|
|
formatter = logging.Formatter(
|
||
|
|
'%(asctime)s - %(levelname)s - %(message)s',
|
||
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||
|
|
)
|
||
|
|
|
||
|
|
# File handler - using UTF-8 encoding
|
||
|
|
file_handler = RotatingFileHandler(
|
||
|
|
'trading_system.log',
|
||
|
|
maxBytes=10 * 1024 * 1024,
|
||
|
|
backupCount=5,
|
||
|
|
encoding='utf-8'
|
||
|
|
)
|
||
|
|
file_handler.setFormatter(formatter)
|
||
|
|
file_handler.setLevel(logging.INFO)
|
||
|
|
|
||
|
|
# Console handler - handle encoding issues
|
||
|
|
class SafeStreamHandler(logging.StreamHandler):
|
||
|
|
def emit(self, record):
|
||
|
|
try:
|
||
|
|
msg = self.format(record)
|
||
|
|
stream = self.stream
|
||
|
|
if hasattr(stream, 'buffer'):
|
||
|
|
stream.buffer.write(msg.encode('utf-8') + b'\n')
|
||
|
|
stream.buffer.flush()
|
||
|
|
else:
|
||
|
|
stream.write(msg + self.terminator)
|
||
|
|
self.flush()
|
||
|
|
except UnicodeEncodeError:
|
||
|
|
safe_msg = msg.encode('ascii', 'ignore').decode('ascii')
|
||
|
|
stream.write(safe_msg + self.terminator)
|
||
|
|
self.flush()
|
||
|
|
except Exception:
|
||
|
|
self.handleError(record)
|
||
|
|
|
||
|
|
console_handler = SafeStreamHandler()
|
||
|
|
console_handler.setFormatter(formatter)
|
||
|
|
console_handler.setLevel(logging.INFO)
|
||
|
|
|
||
|
|
# Configure root logger
|
||
|
|
logging.basicConfig(
|
||
|
|
level=logging.INFO,
|
||
|
|
handlers=[file_handler, console_handler]
|
||
|
|
)
|
||
|
|
|
||
|
|
return logging.getLogger(__name__)
|
||
|
|
|
||
|
|
logger = setup_logging()
|
||
|
|
|
||
|
|
class APIRateLimiter:
|
||
|
|
"""API rate limiter"""
|
||
|
|
|
||
|
|
def __init__(self, calls_per_second=2):
|
||
|
|
self.min_interval = 1.0 / calls_per_second
|
||
|
|
self.last_call = {}
|
||
|
|
self.lock = threading.Lock()
|
||
|
|
|
||
|
|
def wait(self, api_name):
|
||
|
|
"""Wait until API can be called"""
|
||
|
|
with self.lock:
|
||
|
|
current_time = time.time()
|
||
|
|
if api_name in self.last_call:
|
||
|
|
elapsed = current_time - self.last_call[api_name]
|
||
|
|
if elapsed < self.min_interval:
|
||
|
|
time.sleep(self.min_interval - elapsed)
|
||
|
|
self.last_call[api_name] = current_time
|
||
|
|
|
||
|
|
class OKXAPIClient:
|
||
|
|
"""OKX API Client (using official SDK)"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.api_key = os.getenv('OKX_API_KEY')
|
||
|
|
self.secret_key = os.getenv('OKX_SECRET_KEY')
|
||
|
|
self.password = os.getenv('OKX_PASSWORD')
|
||
|
|
|
||
|
|
if not all([self.api_key, self.secret_key, self.password]):
|
||
|
|
raise ValueError("Please set OKX API key and password")
|
||
|
|
|
||
|
|
# Initialize OKX SDK client - using live trading environment
|
||
|
|
Flag = "1" # Live trading environment
|
||
|
|
self.account_api = Account.AccountAPI(self.api_key, self.secret_key, self.password, False, Flag)
|
||
|
|
self.market_api = MarketData.MarketAPI(self.api_key, self.secret_key, self.password, False, Flag)
|
||
|
|
self.trade_api = Trade.TradeAPI(self.api_key, self.secret_key, self.password, False, Flag)
|
||
|
|
|
||
|
|
# API rate limiting
|
||
|
|
self.rate_limiter = APIRateLimiter(2)
|
||
|
|
self.log_http = False
|
||
|
|
|
||
|
|
# Cache instrument info
|
||
|
|
self.instrument_cache = {}
|
||
|
|
|
||
|
|
def get_market_data(self, symbol, timeframe='1H', limit=200):
|
||
|
|
"""Get market data"""
|
||
|
|
self.rate_limiter.wait("market_data")
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = self.market_api.get_candlesticks(
|
||
|
|
instId=symbol,
|
||
|
|
bar=timeframe,
|
||
|
|
limit=str(limit)
|
||
|
|
)
|
||
|
|
|
||
|
|
if self.log_http:
|
||
|
|
logger.debug(f"HTTP Request: GET {symbol} {timeframe} {result.get('code', 'Unknown')}")
|
||
|
|
|
||
|
|
# Error checking
|
||
|
|
if 'code' not in result or result['code'] != '0':
|
||
|
|
error_msg = result.get('msg', 'Unknown error')
|
||
|
|
error_code = result.get('code', 'Unknown code')
|
||
|
|
logger.error(f"Failed to get {symbol} market data: {error_msg} (code: {error_code})")
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Check if data exists
|
||
|
|
if 'data' not in result or not result['data']:
|
||
|
|
logger.warning(f"{symbol} market data is empty")
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Create DataFrame
|
||
|
|
columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote', 'confirm']
|
||
|
|
df = pd.DataFrame(result['data'], columns=columns)
|
||
|
|
|
||
|
|
# Convert data types
|
||
|
|
df['timestamp'] = pd.to_datetime(df['timestamp'].astype(np.int64), unit='ms')
|
||
|
|
numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote']
|
||
|
|
df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric)
|
||
|
|
|
||
|
|
return df.sort_values('timestamp')
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting market data: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_account_balance(self):
|
||
|
|
"""Get account balance (USDT)"""
|
||
|
|
self.rate_limiter.wait("balance")
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = self.account_api.get_account_balance()
|
||
|
|
|
||
|
|
if self.log_http:
|
||
|
|
logger.debug(f"HTTP Request: GET balance {result['code']}")
|
||
|
|
|
||
|
|
# Correction: code "0" indicates success
|
||
|
|
if result['code'] != '0':
|
||
|
|
logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})")
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Extract USDT balance
|
||
|
|
for currency in result['data'][0]['details']:
|
||
|
|
if currency['ccy'] == 'USDT':
|
||
|
|
return float(currency['availBal'])
|
||
|
|
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting balance: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_currency_balances(self):
|
||
|
|
"""Get all currency balances"""
|
||
|
|
self.rate_limiter.wait("balances")
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = self.account_api.get_account_balance()
|
||
|
|
|
||
|
|
# Correction: code "0" indicates success
|
||
|
|
if result['code'] != '0':
|
||
|
|
logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
# Check response data structure
|
||
|
|
if not result['data'] or len(result['data']) == 0:
|
||
|
|
logger.error("No balance data in API response")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
# Check if details field exists
|
||
|
|
if 'details' not in result['data'][0]:
|
||
|
|
logger.error("No details field in API response")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
balances = {}
|
||
|
|
for currency in result['data'][0]['details']:
|
||
|
|
if float(currency.get('availBal', 0)) > 0:
|
||
|
|
balances[currency['ccy']] = {
|
||
|
|
'amount': float(currency.get('availBal', 0)),
|
||
|
|
'frozen': float(currency.get('frozenBal', 0))
|
||
|
|
}
|
||
|
|
|
||
|
|
return balances
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting currency balances: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def get_positions(self):
|
||
|
|
"""Get exchange position information (based on currency balances)"""
|
||
|
|
try:
|
||
|
|
# Get all currency balances
|
||
|
|
balances = self.get_currency_balances()
|
||
|
|
if not balances:
|
||
|
|
return {}
|
||
|
|
|
||
|
|
# Filter out non-USDT currencies as positions
|
||
|
|
positions = {}
|
||
|
|
for currency, balance in balances.items():
|
||
|
|
if currency != 'USDT' and balance['amount'] > 0:
|
||
|
|
# Construct trading pair symbol
|
||
|
|
symbol = f"{currency}-USDT"
|
||
|
|
# Get current price to calculate position value
|
||
|
|
current_price = self.get_current_price(symbol)
|
||
|
|
if current_price:
|
||
|
|
positions[symbol] = {
|
||
|
|
'amount': balance['amount'],
|
||
|
|
'value': balance['amount'] * current_price,
|
||
|
|
'avg_price': 0.0 # Spot positions don't have average price concept
|
||
|
|
}
|
||
|
|
else:
|
||
|
|
positions[symbol] = {
|
||
|
|
'amount': balance['amount'],
|
||
|
|
'value': 0.0,
|
||
|
|
'avg_price': 0.0
|
||
|
|
}
|
||
|
|
|
||
|
|
return positions
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting positions: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def get_current_price(self, symbol):
|
||
|
|
"""Get current price"""
|
||
|
|
self.rate_limiter.wait("price")
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = self.market_api.get_ticker(instId=symbol)
|
||
|
|
|
||
|
|
# Correction: code "0" indicates success
|
||
|
|
if result['code'] != '0':
|
||
|
|
logger.error(f"Failed to get price: {result['msg']} (code: {result['code']})")
|
||
|
|
return None
|
||
|
|
|
||
|
|
return float(result['data'][0]['last'])
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting price: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_instrument_info(self, symbol):
|
||
|
|
"""Get trading pair information"""
|
||
|
|
if symbol in self.instrument_cache:
|
||
|
|
return self.instrument_cache[symbol]
|
||
|
|
|
||
|
|
self.rate_limiter.wait("instrument")
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = self.account_api.get_instruments(instType='SPOT')
|
||
|
|
|
||
|
|
if result['code'] != '0':
|
||
|
|
logger.error(f"Failed to get instrument: {result['msg']} (code: {result['code']})")
|
||
|
|
return None, None
|
||
|
|
|
||
|
|
# Find specified trading pair
|
||
|
|
for inst in result['data']:
|
||
|
|
if inst['instId'] == symbol:
|
||
|
|
min_sz = float(inst['minSz'])
|
||
|
|
lot_sz = float(inst['lotSz'])
|
||
|
|
logger.debug(f"Got {symbol} precision: minSz={min_sz}, lotSz={lot_sz}")
|
||
|
|
self.instrument_cache[symbol] = (min_sz, lot_sz)
|
||
|
|
return min_sz, lot_sz
|
||
|
|
|
||
|
|
logger.error(f"Trading pair not found: {symbol}")
|
||
|
|
return None, None
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting instrument info: {e}")
|
||
|
|
return None, None
|
||
|
|
|
||
|
|
def get_default_min_size(self, symbol):
|
||
|
|
"""Get default minimum order size"""
|
||
|
|
# Set default minimum order size based on currency
|
||
|
|
defaults = {
|
||
|
|
'BTC-USDT': 0.0001,
|
||
|
|
'ETH-USDT': 0.001,
|
||
|
|
'SOL-USDT': 0.01,
|
||
|
|
'XRP-USDT': 1.0
|
||
|
|
}
|
||
|
|
return defaults.get(symbol, 0.01) # Default 0.01
|
||
|
|
|
||
|
|
def create_order(self, symbol, side, amount, retries=3):
|
||
|
|
"""Create order"""
|
||
|
|
for attempt in range(retries):
|
||
|
|
try:
|
||
|
|
self.rate_limiter.wait("order")
|
||
|
|
|
||
|
|
# Parse trading pair symbol
|
||
|
|
parts = symbol.split('-')
|
||
|
|
if len(parts) != 2:
|
||
|
|
logger.error(f"Invalid trading pair format: {symbol}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
base_currency, quote_currency = parts
|
||
|
|
|
||
|
|
# Adjust parameters based on buy/sell direction
|
||
|
|
if side == 'buy':
|
||
|
|
# When buying, amount is quote currency amount (USDT amount)
|
||
|
|
# Use amount-based order placement
|
||
|
|
order_params = {
|
||
|
|
'instId': symbol,
|
||
|
|
'tdMode': tdmode,
|
||
|
|
'side': 'buy',
|
||
|
|
'ordType': 'market',
|
||
|
|
'sz': str(amount), # Quote currency amount
|
||
|
|
'tgtCcy': 'quote_ccy' # Specify sz as quote currency
|
||
|
|
}
|
||
|
|
logger.info(f"[{symbol}] Create buy order: amount={amount:.2f} {quote_currency}")
|
||
|
|
|
||
|
|
else:
|
||
|
|
# When selling, amount is base currency quantity
|
||
|
|
# Get precision info and adjust quantity
|
||
|
|
min_sz, lot_sz = self.get_instrument_info(symbol)
|
||
|
|
if min_sz is None:
|
||
|
|
min_sz = self.get_default_min_size(symbol)
|
||
|
|
if lot_sz is None:
|
||
|
|
lot_sz = min_sz
|
||
|
|
|
||
|
|
# Adjust quantity to appropriate precision
|
||
|
|
if lot_sz > 0:
|
||
|
|
amount = (amount / lot_sz) * lot_sz
|
||
|
|
|
||
|
|
amount_str = f"{amount:.10f}"
|
||
|
|
|
||
|
|
order_params = {
|
||
|
|
'instId': symbol,
|
||
|
|
'tdMode': tdmode,
|
||
|
|
'side': 'sell',
|
||
|
|
'ordType': 'market',
|
||
|
|
'sz': amount_str # Base currency quantity
|
||
|
|
}
|
||
|
|
logger.info(f"[{symbol}] Create sell order: quantity={amount_str} {base_currency}")
|
||
|
|
|
||
|
|
# Use SDK to create order
|
||
|
|
result = self.trade_api.place_order(**order_params)
|
||
|
|
|
||
|
|
if self.log_http:
|
||
|
|
logger.debug(f"HTTP Request: POST create order {result['code']}")
|
||
|
|
|
||
|
|
# Check API response
|
||
|
|
if result['code'] != '0':
|
||
|
|
logger.error(f"Failed to create order: {result['msg']} (code: {result['code']})")
|
||
|
|
if 'data' in result and len(result['data']) > 0:
|
||
|
|
for item in result['data']:
|
||
|
|
logger.error(f"Detailed error: {item.get('sMsg', 'Unknown')} (sCode: {item.get('sCode', 'Unknown')})")
|
||
|
|
# Specific error handling
|
||
|
|
if result['code'] == '50113': # Insufficient permissions
|
||
|
|
logger.error("API key may not have trading permissions, please check API key settings")
|
||
|
|
elif result['code'] == '51020': # Minimum order amount
|
||
|
|
logger.error("Order amount below exchange minimum requirement")
|
||
|
|
if attempt < retries - 1:
|
||
|
|
wait_time = 2 ** attempt
|
||
|
|
time.sleep(wait_time)
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Check order status
|
||
|
|
if len(result['data']) > 0:
|
||
|
|
order_data = result['data'][0]
|
||
|
|
if order_data.get('sCode') != '0':
|
||
|
|
logger.error(f"Order creation failed: {order_data.get('sMsg', 'Unknown error')} (sCode: {order_data.get('sCode', 'Unknown')})")
|
||
|
|
if attempt < retries - 1:
|
||
|
|
wait_time = 2 ** attempt
|
||
|
|
time.sleep(wait_time)
|
||
|
|
continue
|
||
|
|
|
||
|
|
order_id = order_data.get('ordId')
|
||
|
|
if order_id:
|
||
|
|
logger.info(f"Order created successfully: {order_id}")
|
||
|
|
return order_id
|
||
|
|
else:
|
||
|
|
logger.error("Order ID is empty")
|
||
|
|
if attempt < retries - 1:
|
||
|
|
wait_time = 2 ** attempt
|
||
|
|
time.sleep(wait_time)
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
logger.error("No order data in API response")
|
||
|
|
if attempt < retries - 1:
|
||
|
|
wait_time = 2 ** attempt
|
||
|
|
time.sleep(wait_time)
|
||
|
|
continue
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error creating order (attempt {attempt+1}/{retries}): {str(e)}")
|
||
|
|
if attempt < retries - 1:
|
||
|
|
wait_time = 2 ** attempt
|
||
|
|
time.sleep(wait_time)
|
||
|
|
else:
|
||
|
|
return None
|
||
|
|
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_order_status(self, symbol, order_id):
|
||
|
|
"""Get order status"""
|
||
|
|
self.rate_limiter.wait("order_status")
|
||
|
|
|
||
|
|
try:
|
||
|
|
result = self.trade_api.get_order(instId=symbol, ordId=order_id)
|
||
|
|
|
||
|
|
# Correction: code "0" indicates success
|
||
|
|
if result['code'] != '0':
|
||
|
|
logger.error(f"Failed to get order status: {result['msg']} (code: {result['code']})")
|
||
|
|
return None
|
||
|
|
|
||
|
|
if len(result['data']) > 0:
|
||
|
|
order_data = result['data'][0]
|
||
|
|
return {
|
||
|
|
'state': order_data.get('state'),
|
||
|
|
'avgPx': float(order_data.get('avgPx', 0)),
|
||
|
|
'accFillSz': float(order_data.get('accFillSz', 0)),
|
||
|
|
'fillPx': float(order_data.get('fillPx', 0)),
|
||
|
|
'fillSz': float(order_data.get('fillSz', 0)),
|
||
|
|
'fillTime': order_data.get('fillTime')
|
||
|
|
}
|
||
|
|
else:
|
||
|
|
logger.error("No order data in API response")
|
||
|
|
return None
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting order status: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def wait_for_order_completion(self, symbol, order_id, max_attempts=10, interval=1):
|
||
|
|
"""Wait for order completion"""
|
||
|
|
for attempt in range(max_attempts):
|
||
|
|
order_status = self.get_order_status(symbol, order_id)
|
||
|
|
if order_status is None:
|
||
|
|
return None
|
||
|
|
|
||
|
|
state = order_status['state']
|
||
|
|
if state == 'filled':
|
||
|
|
logger.info(f"Order completed: {order_id}, fill price={order_status['avgPx']:.2f}, fill quantity={order_status['accFillSz']:.10f}")
|
||
|
|
return order_status
|
||
|
|
elif state == 'canceled':
|
||
|
|
logger.warning(f"Order canceled: {order_id}")
|
||
|
|
return None
|
||
|
|
elif state == 'partially_filled':
|
||
|
|
logger.info(f"Order partially filled: {order_id}, filled={order_status['accFillSz']:.10f}")
|
||
|
|
time.sleep(interval)
|
||
|
|
else:
|
||
|
|
logger.info(f"Order status: {state}, waiting...")
|
||
|
|
time.sleep(interval)
|
||
|
|
|
||
|
|
logger.warning(f"Order not completed within specified time: {order_id}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
class DeepSeekAnalyzer:
|
||
|
|
"""DeepSeek AI Analyzer (KEN2.0 optimized)"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.api_key = os.getenv('DEEPSEEK_API_KEY')
|
||
|
|
self.api_url = 'https://api.deepseek.com/v1/chat/completions'
|
||
|
|
|
||
|
|
def analyze_market(self, symbol, market_data, technical_indicators):
|
||
|
|
"""Use DeepSeek to analyze market (optimized version)"""
|
||
|
|
if not self.api_key:
|
||
|
|
logger.warning("DeepSeek API key not set, skipping AI analysis")
|
||
|
|
return None
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Prepare analysis data
|
||
|
|
latest = market_data.iloc[-1]
|
||
|
|
technical_summary = self._prepare_enhanced_technical_summary(technical_indicators, market_data)
|
||
|
|
|
||
|
|
prompt = self._create_enhanced_analysis_prompt(symbol, latest, market_data, technical_summary)
|
||
|
|
response = self._call_deepseek_api(prompt)
|
||
|
|
|
||
|
|
return self._parse_deepseek_response(response)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"DeepSeek analysis error: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def _prepare_enhanced_technical_summary(self, indicators, market_data):
|
||
|
|
"""Prepare enhanced technical indicator summary"""
|
||
|
|
summary = []
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Price information
|
||
|
|
current_price = indicators.get('current_price', 0)
|
||
|
|
summary.append(f"Current price: ${current_price:.4f}")
|
||
|
|
|
||
|
|
# Multi-period RSI
|
||
|
|
rsi_7 = indicators.get('rsi_7', 50)
|
||
|
|
rsi_14 = indicators.get('rsi', 50)
|
||
|
|
rsi_21 = indicators.get('rsi_21', 50)
|
||
|
|
if hasattr(rsi_7, '__len__'):
|
||
|
|
rsi_7, rsi_14, rsi_21 = rsi_7.iloc[-1], rsi_14.iloc[-1], rsi_21.iloc[-1]
|
||
|
|
|
||
|
|
summary.append(f"RSI(7/14/21): {rsi_7:.1f}/{rsi_14:.1f}/{rsi_21:.1f}")
|
||
|
|
|
||
|
|
# RSI status analysis
|
||
|
|
rsi_status = []
|
||
|
|
for rsi_val, period in [(rsi_7, 7), (rsi_14, 14), (rsi_21, 21)]:
|
||
|
|
if rsi_val > 70:
|
||
|
|
rsi_status.append(f"RSI{period} overbought")
|
||
|
|
elif rsi_val < 30:
|
||
|
|
rsi_status.append(f"RSI{period} oversold")
|
||
|
|
if rsi_status:
|
||
|
|
summary.append(f"RSI status: {', '.join(rsi_status)}")
|
||
|
|
|
||
|
|
# KDJ indicator
|
||
|
|
k, d, j = indicators.get('kdj', (50, 50, 50))
|
||
|
|
if hasattr(k, '__len__'):
|
||
|
|
k, d, j = k.iloc[-1], d.iloc[-1], j.iloc[-1]
|
||
|
|
|
||
|
|
summary.append(f"KDJ: K={k:.1f}, D={d:.1f}, J={j:.1f}")
|
||
|
|
if j > 80:
|
||
|
|
summary.append("KDJ status: Overbought area")
|
||
|
|
elif j < 20:
|
||
|
|
summary.append("KDJ status: Oversold area")
|
||
|
|
|
||
|
|
# MACD indicator
|
||
|
|
macd_line, signal_line, histogram = indicators.get('macd', (0, 0, 0))
|
||
|
|
if hasattr(macd_line, '__len__'):
|
||
|
|
macd_line, signal_line, histogram = macd_line.iloc[-1], signal_line.iloc[-1], histogram.iloc[-1]
|
||
|
|
|
||
|
|
summary.append(f"MACD: line={macd_line:.4f}, signal={signal_line:.4f}, histogram={histogram:.4f}")
|
||
|
|
summary.append(f"MACD status: {'Golden cross' if macd_line > signal_line else 'Death cross'}")
|
||
|
|
|
||
|
|
# Moving averages
|
||
|
|
sma_20 = indicators.get('sma_20', 0)
|
||
|
|
sma_50 = indicators.get('sma_50', 0)
|
||
|
|
sma_100 = indicators.get('sma_100', 0)
|
||
|
|
if hasattr(sma_20, '__len__'):
|
||
|
|
sma_20, sma_50, sma_100 = sma_20.iloc[-1], sma_50.iloc[-1], sma_100.iloc[-1]
|
||
|
|
|
||
|
|
price_vs_ma = []
|
||
|
|
if current_price > sma_20: price_vs_ma.append("Above 20-day MA")
|
||
|
|
else: price_vs_ma.append("Below 20-day MA")
|
||
|
|
if current_price > sma_50: price_vs_ma.append("Above 50-day MA")
|
||
|
|
else: price_vs_ma.append("Below 50-day MA")
|
||
|
|
|
||
|
|
summary.append(f"Moving averages: 20-day=${sma_20:.2f}, 50-day=${sma_50:.2f}, 100-day=${sma_100:.2f}")
|
||
|
|
summary.append(f"Price position: {', '.join(price_vs_ma)}")
|
||
|
|
|
||
|
|
# Bollinger Bands
|
||
|
|
upper, middle, lower = indicators.get('bollinger_2std', (0, 0, 0))
|
||
|
|
if hasattr(upper, '__len__'):
|
||
|
|
upper, middle, lower = upper.iloc[-1], middle.iloc[-1], lower.iloc[-1]
|
||
|
|
|
||
|
|
bb_position = (current_price - lower) / (upper - lower) * 100 if (upper - lower) > 0 else 50
|
||
|
|
summary.append(f"Bollinger Band position: {bb_position:.1f}% (0%=lower band, 100%=upper band)")
|
||
|
|
|
||
|
|
# Trend strength
|
||
|
|
trend_strength = indicators.get('trend_strength', 0)
|
||
|
|
summary.append(f"Trend strength: {trend_strength:.2f} (0-1, higher means stronger trend)")
|
||
|
|
|
||
|
|
# Volatility
|
||
|
|
volatility = indicators.get('volatility', 0) * 100 # Convert to percentage
|
||
|
|
summary.append(f"Annualized volatility: {volatility:.1f}%")
|
||
|
|
|
||
|
|
# ATR
|
||
|
|
atr = indicators.get('atr', 0)
|
||
|
|
if hasattr(atr, '__len__'):
|
||
|
|
atr = atr.iloc[-1]
|
||
|
|
atr_percent = (atr / current_price * 100) if current_price > 0 else 0
|
||
|
|
summary.append(f"ATR: {atr:.4f} ({atr_percent:.2f}%)")
|
||
|
|
|
||
|
|
# Support resistance information
|
||
|
|
sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data)
|
||
|
|
if sr_levels:
|
||
|
|
summary.append(f"Static support: ${sr_levels['static_support']:.4f}, resistance: ${sr_levels['static_resistance']:.4f}")
|
||
|
|
summary.append(f"Dynamic support: ${sr_levels['dynamic_support']:.4f}, resistance: ${sr_levels['dynamic_resistance']:.4f}")
|
||
|
|
summary.append(f"Relative resistance: {sr_levels['current_vs_resistance']:+.2f}%, relative support: {sr_levels['current_vs_support']:+.2f}%")
|
||
|
|
|
||
|
|
return "\n".join(summary)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error preparing technical summary: {e}")
|
||
|
|
return "Technical indicator calculation exception"
|
||
|
|
|
||
|
|
def _create_enhanced_analysis_prompt(self, symbol, latest, market_data, technical_summary):
|
||
|
|
"""Create enhanced analysis prompt"""
|
||
|
|
# Calculate price changes
|
||
|
|
price_change_24h = 0
|
||
|
|
price_change_7d = 0
|
||
|
|
|
||
|
|
if len(market_data) >= 24:
|
||
|
|
price_change_24h = ((latest['close'] - market_data.iloc[-24]['close']) / market_data.iloc[-24]['close'] * 100)
|
||
|
|
|
||
|
|
if len(market_data) >= 168: # 7 days data (hourly)
|
||
|
|
price_change_7d = ((latest['close'] - market_data.iloc[-168]['close']) / market_data.iloc[-168]['close'] * 100)
|
||
|
|
|
||
|
|
# Calculate volume changes
|
||
|
|
volume_trend = "Rising" if latest['volume'] > market_data['volume'].mean() else "Falling"
|
||
|
|
|
||
|
|
prompt = f"""
|
||
|
|
You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Please comprehensively analyze the {symbol} trading pair and execute trades.
|
||
|
|
|
||
|
|
# Core Objective
|
||
|
|
**Maximize Sharpe Ratio**
|
||
|
|
Sharpe Ratio = Average Return / Return Volatility, which means:
|
||
|
|
Quality trades (high win rate, large profit/loss ratio) → Improve Sharpe
|
||
|
|
Stable returns, control drawdowns → Improve Sharpe
|
||
|
|
Frequent trading, small profits and losses → Increase volatility, severely reduce Sharpe
|
||
|
|
|
||
|
|
【Market Overview】
|
||
|
|
- Current price: ${latest['close']:.4f}
|
||
|
|
- 24-hour change: {price_change_24h:+.2f}%
|
||
|
|
- 7-day change: {price_change_7d:+.2f}%
|
||
|
|
- Volume: {latest['volume']:.0f} ({volume_trend})
|
||
|
|
- Volume relative level: {'High' if latest['volume'] > market_data['volume'].quantile(0.7) else 'Normal' if latest['volume'] > market_data['volume'].quantile(0.3) else 'Low'}
|
||
|
|
|
||
|
|
【Multi-dimensional Technical Analysis】
|
||
|
|
{technical_summary}
|
||
|
|
|
||
|
|
【Market Environment Assessment】
|
||
|
|
Please consider comprehensively:
|
||
|
|
1. Trend direction and strength
|
||
|
|
2. Overbought/oversold status
|
||
|
|
3. Volume coordination
|
||
|
|
4. Volatility level
|
||
|
|
5. Support resistance positions
|
||
|
|
6. Multi-timeframe consistency
|
||
|
|
|
||
|
|
【Risk Management Suggestions】
|
||
|
|
- Suggested position: Adjust based on signal strength and volatility
|
||
|
|
- Stop-loss setting: Reference ATR and support levels
|
||
|
|
- Holding time: Based on trend strength
|
||
|
|
|
||
|
|
【Output Format Requirements】
|
||
|
|
Please reply strictly in the following JSON format:
|
||
|
|
{{
|
||
|
|
"action": "BUY/SELL/HOLD",
|
||
|
|
"confidence": "HIGH/MEDIUM/LOW",
|
||
|
|
"position_size": "Suggested position ratio (0.1-0.5)",
|
||
|
|
"stop_loss": "Suggested stop-loss price or percentage",
|
||
|
|
"take_profit": "Suggested take-profit price or percentage",
|
||
|
|
"timeframe": "Suggested holding time (SHORT/MEDIUM/LONG)",
|
||
|
|
"reasoning": "Detailed analysis logic and risk explanation"
|
||
|
|
}}
|
||
|
|
|
||
|
|
Please provide objective suggestions based on professional technical analysis and risk management principles.
|
||
|
|
"""
|
||
|
|
|
||
|
|
return prompt
|
||
|
|
|
||
|
|
def analyze_sell_proportion(self, symbol, position_info, market_analysis, current_price):
|
||
|
|
"""Analyze sell proportion"""
|
||
|
|
if not self.api_key:
|
||
|
|
logger.warning("DeepSeek API key not set, using default sell proportion")
|
||
|
|
return self._get_fallback_sell_proportion(position_info, 'MEDIUM')
|
||
|
|
|
||
|
|
try:
|
||
|
|
prompt = self._create_sell_proportion_prompt(symbol, position_info, market_analysis, current_price)
|
||
|
|
response = self._call_deepseek_api(prompt)
|
||
|
|
return self._parse_sell_proportion_response(response)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"DeepSeek sell proportion analysis error: {e}")
|
||
|
|
return self._get_fallback_sell_proportion(position_info, 'MEDIUM')
|
||
|
|
|
||
|
|
def _create_sell_proportion_prompt(self, symbol, position_info, market_analysis, current_price):
|
||
|
|
"""Create sell proportion analysis prompt"""
|
||
|
|
|
||
|
|
# Calculate profit situation
|
||
|
|
entry_price = position_info['entry_price']
|
||
|
|
profit_ratio = (current_price - entry_price) / entry_price if entry_price > 0 else 0
|
||
|
|
profit_status = "Profit" if profit_ratio > 0 else "Loss"
|
||
|
|
profit_amount = (current_price - entry_price) * position_info['base_amount']
|
||
|
|
|
||
|
|
# Calculate position ratio
|
||
|
|
position_ratio = position_info['position_ratio'] * 100 # Convert to percentage
|
||
|
|
|
||
|
|
# Prepare technical indicator summary
|
||
|
|
technical_summary = self._prepare_enhanced_technical_summary(
|
||
|
|
market_analysis['technical_indicators'],
|
||
|
|
market_analysis['market_data']
|
||
|
|
)
|
||
|
|
|
||
|
|
prompt = f"""
|
||
|
|
You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Analyze the {symbol} position situation and consider and execute sell decisions.
|
||
|
|
|
||
|
|
【Current Position Situation】
|
||
|
|
- Position quantity: {position_info['base_amount']:.8f}
|
||
|
|
- Average entry price: ${entry_price:.4f}
|
||
|
|
- Current price: ${current_price:.4f}
|
||
|
|
- Profit ratio: {profit_ratio:+.2%} ({profit_status})
|
||
|
|
- Profit amount: ${profit_amount:+.2f}
|
||
|
|
- Position ratio: {position_ratio:.2f}% (percentage of total portfolio)
|
||
|
|
- Position value: ${position_info['position_value']:.2f}
|
||
|
|
|
||
|
|
【Market Technical Analysis】
|
||
|
|
{technical_summary}
|
||
|
|
|
||
|
|
【Risk Situation】
|
||
|
|
- Current price distance to stop-loss: {position_info['distance_to_stop_loss']:.2%}
|
||
|
|
- Current price distance to take-profit: {position_info['distance_to_take_profit']:.2%}
|
||
|
|
- Market volatility: {position_info['market_volatility']:.2%}
|
||
|
|
- Trend strength: {position_info.get('trend_strength', 0):.2f}
|
||
|
|
|
||
|
|
【Sell Strategy Considerations】
|
||
|
|
Please consider the following factors to determine sell proportion:
|
||
|
|
1. Profit ratio: Large profits can consider partial profit-taking, keep some position for higher gains
|
||
|
|
2. Position ratio: Overweight positions should reduce position to lower risk, light positions can consider holding
|
||
|
|
3. Technical signals: Strong bearish signals should increase sell proportion, bullish signals can reduce sell proportion
|
||
|
|
4. Market environment: High volatility markets should be more conservative, low volatility markets can be more aggressive
|
||
|
|
5. Risk control: Close to stop-loss should sell decisively, far from stop-loss can be more flexible
|
||
|
|
|
||
|
|
【Position Management Principles】
|
||
|
|
- Profit over 20%: Consider partial profit-taking (30-70%), lock in profits
|
||
|
|
- Profit 10-20%: Decide based on signal strength (20-50%)
|
||
|
|
- Small profit (0-10%): Mainly based on technical signals (0-30%)
|
||
|
|
- Small loss (0-5%): Based on risk control (50-80%)
|
||
|
|
- Large loss (>5%): Consider stop-loss or position reduction (80-100%)
|
||
|
|
|
||
|
|
【Output Format Requirements】
|
||
|
|
Please reply strictly in the following JSON format, only containing numeric ratio (0-1):
|
||
|
|
{{
|
||
|
|
"sell_proportion": 0.75
|
||
|
|
}}
|
||
|
|
|
||
|
|
Explanation:
|
||
|
|
- 0.1 means sell 10% of position
|
||
|
|
- 0.5 means sell 50% of position
|
||
|
|
- 1.0 means sell all position
|
||
|
|
- 0.0 means don't sell (only in extremely bullish situations)
|
||
|
|
|
||
|
|
Please provide reasonable sell proportion suggestions based on professional analysis and risk management.
|
||
|
|
"""
|
||
|
|
|
||
|
|
return prompt
|
||
|
|
|
||
|
|
def _parse_sell_proportion_response(self, response):
|
||
|
|
"""Parse sell proportion response"""
|
||
|
|
try:
|
||
|
|
content = response['choices'][0]['message']['content']
|
||
|
|
|
||
|
|
# Try to parse JSON format
|
||
|
|
if '{' in content and '}' in content:
|
||
|
|
json_start = content.find('{')
|
||
|
|
json_end = content.rfind('}') + 1
|
||
|
|
json_str = content[json_start:json_end]
|
||
|
|
|
||
|
|
parsed = json.loads(json_str)
|
||
|
|
proportion = parsed.get('sell_proportion', 0.5)
|
||
|
|
|
||
|
|
# Ensure proportion is within reasonable range
|
||
|
|
proportion = max(0.0, min(1.0, proportion))
|
||
|
|
logger.info(f"DeepSeek suggested sell proportion: {proportion:.1%}")
|
||
|
|
return proportion
|
||
|
|
else:
|
||
|
|
# Text analysis: try to extract proportion from text
|
||
|
|
import re
|
||
|
|
patterns = [
|
||
|
|
r'[\"\"]([\d.]+)%[\"\"]', # "50%"
|
||
|
|
r'sell\s*([\d.]+)%', # sell 50%
|
||
|
|
r'([\d.]+)%', # 50%
|
||
|
|
r'[\"\"]([\d.]+)[\"\"]', # "0.5"
|
||
|
|
]
|
||
|
|
|
||
|
|
for pattern in patterns:
|
||
|
|
match = re.search(pattern, content)
|
||
|
|
if match:
|
||
|
|
value = float(match.group(1))
|
||
|
|
if value > 1: # If in percentage format
|
||
|
|
value = value / 100
|
||
|
|
proportion = max(0.0, min(1.0, value))
|
||
|
|
logger.info(f"Parsed sell proportion from text: {proportion:.1%}")
|
||
|
|
return proportion
|
||
|
|
|
||
|
|
# Default value
|
||
|
|
logger.warning("Unable to parse sell proportion, using default 50%")
|
||
|
|
return 0.5
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error parsing sell proportion response: {e}")
|
||
|
|
return 0.5
|
||
|
|
|
||
|
|
def _get_fallback_sell_proportion(self, position_info, decision_confidence):
|
||
|
|
"""Fallback sell proportion decision (when DeepSeek is unavailable)"""
|
||
|
|
|
||
|
|
profit_ratio = position_info['profit_ratio']
|
||
|
|
position_ratio = position_info['position_ratio']
|
||
|
|
market_volatility = position_info.get('market_volatility', 0)
|
||
|
|
trend_strength = position_info.get('trend_strength', 0)
|
||
|
|
|
||
|
|
# Decision based on profit situation
|
||
|
|
if profit_ratio >= 0.20: # Profit over 20%
|
||
|
|
base_proportion = 0.6 if decision_confidence == 'HIGH' else 0.4
|
||
|
|
elif profit_ratio >= 0.10: # Profit 10%-20%
|
||
|
|
base_proportion = 0.5 if decision_confidence == 'HIGH' else 0.3
|
||
|
|
elif profit_ratio >= 0: # Profit 0%-10%
|
||
|
|
base_proportion = 0.4 if decision_confidence == 'HIGH' else 0.2
|
||
|
|
elif profit_ratio >= -0.05: # Loss 0%-5%
|
||
|
|
base_proportion = 0.7 if decision_confidence == 'HIGH' else 0.5
|
||
|
|
else: # Loss over 5%
|
||
|
|
base_proportion = 0.9 if decision_confidence == 'HIGH' else 0.7
|
||
|
|
|
||
|
|
# Adjustment based on position ratio
|
||
|
|
if position_ratio > 0.15: # Overweight position
|
||
|
|
base_proportion = min(1.0, base_proportion + 0.2)
|
||
|
|
elif position_ratio < 0.05: # Very light position
|
||
|
|
base_proportion = max(0.1, base_proportion - 0.1)
|
||
|
|
|
||
|
|
# Adjustment based on market volatility
|
||
|
|
if market_volatility > 0.05: # High volatility market
|
||
|
|
base_proportion = min(1.0, base_proportion + 0.1)
|
||
|
|
elif market_volatility < 0.02: # Low volatility market
|
||
|
|
base_proportion = max(0.1, base_proportion - 0.1)
|
||
|
|
|
||
|
|
# Adjustment based on trend strength
|
||
|
|
if trend_strength > 0.7: # Strong trend
|
||
|
|
base_proportion = max(0.1, base_proportion - 0.1)
|
||
|
|
elif trend_strength < 0.3: # Weak trend
|
||
|
|
base_proportion = min(1.0, base_proportion + 0.1)
|
||
|
|
|
||
|
|
# Ensure proportion is within reasonable range
|
||
|
|
base_proportion = max(0.1, min(1.0, base_proportion))
|
||
|
|
|
||
|
|
logger.info(f"Fallback sell proportion decision: profit={profit_ratio:+.2%}, position ratio={position_ratio:.2%}, "
|
||
|
|
f"volatility={market_volatility:.2%}, trend strength={trend_strength:.2f}, final proportion={base_proportion:.1%}")
|
||
|
|
return base_proportion
|
||
|
|
|
||
|
|
def _call_deepseek_api(self, prompt, max_retries=3):
|
||
|
|
"""Call DeepSeek API (improved retry mechanism)"""
|
||
|
|
headers = {
|
||
|
|
'Authorization': f'Bearer {self.api_key}',
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
}
|
||
|
|
|
||
|
|
data = {
|
||
|
|
'model': 'deepseek-chat',
|
||
|
|
'messages': [
|
||
|
|
{
|
||
|
|
'role': 'system',
|
||
|
|
'content': 'You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market, pursuing stable and sustainable returns'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
'role': 'user',
|
||
|
|
'content': prompt
|
||
|
|
}
|
||
|
|
],
|
||
|
|
'temperature': 0.3,
|
||
|
|
'max_tokens': 1000
|
||
|
|
}
|
||
|
|
|
||
|
|
for attempt in range(max_retries):
|
||
|
|
try:
|
||
|
|
response = requests.post(
|
||
|
|
self.api_url,
|
||
|
|
headers=headers,
|
||
|
|
json=data,
|
||
|
|
timeout=30 # 30 second timeout
|
||
|
|
)
|
||
|
|
|
||
|
|
if response.status_code == 200:
|
||
|
|
return response.json()
|
||
|
|
elif response.status_code == 429: # Rate limit
|
||
|
|
wait_time = 2 ** attempt # Exponential backoff
|
||
|
|
logger.warning(f"DeepSeek API rate limit, waiting {wait_time} seconds before retry")
|
||
|
|
time.sleep(wait_time)
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
logger.error(f"DeepSeek API call failed: {response.status_code} - {response.text}")
|
||
|
|
if attempt < max_retries - 1:
|
||
|
|
time.sleep(1)
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
raise Exception(f"API call failed: {response.status_code}")
|
||
|
|
|
||
|
|
except requests.exceptions.Timeout:
|
||
|
|
logger.warning(f"DeepSeek API request timeout, attempt {attempt + 1}/{max_retries}")
|
||
|
|
if attempt < max_retries - 1:
|
||
|
|
time.sleep(1)
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
raise Exception("DeepSeek API request timeout")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"DeepSeek API call exception: {e}")
|
||
|
|
if attempt < max_retries - 1:
|
||
|
|
time.sleep(1)
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
raise
|
||
|
|
|
||
|
|
def _parse_deepseek_response(self, response):
|
||
|
|
"""Parse DeepSeek response - enhanced version"""
|
||
|
|
try:
|
||
|
|
content = response['choices'][0]['message']['content'].strip()
|
||
|
|
logger.debug(f"DeepSeek raw response: {content}")
|
||
|
|
|
||
|
|
# Remove possible code block markers
|
||
|
|
if content.startswith('```json'):
|
||
|
|
content = content[7:]
|
||
|
|
if content.endswith('```'):
|
||
|
|
content = content[:-3]
|
||
|
|
content = content.strip()
|
||
|
|
|
||
|
|
# Try to parse JSON format
|
||
|
|
if '{' in content and '}' in content:
|
||
|
|
json_start = content.find('{')
|
||
|
|
json_end = content.rfind('}') + 1
|
||
|
|
json_str = content[json_start:json_end]
|
||
|
|
|
||
|
|
try:
|
||
|
|
parsed = json.loads(json_str)
|
||
|
|
result = {
|
||
|
|
'action': parsed.get('action', 'HOLD').upper(),
|
||
|
|
'confidence': parsed.get('confidence', 'MEDIUM').upper(),
|
||
|
|
'reasoning': parsed.get('reasoning', ''),
|
||
|
|
'raw_response': content
|
||
|
|
}
|
||
|
|
|
||
|
|
# Validate action and confidence legality
|
||
|
|
if result['action'] not in ['BUY', 'SELL', 'HOLD']:
|
||
|
|
logger.warning(f"AI returned illegal action: {result['action']}, using HOLD")
|
||
|
|
result['action'] = 'HOLD'
|
||
|
|
|
||
|
|
if result['confidence'] not in ['HIGH', 'MEDIUM', 'LOW']:
|
||
|
|
logger.warning(f"AI returned illegal confidence: {result['confidence']}, using MEDIUM")
|
||
|
|
result['confidence'] = 'MEDIUM'
|
||
|
|
|
||
|
|
logger.info(f"AI parsing successful: {result['action']} (confidence: {result['confidence']})")
|
||
|
|
return result
|
||
|
|
|
||
|
|
except json.JSONDecodeError as e:
|
||
|
|
logger.warning(f"JSON parsing failed: {e}, trying text analysis")
|
||
|
|
return self._parse_text_response(content)
|
||
|
|
else:
|
||
|
|
# Text analysis
|
||
|
|
return self._parse_text_response(content)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error parsing DeepSeek response: {e}")
|
||
|
|
# Return default values to avoid system crash
|
||
|
|
return {
|
||
|
|
'action': 'HOLD',
|
||
|
|
'confidence': 'MEDIUM',
|
||
|
|
'reasoning': f'Parsing error: {str(e)}',
|
||
|
|
'raw_response': str(response)
|
||
|
|
}
|
||
|
|
|
||
|
|
def _parse_text_response(self, text):
|
||
|
|
"""Parse text response - enhanced version"""
|
||
|
|
text_lower = text.lower()
|
||
|
|
|
||
|
|
# More precise action recognition
|
||
|
|
action = 'HOLD'
|
||
|
|
if any(word in text_lower for word in ['买入', '做多', 'buy', 'long', '上涨', '看涨']):
|
||
|
|
action = 'BUY'
|
||
|
|
elif any(word in text_lower for word in ['卖出', '做空', 'sell', 'short', '下跌', '看跌']):
|
||
|
|
action = 'SELL'
|
||
|
|
|
||
|
|
# More precise confidence level recognition
|
||
|
|
confidence = 'MEDIUM'
|
||
|
|
if any(word in text_lower for word in ['强烈', '高信心', 'high', '非常', '强烈建议']):
|
||
|
|
confidence = 'HIGH'
|
||
|
|
elif any(word in text_lower for word in ['低', '低信心', 'low', '轻微', '谨慎']):
|
||
|
|
confidence = 'LOW'
|
||
|
|
|
||
|
|
logger.info(f"Text parsing result: {action} (confidence: {confidence})")
|
||
|
|
|
||
|
|
return {
|
||
|
|
'action': action,
|
||
|
|
'confidence': confidence,
|
||
|
|
'reasoning': text,
|
||
|
|
'raw_response': text
|
||
|
|
}
|
||
|
|
|
||
|
|
class TechnicalAnalyzer:
|
||
|
|
"""Technical Analyzer"""
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_indicators(df):
|
||
|
|
"""Calculate technical indicators (optimized version)"""
|
||
|
|
if df is None or len(df) < 20: # Lower minimum data requirement
|
||
|
|
logger.warning("Insufficient data, unable to calculate technical indicators")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
try:
|
||
|
|
indicators = {}
|
||
|
|
|
||
|
|
# KDJ indicator
|
||
|
|
k, d, j = TechnicalAnalyzer.calculate_kdj(df)
|
||
|
|
if k is not None:
|
||
|
|
indicators['kdj'] = (k, d, j)
|
||
|
|
|
||
|
|
# RSI indicator (multiple time periods)
|
||
|
|
rsi_14 = TechnicalAnalyzer.calculate_rsi(df['close'], 14)
|
||
|
|
rsi_7 = TechnicalAnalyzer.calculate_rsi(df['close'], 7)
|
||
|
|
rsi_21 = TechnicalAnalyzer.calculate_rsi(df['close'], 21)
|
||
|
|
if rsi_14 is not None:
|
||
|
|
indicators['rsi'] = rsi_14
|
||
|
|
indicators['rsi_7'] = rsi_7
|
||
|
|
indicators['rsi_21'] = rsi_21
|
||
|
|
|
||
|
|
# ATR indicator
|
||
|
|
atr = TechnicalAnalyzer.calculate_atr(df)
|
||
|
|
if atr is not None:
|
||
|
|
indicators['atr'] = atr
|
||
|
|
|
||
|
|
# MACD indicator
|
||
|
|
macd_line, signal_line, histogram = TechnicalAnalyzer.calculate_macd(df['close'])
|
||
|
|
if macd_line is not None:
|
||
|
|
indicators['macd'] = (macd_line, signal_line, histogram)
|
||
|
|
|
||
|
|
# Bollinger Bands (multiple standard deviations)
|
||
|
|
upper1, middle1, lower1 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 1)
|
||
|
|
upper2, middle2, lower2 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2)
|
||
|
|
if upper1 is not None:
|
||
|
|
indicators['bollinger_1std'] = (upper1, middle1, lower1)
|
||
|
|
indicators['bollinger_2std'] = (upper2, middle2, lower2)
|
||
|
|
|
||
|
|
# Moving Averages (multiple periods)
|
||
|
|
sma_20 = TechnicalAnalyzer.calculate_sma(df['close'], 20)
|
||
|
|
sma_50 = TechnicalAnalyzer.calculate_sma(df['close'], 50)
|
||
|
|
sma_100 = TechnicalAnalyzer.calculate_sma(df['close'], 100)
|
||
|
|
ema_12 = TechnicalAnalyzer.calculate_ema(df['close'], 12)
|
||
|
|
ema_26 = TechnicalAnalyzer.calculate_ema(df['close'], 26)
|
||
|
|
if sma_20 is not None:
|
||
|
|
indicators['sma_20'] = sma_20
|
||
|
|
indicators['sma_50'] = sma_50
|
||
|
|
indicators['sma_100'] = sma_100
|
||
|
|
indicators['ema_12'] = ema_12
|
||
|
|
indicators['ema_26'] = ema_26
|
||
|
|
|
||
|
|
# Stochastic Oscillator
|
||
|
|
k, d = TechnicalAnalyzer.calculate_stochastic(df)
|
||
|
|
if k is not None:
|
||
|
|
indicators['stochastic'] = (k, d)
|
||
|
|
|
||
|
|
# Trend strength
|
||
|
|
indicators['trend_strength'] = TechnicalAnalyzer.calculate_trend_strength(df)
|
||
|
|
|
||
|
|
# Volatility
|
||
|
|
indicators['volatility'] = TechnicalAnalyzer.calculate_volatility(df)
|
||
|
|
|
||
|
|
# Current price
|
||
|
|
indicators['current_price'] = df['close'].iloc[-1]
|
||
|
|
|
||
|
|
return indicators
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating technical indicators: {e}")
|
||
|
|
return {}
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_ema(prices, period):
|
||
|
|
"""Calculate Exponential Moving Average"""
|
||
|
|
try:
|
||
|
|
return prices.ewm(span=period, adjust=False).mean()
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating EMA: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_trend_strength(df, period=20):
|
||
|
|
"""Calculate trend strength"""
|
||
|
|
try:
|
||
|
|
if len(df) < period:
|
||
|
|
return 0
|
||
|
|
|
||
|
|
# Use linear regression to calculate trend strength
|
||
|
|
x = np.arange(len(df))
|
||
|
|
y = df['close'].values
|
||
|
|
|
||
|
|
# Linear regression
|
||
|
|
slope, intercept = np.polyfit(x[-period:], y[-period:], 1)
|
||
|
|
|
||
|
|
# Calculate R² value as trend strength
|
||
|
|
y_pred = slope * x[-period:] + intercept
|
||
|
|
ss_res = np.sum((y[-period:] - y_pred) ** 2)
|
||
|
|
ss_tot = np.sum((y[-period:] - np.mean(y[-period:])) ** 2)
|
||
|
|
r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
|
||
|
|
|
||
|
|
return abs(r_squared) # Take absolute value, trend strength doesn't distinguish positive/negative
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating trend strength: {e}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def detect_rsi_divergence(df, rsi_period=14):
|
||
|
|
"""Detect RSI bullish divergence"""
|
||
|
|
try:
|
||
|
|
if len(df) < 30: # Need sufficient data
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Calculate RSI
|
||
|
|
rsi = TechnicalAnalyzer.calculate_rsi(df['close'], rsi_period)
|
||
|
|
if rsi is None:
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Use pandas methods to simplify calculation
|
||
|
|
# Find lowest points in recent 10 periods
|
||
|
|
recent_lows = df['low'].tail(10)
|
||
|
|
recent_rsi = rsi.tail(10)
|
||
|
|
|
||
|
|
# Find price lowest point and RSI lowest point
|
||
|
|
min_price_idx = recent_lows.idxmin()
|
||
|
|
min_rsi_idx = recent_rsi.idxmin()
|
||
|
|
|
||
|
|
# If price lowest point appears later than RSI lowest point, possible bullish divergence
|
||
|
|
if min_price_idx > min_rsi_idx:
|
||
|
|
# Check if price is making new lows while RSI is rising
|
||
|
|
price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5])
|
||
|
|
rsi_trend = (recent_rsi.iloc[-1] > recent_rsi.iloc[-5])
|
||
|
|
|
||
|
|
return price_trend and rsi_trend and recent_rsi.iloc[-1] < 40
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"RSI divergence detection error: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def detect_macd_divergence(df):
|
||
|
|
"""Detect MACD bullish divergence"""
|
||
|
|
try:
|
||
|
|
if len(df) < 30:
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Calculate MACD, especially focus on histogram
|
||
|
|
_, _, histogram = TechnicalAnalyzer.calculate_macd(df['close'])
|
||
|
|
if histogram is None:
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Use recent data
|
||
|
|
recent_lows = df['low'].tail(10)
|
||
|
|
recent_hist = histogram.tail(10)
|
||
|
|
|
||
|
|
# Find price lowest point and MACD histogram lowest point
|
||
|
|
min_price_idx = recent_lows.idxmin()
|
||
|
|
min_hist_idx = recent_hist.idxmin()
|
||
|
|
|
||
|
|
# If price lowest point appears later than histogram lowest point, possible bullish divergence
|
||
|
|
if min_price_idx > min_hist_idx:
|
||
|
|
# Check price trend and histogram trend
|
||
|
|
price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5])
|
||
|
|
hist_trend = (recent_hist.iloc[-1] > recent_hist.iloc[-5])
|
||
|
|
|
||
|
|
# Histogram rising from negative area is stronger signal
|
||
|
|
hist_improving = recent_hist.iloc[-1] > recent_hist.iloc[-3]
|
||
|
|
|
||
|
|
return price_trend and hist_trend and hist_improving
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"MACD divergence detection error: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def volume_confirmation(df, period=5):
|
||
|
|
"""Bottom volume confirmation"""
|
||
|
|
try:
|
||
|
|
if len(df) < period + 5:
|
||
|
|
return False
|
||
|
|
|
||
|
|
current_volume = df['volume'].iloc[-1]
|
||
|
|
avg_volume = df['volume'].tail(period).mean()
|
||
|
|
|
||
|
|
# Current volume significantly higher than average
|
||
|
|
volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1
|
||
|
|
|
||
|
|
# Price falling but volume increasing (possible bottom accumulation)
|
||
|
|
price_change = (df['close'].iloc[-1] - df['close'].iloc[-2]) / df['close'].iloc[-2]
|
||
|
|
|
||
|
|
if volume_ratio > 1.5 and price_change < 0:
|
||
|
|
return True
|
||
|
|
|
||
|
|
return False
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Volume confirmation error: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def detect_hammer_pattern(df, lookback=5):
|
||
|
|
"""Detect hammer reversal pattern"""
|
||
|
|
try:
|
||
|
|
if len(df) < lookback + 1:
|
||
|
|
return False
|
||
|
|
|
||
|
|
latest = df.iloc[-1]
|
||
|
|
body_size = abs(latest['close'] - latest['open'])
|
||
|
|
total_range = latest['high'] - latest['low']
|
||
|
|
|
||
|
|
# Avoid division by zero
|
||
|
|
if total_range == 0:
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Hammer characteristics: lower shadow at least 2x body, very short upper shadow
|
||
|
|
lower_shadow = min(latest['open'], latest['close']) - latest['low']
|
||
|
|
upper_shadow = latest['high'] - max(latest['open'], latest['close'])
|
||
|
|
|
||
|
|
is_hammer = (lower_shadow >= 2 * body_size and
|
||
|
|
upper_shadow <= body_size * 0.5 and
|
||
|
|
body_size > 0)
|
||
|
|
|
||
|
|
# Needs to be in downtrend
|
||
|
|
prev_trend = df['close'].iloc[-lookback] > df['close'].iloc[-1]
|
||
|
|
|
||
|
|
return is_hammer and prev_trend
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Hammer pattern detection error: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def detect_reversal_patterns(df):
|
||
|
|
"""Detect bottom reversal patterns"""
|
||
|
|
reversal_signals = []
|
||
|
|
|
||
|
|
try:
|
||
|
|
# RSI bullish divergence
|
||
|
|
if TechnicalAnalyzer.detect_rsi_divergence(df):
|
||
|
|
reversal_signals.append({
|
||
|
|
'type': 'RSI_DIVERGENCE_BULLISH',
|
||
|
|
'action': 'BUY',
|
||
|
|
'strength': 'MEDIUM',
|
||
|
|
'description': 'RSI bullish divergence, momentum divergence bullish'
|
||
|
|
})
|
||
|
|
|
||
|
|
# MACD bullish divergence
|
||
|
|
if TechnicalAnalyzer.detect_macd_divergence(df):
|
||
|
|
reversal_signals.append({
|
||
|
|
'type': 'MACD_DIVERGENCE_BULLISH',
|
||
|
|
'action': 'BUY',
|
||
|
|
'strength': 'MEDIUM',
|
||
|
|
'description': 'MACD histogram bullish divergence, momentum strengthening'
|
||
|
|
})
|
||
|
|
|
||
|
|
# Volume confirmation
|
||
|
|
if TechnicalAnalyzer.volume_confirmation(df):
|
||
|
|
reversal_signals.append({
|
||
|
|
'type': 'VOLUME_CONFIRMATION',
|
||
|
|
'action': 'BUY',
|
||
|
|
'strength': 'LOW',
|
||
|
|
'description': 'Bottom volume increase, signs of capital entry'
|
||
|
|
})
|
||
|
|
|
||
|
|
# Add hammer pattern detection
|
||
|
|
if TechnicalAnalyzer.detect_hammer_pattern(df):
|
||
|
|
reversal_signals.append({
|
||
|
|
'type': 'HAMMER_PATTERN',
|
||
|
|
'action': 'BUY',
|
||
|
|
'strength': 'LOW',
|
||
|
|
'description': 'Hammer pattern, short-term reversal signal'
|
||
|
|
})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Reversal pattern detection error: {e}")
|
||
|
|
|
||
|
|
return reversal_signals
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_support_resistance(df, period=20):
|
||
|
|
"""Calculate support resistance levels (fixed version)"""
|
||
|
|
try:
|
||
|
|
# Recent high resistance
|
||
|
|
resistance = df['high'].rolling(window=period).max().iloc[-1]
|
||
|
|
|
||
|
|
# Recent low support
|
||
|
|
support = df['low'].rolling(window=period).min().iloc[-1]
|
||
|
|
|
||
|
|
# Dynamic support resistance (based on Bollinger Bands)
|
||
|
|
# Fix: correctly receive three return values from Bollinger Bands
|
||
|
|
upper_bb, middle_bb, lower_bb = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2)
|
||
|
|
|
||
|
|
# Check if Bollinger Band calculation successful
|
||
|
|
if upper_bb is not None and lower_bb is not None:
|
||
|
|
resistance_bb = upper_bb.iloc[-1] if hasattr(upper_bb, 'iloc') else upper_bb
|
||
|
|
support_bb = lower_bb.iloc[-1] if hasattr(lower_bb, 'iloc') else lower_bb
|
||
|
|
else:
|
||
|
|
# If Bollinger Band calculation fails, use static values as backup
|
||
|
|
resistance_bb = resistance
|
||
|
|
support_bb = support
|
||
|
|
|
||
|
|
current_price = df['close'].iloc[-1]
|
||
|
|
|
||
|
|
return {
|
||
|
|
'static_resistance': resistance,
|
||
|
|
'static_support': support,
|
||
|
|
'dynamic_resistance': resistance_bb,
|
||
|
|
'dynamic_support': support_bb,
|
||
|
|
'current_vs_resistance': (current_price - resistance) / resistance * 100 if resistance > 0 else 0,
|
||
|
|
'current_vs_support': (current_price - support) / support * 100 if support > 0 else 0
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating support resistance: {e}")
|
||
|
|
# Return default values to avoid subsequent errors
|
||
|
|
current_price = df['close'].iloc[-1] if len(df) > 0 else 0
|
||
|
|
return {
|
||
|
|
'static_resistance': current_price * 1.1 if current_price > 0 else 0,
|
||
|
|
'static_support': current_price * 0.9 if current_price > 0 else 0,
|
||
|
|
'dynamic_resistance': current_price * 1.05 if current_price > 0 else 0,
|
||
|
|
'dynamic_support': current_price * 0.95 if current_price > 0 else 0,
|
||
|
|
'current_vs_resistance': 0,
|
||
|
|
'current_vs_support': 0
|
||
|
|
}
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_volatility(df, period=20):
|
||
|
|
"""Calculate volatility"""
|
||
|
|
try:
|
||
|
|
returns = df['close'].pct_change().dropna()
|
||
|
|
volatility = returns.rolling(window=period).std() * np.sqrt(365) # Annualized volatility
|
||
|
|
return volatility.iloc[-1] if len(volatility) > 0 else 0
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating volatility: {e}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def generate_weighted_signals(df):
|
||
|
|
"""Generate weighted technical signals"""
|
||
|
|
signals = []
|
||
|
|
weights = {
|
||
|
|
'KDJ_GOLDEN_CROSS': 0.15,
|
||
|
|
'MACD_GOLDEN_CROSS': 0.20,
|
||
|
|
'MA_GOLDEN_CROSS': 0.25,
|
||
|
|
'RSI_OVERSOLD': 0.15,
|
||
|
|
'BOLLINGER_LOWER_TOUCH': 0.15,
|
||
|
|
'STOCH_GOLDEN_CROSS': 0.10
|
||
|
|
}
|
||
|
|
|
||
|
|
# Calculate various indicator signals
|
||
|
|
technical_signals = TechnicalAnalyzer.generate_signals(df)
|
||
|
|
|
||
|
|
# Calculate weighted score
|
||
|
|
total_score = 0
|
||
|
|
signal_count = 0
|
||
|
|
|
||
|
|
for signal in technical_signals:
|
||
|
|
weight = weights.get(signal['type'], 0.05)
|
||
|
|
strength_multiplier = 1.0 if signal['strength'] == 'STRONG' else 0.7
|
||
|
|
total_score += weight * strength_multiplier
|
||
|
|
signal_count += 1
|
||
|
|
|
||
|
|
# Normalize score
|
||
|
|
normalized_score = min(1.0, total_score)
|
||
|
|
|
||
|
|
# Determine action
|
||
|
|
if normalized_score > 0.6:
|
||
|
|
action = 'BUY'
|
||
|
|
elif normalized_score < 0.3:
|
||
|
|
action = 'SELL'
|
||
|
|
else:
|
||
|
|
action = 'HOLD'
|
||
|
|
|
||
|
|
return {
|
||
|
|
'score': normalized_score,
|
||
|
|
'action': action,
|
||
|
|
'signal_count': signal_count,
|
||
|
|
'signals': technical_signals,
|
||
|
|
'confidence': 'HIGH' if normalized_score > 0.7 or normalized_score < 0.2 else 'MEDIUM'
|
||
|
|
}
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_kdj(df, n=9, m1=3, m2=3):
|
||
|
|
"""Calculate KDJ indicator"""
|
||
|
|
try:
|
||
|
|
if len(df) < n:
|
||
|
|
return pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df))
|
||
|
|
|
||
|
|
low_list = df['low'].rolling(window=n, min_periods=1).min()
|
||
|
|
high_list = df['high'].rolling(window=n, min_periods=1).max()
|
||
|
|
|
||
|
|
# Avoid division by zero error
|
||
|
|
denominator = high_list - low_list
|
||
|
|
denominator = denominator.replace(0, np.nan)
|
||
|
|
|
||
|
|
rsv = ((df['close'] - low_list) / denominator) * 100
|
||
|
|
rsv = rsv.fillna(50)
|
||
|
|
|
||
|
|
k_series = rsv.ewm(span=m1-1, adjust=False).mean()
|
||
|
|
d_series = k_series.ewm(span=m2-1, adjust=False).mean()
|
||
|
|
j_series = 3 * k_series - 2 * d_series
|
||
|
|
|
||
|
|
return k_series, d_series, j_series
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating KDJ: {e}")
|
||
|
|
return None, None, None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_rsi(prices, period=14):
|
||
|
|
"""Calculate RSI"""
|
||
|
|
try:
|
||
|
|
if len(prices) < period:
|
||
|
|
return pd.Series([np.nan] * len(prices))
|
||
|
|
|
||
|
|
delta = prices.diff()
|
||
|
|
gain = (delta.where(delta > 0, 0)).rolling(window=period, min_periods=1).mean()
|
||
|
|
loss = (-delta.where(delta < 0, 0)).rolling(window=period, min_periods=1).mean()
|
||
|
|
|
||
|
|
# Avoid division by zero error
|
||
|
|
rs = gain / loss.replace(0, np.nan)
|
||
|
|
rsi = 100 - (100 / (1 + rs))
|
||
|
|
return rsi.fillna(50)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating RSI: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_atr(df, period=14):
|
||
|
|
"""Calculate ATR"""
|
||
|
|
try:
|
||
|
|
if len(df) < period:
|
||
|
|
return pd.Series([np.nan] * len(df))
|
||
|
|
|
||
|
|
high_low = df['high'] - df['low']
|
||
|
|
high_close = np.abs(df['high'] - df['close'].shift())
|
||
|
|
low_close = np.abs(df['low'] - df['close'].shift())
|
||
|
|
|
||
|
|
true_range = np.maximum(high_low, np.maximum(high_close, low_close))
|
||
|
|
atr = true_range.rolling(window=period, min_periods=1).mean()
|
||
|
|
return atr
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating ATR: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_macd(prices, fast_period=12, slow_period=26, signal_period=9):
|
||
|
|
"""Calculate MACD"""
|
||
|
|
try:
|
||
|
|
ema_fast = prices.ewm(span=fast_period, adjust=False).mean()
|
||
|
|
ema_slow = prices.ewm(span=slow_period, adjust=False).mean()
|
||
|
|
macd_line = ema_fast - ema_slow
|
||
|
|
signal_line = macd_line.ewm(span=signal_period, adjust=False).mean()
|
||
|
|
histogram = macd_line - signal_line
|
||
|
|
return macd_line, signal_line, histogram
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating MACD: {e}")
|
||
|
|
return None, None, None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_bollinger_bands(prices, period=20, std_dev=2):
|
||
|
|
"""Calculate Bollinger Bands"""
|
||
|
|
try:
|
||
|
|
if len(prices) < period:
|
||
|
|
logger.warning(f"Data length {len(prices)} insufficient, unable to calculate {period}-period Bollinger Bands")
|
||
|
|
return None, None, None
|
||
|
|
|
||
|
|
middle = prices.rolling(window=period).mean()
|
||
|
|
std = prices.rolling(window=period).std()
|
||
|
|
|
||
|
|
# Handle NaN standard deviation
|
||
|
|
std = std.fillna(0)
|
||
|
|
|
||
|
|
upper = middle + (std * std_dev)
|
||
|
|
lower = middle - (std * std_dev)
|
||
|
|
|
||
|
|
return upper, middle, lower
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating Bollinger Bands: {e}")
|
||
|
|
return None, None, None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_sma(prices, period):
|
||
|
|
"""Calculate Simple Moving Average"""
|
||
|
|
try:
|
||
|
|
return prices.rolling(window=period).mean()
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating SMA: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def calculate_stochastic(df, k_period=14, d_period=3):
|
||
|
|
"""Calculate Stochastic Oscillator"""
|
||
|
|
try:
|
||
|
|
low_min = df['low'].rolling(window=k_period).min()
|
||
|
|
high_max = df['high'].rolling(window=k_period).max()
|
||
|
|
k = 100 * ((df['close'] - low_min) / (high_max - low_min))
|
||
|
|
d = k.rolling(window=d_period).mean()
|
||
|
|
return k, d
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating Stochastic: {e}")
|
||
|
|
return None, None
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def generate_signals(df):
|
||
|
|
"""Generate technical signals"""
|
||
|
|
signals = []
|
||
|
|
|
||
|
|
try:
|
||
|
|
# KDJ
|
||
|
|
k, d, j = TechnicalAnalyzer.calculate_kdj(df)
|
||
|
|
if k is not None and len(k) > 1:
|
||
|
|
latest_k, latest_d, latest_j = k.iloc[-1], d.iloc[-1], j.iloc[-1]
|
||
|
|
prev_k, prev_d, prev_j = k.iloc[-2], d.iloc[-2], j.iloc[-2]
|
||
|
|
|
||
|
|
if prev_k <= prev_d and latest_k > latest_d:
|
||
|
|
signals.append({'type': 'KDJ_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif prev_k >= prev_d and latest_k < latest_d:
|
||
|
|
signals.append({'type': 'KDJ_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
if latest_j < 20:
|
||
|
|
signals.append({'type': 'KDJ_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif latest_j > 80:
|
||
|
|
signals.append({'type': 'KDJ_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
# RSI
|
||
|
|
rsi = TechnicalAnalyzer.calculate_rsi(df['close'])
|
||
|
|
if rsi is not None and len(rsi) > 0:
|
||
|
|
latest_rsi = rsi.iloc[-1]
|
||
|
|
|
||
|
|
if latest_rsi < 30:
|
||
|
|
signals.append({'type': 'RSI_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif latest_rsi > 70:
|
||
|
|
signals.append({'type': 'RSI_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
# MACD
|
||
|
|
macd_line, signal_line, _ = TechnicalAnalyzer.calculate_macd(df['close'])
|
||
|
|
if macd_line is not None and len(macd_line) > 1:
|
||
|
|
latest_macd = macd_line.iloc[-1]
|
||
|
|
latest_signal = signal_line.iloc[-1]
|
||
|
|
prev_macd = macd_line.iloc[-2]
|
||
|
|
prev_signal = signal_line.iloc[-2]
|
||
|
|
|
||
|
|
if prev_macd <= prev_signal and latest_macd > latest_signal:
|
||
|
|
signals.append({'type': 'MACD_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif prev_macd >= prev_signal and latest_macd < latest_signal:
|
||
|
|
signals.append({'type': 'MACD_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
# Bollinger Bands
|
||
|
|
upper, middle, lower = TechnicalAnalyzer.calculate_bollinger_bands(df['close'])
|
||
|
|
if upper is not None:
|
||
|
|
latest_close = df['close'].iloc[-1]
|
||
|
|
if latest_close <= lower.iloc[-1]:
|
||
|
|
signals.append({'type': 'BOLlinger_LOWER_TOUCH', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif latest_close >= upper.iloc[-1]:
|
||
|
|
signals.append({'type': 'BOLlinger_UPPER_TOUCH', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
# Moving Averages
|
||
|
|
sma_short = TechnicalAnalyzer.calculate_sma(df['close'], 50)
|
||
|
|
sma_long = TechnicalAnalyzer.calculate_sma(df['close'], 200)
|
||
|
|
if sma_short is not None and sma_long is not None and len(sma_short) > 1:
|
||
|
|
latest_short = sma_short.iloc[-1]
|
||
|
|
latest_long = sma_long.iloc[-1]
|
||
|
|
prev_short = sma_short.iloc[-2]
|
||
|
|
prev_long = sma_long.iloc[-2]
|
||
|
|
|
||
|
|
if prev_short <= prev_long and latest_short > latest_long:
|
||
|
|
signals.append({'type': 'MA_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif prev_short >= prev_long and latest_short < latest_long:
|
||
|
|
signals.append({'type': 'MA_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
# Stochastic
|
||
|
|
k, d = TechnicalAnalyzer.calculate_stochastic(df)
|
||
|
|
if k is not None and len(k) > 1:
|
||
|
|
latest_k = k.iloc[-1]
|
||
|
|
latest_d = d.iloc[-1]
|
||
|
|
prev_k = k.iloc[-2]
|
||
|
|
prev_d = d.iloc[-2]
|
||
|
|
|
||
|
|
if latest_k < 20:
|
||
|
|
signals.append({'type': 'STOCH_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif latest_k > 80:
|
||
|
|
signals.append({'type': 'STOCH_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
if prev_k <= prev_d and latest_k > latest_d and latest_k < 80:
|
||
|
|
signals.append({'type': 'STOCH_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'})
|
||
|
|
elif prev_k >= prev_d and latest_k < latest_d and latest_k > 20:
|
||
|
|
signals.append({'type': 'STOCH_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'})
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error generating technical signals: {e}")
|
||
|
|
|
||
|
|
return signals
|
||
|
|
|
||
|
|
class RiskManager:
|
||
|
|
"""Risk Control Manager (Dynamic Position Management Version)"""
|
||
|
|
|
||
|
|
def __init__(self, api):
|
||
|
|
self.api = api
|
||
|
|
self.max_total_position = float(os.getenv('MAX_TOTAL_POSITION', '1')) # Maximum total position 100%
|
||
|
|
self.max_single_position = float(os.getenv('MAX_SINGLE_POSITION', '0.3')) # Single currency maximum position 30%
|
||
|
|
self.stop_loss = float(os.getenv('STOP_LOSS', '0.05')) # Stop-loss 5%
|
||
|
|
self.take_profit = float(os.getenv('TAKE_PROFIT', '0.15')) # Take-profit 15%
|
||
|
|
self.max_daily_trades = int(os.getenv('MAX_DAILY_TRADES', '10')) # Maximum daily trades
|
||
|
|
self.dust_threshold_value = 1.0 # Dust position value threshold (USDT)
|
||
|
|
self.low_position_ratio = 0.05 # Low position ratio threshold (5%)
|
||
|
|
|
||
|
|
self.trailing_stop_percent = float(os.getenv('TRAILING_STOP_PERCENT', '0.03')) # Trailing stop percentage
|
||
|
|
self.monitor_interval = int(os.getenv('MONITOR_INTERVAL', '300')) # Monitoring interval seconds
|
||
|
|
|
||
|
|
# Dynamic position parameters
|
||
|
|
self.volatility_adjustment = True
|
||
|
|
self.trend_adjustment = True
|
||
|
|
self.market_sentiment_adjustment = True
|
||
|
|
|
||
|
|
# Trade records
|
||
|
|
self.daily_trade_count = {}
|
||
|
|
self.db = DatabaseManager()
|
||
|
|
|
||
|
|
def calculate_total_assets(self):
|
||
|
|
"""Calculate total assets (in USDT)"""
|
||
|
|
try:
|
||
|
|
# Get all currency balances
|
||
|
|
balances = self.api.get_currency_balances()
|
||
|
|
if not balances:
|
||
|
|
logger.error("Unable to get currency balances")
|
||
|
|
return 0.0, 0.0, {}
|
||
|
|
|
||
|
|
total_usdt_value = 0.0
|
||
|
|
position_details = {}
|
||
|
|
|
||
|
|
# Calculate USDT balance (including available and frozen)
|
||
|
|
usdt_balance = balances.get('USDT', {})
|
||
|
|
usdt_avail = usdt_balance.get('amount', 0.0)
|
||
|
|
usdt_frozen = usdt_balance.get('frozen', 0.0)
|
||
|
|
total_usdt = usdt_avail + usdt_frozen
|
||
|
|
total_usdt_value += total_usdt
|
||
|
|
|
||
|
|
# Calculate value of other currencies
|
||
|
|
for currency, balance_info in balances.items():
|
||
|
|
if currency != 'USDT':
|
||
|
|
avail = balance_info.get('amount', 0.0)
|
||
|
|
frozen = balance_info.get('frozen', 0.0)
|
||
|
|
total_amount = avail + frozen
|
||
|
|
if total_amount > 0:
|
||
|
|
symbol = f"{currency}-USDT"
|
||
|
|
current_price = self.api.get_current_price(symbol)
|
||
|
|
if current_price:
|
||
|
|
value = total_amount * current_price
|
||
|
|
total_usdt_value += value
|
||
|
|
position_details[symbol] = {
|
||
|
|
'amount': total_amount, # Total quantity (available + frozen)
|
||
|
|
'value': value,
|
||
|
|
'current_price': current_price
|
||
|
|
}
|
||
|
|
else:
|
||
|
|
logger.warning(f"Unable to get {currency} price, skipping value calculation")
|
||
|
|
|
||
|
|
logger.debug(f"Total asset calculation: USDT={total_usdt:.2f}(available{usdt_avail:.2f}+frozen{usdt_frozen:.2f}), position value={total_usdt_value - total_usdt:.2f}, total={total_usdt_value:.2f}")
|
||
|
|
|
||
|
|
return total_usdt_value, usdt_avail, position_details
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating total assets: {e}")
|
||
|
|
return 0.0, 0.0, {}
|
||
|
|
|
||
|
|
def get_position_ratio(self, symbol):
|
||
|
|
"""Get specified currency's position ratio"""
|
||
|
|
try:
|
||
|
|
total_assets, usdt_balance, positions = self.calculate_total_assets()
|
||
|
|
if total_assets == 0:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
position_value = positions.get(symbol, {}).get('value', 0.0)
|
||
|
|
return position_value / total_assets
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating position ratio: {e}")
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
def get_available_usdt_ratio(self):
|
||
|
|
"""Get available USDT ratio"""
|
||
|
|
try:
|
||
|
|
total_assets, usdt_balance, _ = self.calculate_total_assets()
|
||
|
|
if total_assets == 0:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
return usdt_balance / total_assets
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating USDT ratio: {e}")
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
def get_position_size(self, symbol, confidence, current_price):
|
||
|
|
"""Calculate position size based on confidence level and USDT availability"""
|
||
|
|
try:
|
||
|
|
total_assets, usdt_balance, positions = self.calculate_total_assets()
|
||
|
|
|
||
|
|
if total_assets == 0:
|
||
|
|
logger.warning("Total assets is 0, unable to calculate position")
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
# Get current position value
|
||
|
|
current_pos_value = positions.get(symbol, {}).get('value', 0.0)
|
||
|
|
|
||
|
|
# Calculate USDT available ratio
|
||
|
|
usdt_ratio = usdt_balance / total_assets
|
||
|
|
|
||
|
|
# Adjust maximum position based on USDT availability
|
||
|
|
adjusted_max_single = self.max_single_position * min(1.0, usdt_ratio * 3) # More USDT allows larger positions
|
||
|
|
|
||
|
|
# Calculate maximum additional position
|
||
|
|
max_single_add = max(0, total_assets * adjusted_max_single - current_pos_value)
|
||
|
|
max_total_add = max(0, total_assets * self.max_total_position - (total_assets - usdt_balance))
|
||
|
|
|
||
|
|
max_add = min(max_single_add, max_total_add, usdt_balance)
|
||
|
|
|
||
|
|
# Adjust based on confidence level
|
||
|
|
multiplier = {
|
||
|
|
'HIGH': 0.8, # High confidence uses 80% of available amount
|
||
|
|
'MEDIUM': 0.5, # Medium confidence uses 50%
|
||
|
|
'LOW': 0.2 # Low confidence uses 20%
|
||
|
|
}.get(confidence, 0.2)
|
||
|
|
|
||
|
|
position_size = max_add * multiplier
|
||
|
|
|
||
|
|
# Ensure doesn't exceed available USDT balance
|
||
|
|
position_size = min(position_size, usdt_balance)
|
||
|
|
|
||
|
|
logger.info(f"Position calculation: {symbol}, total assets=${total_assets:.2f}, USDT=${usdt_balance:.2f}, "
|
||
|
|
f"current position=${current_pos_value:.2f}, suggested position=${position_size:.2f}")
|
||
|
|
|
||
|
|
return position_size
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error calculating position size: {e}")
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
def get_dynamic_position_size(self, symbol, confidence, technical_indicators, current_price):
|
||
|
|
"""Dynamic position calculation"""
|
||
|
|
try:
|
||
|
|
# Base position calculation
|
||
|
|
base_size = self.get_position_size(symbol, confidence, current_price)
|
||
|
|
if base_size == 0:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
# Get market state parameters
|
||
|
|
volatility = technical_indicators.get('volatility', 0)
|
||
|
|
trend_strength = technical_indicators.get('trend_strength', 0)
|
||
|
|
market_state = self.assess_market_state(technical_indicators)
|
||
|
|
|
||
|
|
# Volatility adjustment (high volatility reduces position)
|
||
|
|
volatility_factor = self._calculate_volatility_factor(volatility)
|
||
|
|
|
||
|
|
# Trend strength adjustment (strong trend increases position)
|
||
|
|
trend_factor = self._calculate_trend_factor(trend_strength)
|
||
|
|
|
||
|
|
# Market state adjustment
|
||
|
|
market_factor = self._calculate_market_factor(market_state)
|
||
|
|
|
||
|
|
# Confidence level adjustment
|
||
|
|
confidence_factor = self._calculate_confidence_factor(confidence)
|
||
|
|
|
||
|
|
# Calculate dynamic position
|
||
|
|
dynamic_size = base_size * volatility_factor * trend_factor * market_factor * confidence_factor
|
||
|
|
|
||
|
|
logger.info(f"Dynamic position calculation: {symbol}, base={base_size:.2f}, "
|
||
|
|
f"volatility factor={volatility_factor:.2f}, trend factor={trend_factor:.2f}, "
|
||
|
|
f"market factor={market_factor:.2f}, confidence factor={confidence_factor:.2f}, "
|
||
|
|
f"final={dynamic_size:.2f}")
|
||
|
|
|
||
|
|
return dynamic_size
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Dynamic position calculation error: {e}")
|
||
|
|
return self.get_position_size(symbol, confidence, current_price)
|
||
|
|
|
||
|
|
def _calculate_volatility_factor(self, volatility):
|
||
|
|
"""Calculate volatility adjustment factor"""
|
||
|
|
if not self.volatility_adjustment:
|
||
|
|
return 1.0
|
||
|
|
|
||
|
|
# Annualized volatility conversion and adjustment
|
||
|
|
if volatility > 0.8: # 80%+ annualized volatility
|
||
|
|
return 0.3
|
||
|
|
elif volatility > 0.6: # 60-80%
|
||
|
|
return 0.5
|
||
|
|
elif volatility > 0.4: # 40-60%
|
||
|
|
return 0.7
|
||
|
|
elif volatility > 0.2: # 20-40%
|
||
|
|
return 0.9
|
||
|
|
else: # <20%
|
||
|
|
return 1.0
|
||
|
|
|
||
|
|
def _calculate_trend_factor(self, trend_strength):
|
||
|
|
"""Calculate trend strength adjustment factor"""
|
||
|
|
if not self.trend_adjustment:
|
||
|
|
return 1.0
|
||
|
|
|
||
|
|
if trend_strength > 0.7: # Strong trend
|
||
|
|
return 1.3
|
||
|
|
elif trend_strength > 0.4: # Medium trend
|
||
|
|
return 1.1
|
||
|
|
elif trend_strength > 0.2: # Weak trend
|
||
|
|
return 1.0
|
||
|
|
else: # No trend
|
||
|
|
return 0.8
|
||
|
|
|
||
|
|
def _calculate_market_factor(self, market_state):
|
||
|
|
"""Calculate market state adjustment factor"""
|
||
|
|
if not self.market_sentiment_adjustment:
|
||
|
|
return 1.0
|
||
|
|
|
||
|
|
factors = {
|
||
|
|
'STRONG_BULL': 1.2,
|
||
|
|
'BULL': 1.1,
|
||
|
|
'NEUTRAL': 1.0,
|
||
|
|
'BEAR': 0.7,
|
||
|
|
'STRONG_BEAR': 0.5
|
||
|
|
}
|
||
|
|
return factors.get(market_state, 1.0)
|
||
|
|
|
||
|
|
def _calculate_confidence_factor(self, confidence):
|
||
|
|
"""Calculate confidence level adjustment factor"""
|
||
|
|
factors = {
|
||
|
|
'HIGH': 1.0,
|
||
|
|
'MEDIUM': 0.7,
|
||
|
|
'LOW': 0.4
|
||
|
|
}
|
||
|
|
return factors.get(confidence, 0.5)
|
||
|
|
|
||
|
|
def assess_market_state(self, technical_indicators):
|
||
|
|
"""Assess market state"""
|
||
|
|
try:
|
||
|
|
# Get key indicators
|
||
|
|
rsi = technical_indicators.get('rsi', 50)
|
||
|
|
if hasattr(rsi, '__len__'):
|
||
|
|
rsi = rsi.iloc[-1]
|
||
|
|
|
||
|
|
macd_line, signal_line, _ = technical_indicators.get('macd', (0, 0, 0))
|
||
|
|
if hasattr(macd_line, '__len__'):
|
||
|
|
macd_line, signal_line = macd_line.iloc[-1], signal_line.iloc[-1]
|
||
|
|
|
||
|
|
sma_20 = technical_indicators.get('sma_20', 0)
|
||
|
|
sma_50 = technical_indicators.get('sma_50', 0)
|
||
|
|
current_price = technical_indicators.get('current_price', 0)
|
||
|
|
|
||
|
|
if hasattr(sma_20, '__len__'):
|
||
|
|
sma_20, sma_50 = sma_20.iloc[-1], sma_50.iloc[-1]
|
||
|
|
|
||
|
|
# Calculate bullish signal score
|
||
|
|
bull_signals = 0
|
||
|
|
total_signals = 0
|
||
|
|
|
||
|
|
# RSI signal
|
||
|
|
if rsi > 50: bull_signals += 1
|
||
|
|
total_signals += 1
|
||
|
|
|
||
|
|
# MACD signal
|
||
|
|
if macd_line > signal_line: bull_signals += 1
|
||
|
|
total_signals += 1
|
||
|
|
|
||
|
|
# Moving average signal
|
||
|
|
if current_price > sma_20: bull_signals += 1
|
||
|
|
if current_price > sma_50: bull_signals += 1
|
||
|
|
total_signals += 2
|
||
|
|
|
||
|
|
# Determine market state
|
||
|
|
bull_ratio = bull_signals / total_signals
|
||
|
|
|
||
|
|
if bull_ratio >= 0.8:
|
||
|
|
return 'STRONG_BULL'
|
||
|
|
elif bull_ratio >= 0.6:
|
||
|
|
return 'BULL'
|
||
|
|
elif bull_ratio >= 0.4:
|
||
|
|
return 'NEUTRAL'
|
||
|
|
elif bull_ratio >= 0.2:
|
||
|
|
return 'BEAR'
|
||
|
|
else:
|
||
|
|
return 'STRONG_BEAR'
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error assessing market state: {e}")
|
||
|
|
return 'NEUTRAL'
|
||
|
|
|
||
|
|
def is_dust_position(self, symbol, current_price, base_amount):
|
||
|
|
"""Check if dust position"""
|
||
|
|
hold_value = base_amount * current_price if current_price else 0
|
||
|
|
return hold_value < self.dust_threshold_value
|
||
|
|
|
||
|
|
def is_low_position_ratio(self, symbol, total_assets, current_price, base_amount):
|
||
|
|
"""Check if position ratio is low"""
|
||
|
|
hold_value = base_amount * current_price if current_price else 0
|
||
|
|
return (hold_value / total_assets) < self.low_position_ratio if total_assets > 0 else False
|
||
|
|
|
||
|
|
def can_trade(self, symbol, amount, current_price):
|
||
|
|
"""Check if can trade"""
|
||
|
|
try:
|
||
|
|
# Check daily trade count
|
||
|
|
today = datetime.now().date()
|
||
|
|
if symbol not in self.daily_trade_count:
|
||
|
|
self.daily_trade_count[symbol] = {'date': today, 'count': 0}
|
||
|
|
|
||
|
|
if self.daily_trade_count[symbol]['date'] != today:
|
||
|
|
self.daily_trade_count[symbol] = {'date': today, 'count': 0}
|
||
|
|
|
||
|
|
if self.daily_trade_count[symbol]['count'] >= self.max_daily_trades:
|
||
|
|
logger.warning(f"{symbol} daily trade count reached {self.max_daily_trades} limit")
|
||
|
|
return False
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error checking trade conditions: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def increment_trade_count(self, symbol):
|
||
|
|
"""Increment trade count"""
|
||
|
|
try:
|
||
|
|
today = datetime.now().date()
|
||
|
|
if symbol in self.daily_trade_count and self.daily_trade_count[symbol]['date'] == today:
|
||
|
|
self.daily_trade_count[symbol]['count'] += 1
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error incrementing trade count: {e}")
|
||
|
|
|
||
|
|
class TradingStrategy:
|
||
|
|
"""Trading Strategy (Comprehensive Optimized Version)"""
|
||
|
|
|
||
|
|
def __init__(self, symbol, config, api, risk_manager, deepseek):
|
||
|
|
self.symbol = symbol
|
||
|
|
self.config = config
|
||
|
|
self.api = api
|
||
|
|
self.risk_manager = risk_manager
|
||
|
|
self.deepseek = deepseek
|
||
|
|
self.technical_analyzer = TechnicalAnalyzer()
|
||
|
|
self.db = DatabaseManager() # Use default configuration
|
||
|
|
|
||
|
|
# Position status
|
||
|
|
self.base_amount = 0.0
|
||
|
|
self.entry_price = 0.0
|
||
|
|
|
||
|
|
# Dynamic stop-loss monitoring - using environment variable configured interval
|
||
|
|
self.monitor_interval = 300 # Default 5 minutes
|
||
|
|
if hasattr(risk_manager, 'monitor_interval'):
|
||
|
|
self.monitor_interval = risk_manager.monitor_interval
|
||
|
|
self.monitor_running = False
|
||
|
|
self.monitor_thread = None
|
||
|
|
|
||
|
|
# Running status control
|
||
|
|
self.strategy_running = False
|
||
|
|
|
||
|
|
# Load position status
|
||
|
|
self.load_position()
|
||
|
|
|
||
|
|
# Start dynamic stop-loss monitoring
|
||
|
|
self.start_dynamic_stop_monitor()
|
||
|
|
|
||
|
|
def load_position(self):
|
||
|
|
"""Load position status from database"""
|
||
|
|
try:
|
||
|
|
position_data = self.db.load_position(self.symbol)
|
||
|
|
if position_data:
|
||
|
|
self.base_amount = position_data['base_amount']
|
||
|
|
self.entry_price = position_data['entry_price']
|
||
|
|
logger.info(f"Loaded {self.symbol} position: currency quantity={self.base_amount:.10f}, entry price=${self.entry_price:.2f}")
|
||
|
|
else:
|
||
|
|
# If no database record, sync with exchange position
|
||
|
|
self.sync_with_exchange()
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error loading position status: {e}")
|
||
|
|
self.sync_with_exchange()
|
||
|
|
|
||
|
|
def save_position(self):
|
||
|
|
"""Save position status to database"""
|
||
|
|
try:
|
||
|
|
if self.base_amount > 0:
|
||
|
|
self.db.save_position(self.symbol, self.base_amount, self.entry_price)
|
||
|
|
else:
|
||
|
|
# If position is 0, delete record
|
||
|
|
self.db.delete_position(self.symbol)
|
||
|
|
# Also delete dynamic stop record
|
||
|
|
self.db.delete_dynamic_stop(self.symbol)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error saving position status: {e}")
|
||
|
|
|
||
|
|
def sync_with_exchange(self):
|
||
|
|
"""Sync with exchange position"""
|
||
|
|
try:
|
||
|
|
logger.debug(f"Starting {self.symbol} position status sync")
|
||
|
|
|
||
|
|
# Get all currency balances
|
||
|
|
balances = self.api.get_currency_balances()
|
||
|
|
if not balances:
|
||
|
|
logger.warning("Unable to get currency balances")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Extract base currency
|
||
|
|
base_currency = self.symbol.split('-')[0]
|
||
|
|
|
||
|
|
# Update local balance
|
||
|
|
if base_currency in balances:
|
||
|
|
new_base_amount = balances[base_currency].get('amount', 0.0)
|
||
|
|
|
||
|
|
# If position quantity changes, print log
|
||
|
|
if abs(new_base_amount - self.base_amount) > 1e-10:
|
||
|
|
logger.info(f"Synced {self.symbol} latest balance: {base_currency}={new_base_amount:.10f}")
|
||
|
|
|
||
|
|
# If position decreases but entry price not updated, keep original entry price (for calculating profit/loss)
|
||
|
|
if new_base_amount < self.base_amount and self.entry_price > 0:
|
||
|
|
# Position decreased, reset entry price to conservative estimate (current price)
|
||
|
|
if new_base_amount <= 0:
|
||
|
|
self.entry_price = 0
|
||
|
|
logger.info(f"{self.symbol} position cleared, reset entry price to 0")
|
||
|
|
else:
|
||
|
|
current_price = self.api.get_current_price(self.symbol)
|
||
|
|
if current_price:
|
||
|
|
self.entry_price = current_price
|
||
|
|
logger.info(f"{self.symbol} partial sell, reset entry price to current price: ${current_price:.2f}")
|
||
|
|
else:
|
||
|
|
logger.warning(f"{self.symbol} partial sell but unable to get current price, keep original entry price")
|
||
|
|
|
||
|
|
# If from no position to having position, and no entry price record
|
||
|
|
elif self.base_amount == 0 and new_base_amount > 0 and self.entry_price == 0:
|
||
|
|
# Try to load historical entry price from database
|
||
|
|
position_data = self.db.load_position(self.symbol)
|
||
|
|
if position_data and position_data['entry_price'] > 0:
|
||
|
|
self.entry_price = position_data['entry_price']
|
||
|
|
logger.info(f"Restored entry price from database: ${self.entry_price:.2f}")
|
||
|
|
else:
|
||
|
|
# Get current price as reference (but note this is reference price)
|
||
|
|
current_price = self.api.get_current_price(self.symbol)
|
||
|
|
if current_price:
|
||
|
|
self.entry_price = current_price
|
||
|
|
logger.info(f"Set reference entry price: ${current_price:.2f} (Note: This is reference price, actual entry price may differ)")
|
||
|
|
|
||
|
|
self.base_amount = new_base_amount
|
||
|
|
|
||
|
|
else:
|
||
|
|
self.base_amount = 0.0
|
||
|
|
# Don't clear entry price, keep for historical record analysis
|
||
|
|
logger.info(f"Synced {self.symbol}: No position")
|
||
|
|
|
||
|
|
# Save position status
|
||
|
|
self.save_position()
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error syncing exchange position: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def get_actual_position_from_exchange(self):
|
||
|
|
"""Get actual position data from exchange"""
|
||
|
|
try:
|
||
|
|
balances = self.api.get_currency_balances()
|
||
|
|
if not balances:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
base_currency = self.symbol.split('-')[0]
|
||
|
|
return balances.get(base_currency, {}).get('amount', 0.0)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error getting exchange position: {e}")
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
def analyze_market(self):
|
||
|
|
"""Comprehensive market analysis (optimized version)"""
|
||
|
|
try:
|
||
|
|
# Get market data
|
||
|
|
df = self.api.get_market_data(self.symbol, self.config['timeframe'])
|
||
|
|
if df is None or len(df) < 20:
|
||
|
|
logger.error(f"Failed to get {self.symbol} market data")
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Calculate technical indicators
|
||
|
|
technical_indicators = self.technical_analyzer.calculate_indicators(df)
|
||
|
|
if not technical_indicators:
|
||
|
|
logger.warning(f"Failed to calculate {self.symbol} technical indicators")
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Weighted technical signals
|
||
|
|
weighted_signals = self.technical_analyzer.generate_weighted_signals(df)
|
||
|
|
|
||
|
|
# DeepSeek AI analysis
|
||
|
|
deepseek_analysis = self.deepseek.analyze_market(self.symbol, df, technical_indicators)
|
||
|
|
|
||
|
|
# Technical signals
|
||
|
|
technical_signals = self.technical_analyzer.generate_signals(df)
|
||
|
|
|
||
|
|
return {
|
||
|
|
'deepseek': deepseek_analysis,
|
||
|
|
'technical_signals': technical_signals,
|
||
|
|
'weighted_signals': weighted_signals,
|
||
|
|
'technical_indicators': technical_indicators,
|
||
|
|
'current_price': df['close'].iloc[-1],
|
||
|
|
'market_data': df
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"{self.symbol} market analysis error: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def make_enhanced_decision(self, analysis):
|
||
|
|
"""Enhanced trading decision"""
|
||
|
|
if analysis is None:
|
||
|
|
return {'action': 'HOLD', 'confidence': 'LOW', 'reason': 'Insufficient analysis data'}
|
||
|
|
|
||
|
|
deepseek_signal = analysis['deepseek']
|
||
|
|
weighted_signals = analysis['weighted_signals']
|
||
|
|
technical_indicators = analysis['technical_indicators']
|
||
|
|
market_data = analysis['market_data']
|
||
|
|
|
||
|
|
# Build decision matrix
|
||
|
|
decision_matrix = self._build_decision_matrix(deepseek_signal, weighted_signals, technical_indicators, market_data)
|
||
|
|
|
||
|
|
return self._finalize_decision(decision_matrix)
|
||
|
|
|
||
|
|
def _build_decision_matrix(self, ai_signal, weighted_signal, technical_indicators, market_data):
|
||
|
|
"""Build decision matrix"""
|
||
|
|
matrix = {
|
||
|
|
'ai_weight': 0.6, # AI analysis weight
|
||
|
|
'technical_weight': 0.2, # Traditional technical indicator weight
|
||
|
|
'reversal_weight': 0.05, # Reversal signal weight
|
||
|
|
'support_resistance_weight': 0.1, # Support resistance weight
|
||
|
|
'market_weight': 0.05 # Market state weight
|
||
|
|
}
|
||
|
|
|
||
|
|
# AI signal scoring
|
||
|
|
ai_score = self._score_ai_signal(ai_signal) if ai_signal else 0.5
|
||
|
|
|
||
|
|
# Technical signal scoring
|
||
|
|
tech_score = self._score_technical_signal(weighted_signal)
|
||
|
|
|
||
|
|
# Market state scoring
|
||
|
|
market_score = self._score_market_condition(technical_indicators)
|
||
|
|
|
||
|
|
# Reversal signal scoring
|
||
|
|
reversal_signals = TechnicalAnalyzer.detect_reversal_patterns(market_data)
|
||
|
|
reversal_score = 0.5
|
||
|
|
if reversal_signals:
|
||
|
|
# Score based on reversal signal quantity and strength
|
||
|
|
strong_signals = [s for s in reversal_signals if s['strength'] == 'STRONG']
|
||
|
|
reversal_score = 0.5 + (len(strong_signals) * 0.1) + (len(reversal_signals) * 0.05)
|
||
|
|
reversal_score = min(0.9, reversal_score)
|
||
|
|
|
||
|
|
# Support resistance scoring
|
||
|
|
sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data)
|
||
|
|
sr_score = 0.5
|
||
|
|
if sr_levels:
|
||
|
|
# Near support level adds points, near resistance level subtracts points
|
||
|
|
if sr_levels['current_vs_support'] > -2: # Very close to support
|
||
|
|
sr_score = 0.7
|
||
|
|
elif sr_levels['current_vs_resistance'] < 2: # Very close to resistance
|
||
|
|
sr_score = 0.3
|
||
|
|
|
||
|
|
# Comprehensive scoring
|
||
|
|
total_score = (
|
||
|
|
ai_score * matrix['ai_weight'] +
|
||
|
|
tech_score * matrix['technical_weight'] +
|
||
|
|
reversal_score * matrix['reversal_weight'] +
|
||
|
|
sr_score * matrix['support_resistance_weight'] +
|
||
|
|
market_score * matrix['market_weight']
|
||
|
|
)
|
||
|
|
logger.info(f"[{self.symbol}] Decision matrix details: "
|
||
|
|
f"AI score={ai_score:.3f}, technical score={tech_score:.3f}, "
|
||
|
|
f"market score={market_score:.3f}, reversal score={reversal_score:.3f}, "
|
||
|
|
f"support resistance score={sr_score:.3f}, total score={total_score:.3f}")
|
||
|
|
|
||
|
|
# Determine action and confidence
|
||
|
|
if total_score > 0.6:
|
||
|
|
action = 'BUY'
|
||
|
|
confidence = 'HIGH' if total_score > 0.75 else 'MEDIUM'
|
||
|
|
elif total_score < 0.4:
|
||
|
|
action = 'SELL'
|
||
|
|
confidence = 'HIGH' if total_score < 0.25 else 'MEDIUM'
|
||
|
|
else:
|
||
|
|
action = 'HOLD'
|
||
|
|
confidence = 'MEDIUM' if abs(total_score - 0.5) > 0.1 else 'LOW'
|
||
|
|
|
||
|
|
return {
|
||
|
|
'total_score': total_score,
|
||
|
|
'action': action,
|
||
|
|
'confidence': confidence,
|
||
|
|
'components': {
|
||
|
|
'ai_score': ai_score,
|
||
|
|
'tech_score': tech_score,
|
||
|
|
'market_score': market_score,
|
||
|
|
'reversal_score': reversal_score,
|
||
|
|
'sr_score': sr_score
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
def _score_ai_signal(self, ai_signal):
|
||
|
|
"""AI signal scoring"""
|
||
|
|
if not ai_signal:
|
||
|
|
logger.warning("AI signal is empty, using neutral score")
|
||
|
|
return 0.5
|
||
|
|
|
||
|
|
action = ai_signal.get('action', 'HOLD')
|
||
|
|
confidence = ai_signal.get('confidence', 'MEDIUM')
|
||
|
|
|
||
|
|
# Correction: Redesign scoring logic to ensure score has differentiation
|
||
|
|
if action == 'BUY':
|
||
|
|
base_score = 0.7 # Buy base score
|
||
|
|
elif action == 'SELL':
|
||
|
|
base_score = 0.3 # Sell base score
|
||
|
|
else: # HOLD
|
||
|
|
base_score = 0.5 # Hold neutral score
|
||
|
|
|
||
|
|
# Confidence adjustment - ensure final score is within reasonable range
|
||
|
|
confidence_adjustments = {
|
||
|
|
'HIGH': 0.2, # High confidence +0.2
|
||
|
|
'MEDIUM': 0.0, # Medium confidence unchanged
|
||
|
|
'LOW': -0.2 # Low confidence -0.2
|
||
|
|
}
|
||
|
|
|
||
|
|
adjustment = confidence_adjustments.get(confidence, 0.0)
|
||
|
|
final_score = base_score + adjustment
|
||
|
|
|
||
|
|
# Ensure within 0-1 range
|
||
|
|
final_score = max(0.1, min(0.9, final_score))
|
||
|
|
|
||
|
|
logger.debug(f"AI signal scoring: action={action}, confidence={confidence}, "
|
||
|
|
f"base={base_score}, adjustment={adjustment}, final={final_score}")
|
||
|
|
|
||
|
|
return final_score
|
||
|
|
|
||
|
|
def _score_technical_signal(self, weighted_signal):
|
||
|
|
"""Technical signal scoring"""
|
||
|
|
if not weighted_signal:
|
||
|
|
logger.warning("Technical signal is empty, using neutral score")
|
||
|
|
return 0.5
|
||
|
|
|
||
|
|
action = weighted_signal.get('action', 'HOLD')
|
||
|
|
score = weighted_signal.get('score', 0.5)
|
||
|
|
signal_count = weighted_signal.get('signal_count', 0)
|
||
|
|
|
||
|
|
# Adjust based on signal strength and quantity
|
||
|
|
if action == 'BUY':
|
||
|
|
# Buy signal: 0.5-1.0 range
|
||
|
|
if score > 0.7 and signal_count >= 3:
|
||
|
|
final_score = 0.8
|
||
|
|
elif score > 0.6:
|
||
|
|
final_score = 0.7
|
||
|
|
else:
|
||
|
|
final_score = 0.6
|
||
|
|
elif action == 'SELL':
|
||
|
|
# Sell signal: 0.0-0.5 range
|
||
|
|
if score < 0.3 and signal_count >= 3:
|
||
|
|
final_score = 0.2
|
||
|
|
elif score < 0.4:
|
||
|
|
final_score = 0.3
|
||
|
|
else:
|
||
|
|
final_score = 0.4
|
||
|
|
else: # HOLD
|
||
|
|
final_score = 0.5
|
||
|
|
|
||
|
|
logger.debug(f"Technical signal scoring: action={action}, score={score}, "
|
||
|
|
f"signal_count={signal_count}, final={final_score}")
|
||
|
|
|
||
|
|
return final_score
|
||
|
|
|
||
|
|
def _score_market_condition(self, technical_indicators):
|
||
|
|
"""Market state scoring"""
|
||
|
|
try:
|
||
|
|
# Assess market state based on multiple indicators
|
||
|
|
rsi = technical_indicators.get('rsi', 50)
|
||
|
|
if hasattr(rsi, '__len__'):
|
||
|
|
rsi = rsi.iloc[-1]
|
||
|
|
|
||
|
|
trend_strength = technical_indicators.get('trend_strength', 0)
|
||
|
|
volatility = technical_indicators.get('volatility', 0)
|
||
|
|
|
||
|
|
# RSI scoring (30-70 is neutral)
|
||
|
|
rsi_score = 0.5
|
||
|
|
if rsi < 30: # Oversold
|
||
|
|
rsi_score = 0.8
|
||
|
|
elif rsi > 70: # Overbought
|
||
|
|
rsi_score = 0.2
|
||
|
|
|
||
|
|
# Trend strength scoring
|
||
|
|
trend_score = 0.5 + (trend_strength * 0.3) # Strong trend favors buying
|
||
|
|
|
||
|
|
# Volatility scoring (medium volatility is best)
|
||
|
|
vol_score = 0.7 if 0.3 < volatility < 0.6 else 0.5
|
||
|
|
|
||
|
|
# Comprehensive scoring
|
||
|
|
return (rsi_score + trend_score + vol_score) / 3
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Market state scoring error: {e}")
|
||
|
|
return 0.5
|
||
|
|
|
||
|
|
def _finalize_decision(self, decision_matrix):
|
||
|
|
"""Final decision"""
|
||
|
|
action = decision_matrix['action']
|
||
|
|
confidence = decision_matrix['confidence']
|
||
|
|
total_score = decision_matrix['total_score']
|
||
|
|
|
||
|
|
reason = f"Comprehensive score: {total_score:.3f} (AI: {decision_matrix['components']['ai_score']:.3f}, "
|
||
|
|
reason += f"Technical: {decision_matrix['components']['tech_score']:.3f}, "
|
||
|
|
reason += f"Market: {decision_matrix['components']['market_score']:.3f},"
|
||
|
|
reason += f"Reversal: {decision_matrix['components']['reversal_score']:.3f},"
|
||
|
|
reason += f"Support Resistance: {decision_matrix['components']['sr_score']:.3f}),"
|
||
|
|
|
||
|
|
return {
|
||
|
|
'action': action,
|
||
|
|
'confidence': confidence,
|
||
|
|
'reason': reason,
|
||
|
|
'total_score': total_score,
|
||
|
|
'source': 'Enhanced Decision Matrix'
|
||
|
|
}
|
||
|
|
|
||
|
|
def execute_trade(self, decision, current_price):
|
||
|
|
"""Execute trade"""
|
||
|
|
if decision['action'] == 'HOLD':
|
||
|
|
logger.info(f"[{self.symbol}] Decision is HOLD, not executing trade")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Sync exchange position
|
||
|
|
self.sync_with_exchange()
|
||
|
|
actual_position = self.base_amount # Use synced position
|
||
|
|
|
||
|
|
logger.info(f"[{self.symbol}] Current position: {self.base_amount:.10f}")
|
||
|
|
|
||
|
|
# Only get needed total asset information
|
||
|
|
total_assets, available_usdt, _ = self.risk_manager.calculate_total_assets()
|
||
|
|
|
||
|
|
if decision['action'] == 'BUY':
|
||
|
|
# Get market analysis for dynamic position calculation
|
||
|
|
market_analysis = self.analyze_market()
|
||
|
|
if market_analysis is None:
|
||
|
|
logger.warning(f"[{self.symbol}] Unable to get market analysis, using base position")
|
||
|
|
buy_amount = self.risk_manager.get_position_size(self.symbol, decision['confidence'], current_price)
|
||
|
|
else:
|
||
|
|
# Use dynamic position calculation
|
||
|
|
buy_amount = self.risk_manager.get_dynamic_position_size(
|
||
|
|
self.symbol, decision['confidence'], market_analysis['technical_indicators'], current_price
|
||
|
|
)
|
||
|
|
|
||
|
|
if buy_amount == 0:
|
||
|
|
logger.warning(f"[{self.symbol}] Calculated position size is 0, skipping buy")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Get exchange minimum trade quantity for buy check
|
||
|
|
min_sz, _ = self.api.get_instrument_info(self.symbol)
|
||
|
|
if min_sz is None:
|
||
|
|
min_sz = self.api.get_default_min_size(self.symbol)
|
||
|
|
|
||
|
|
# Calculate base currency quantity to buy
|
||
|
|
estimated_base_amount = buy_amount / current_price if current_price > 0 else 0
|
||
|
|
|
||
|
|
logger.info(f"[{self.symbol}] Pre-buy check: estimated quantity={estimated_base_amount:.10f}, "
|
||
|
|
f"minimum trade quantity={min_sz}, available USDT=${available_usdt:.2f}, "
|
||
|
|
f"buy amount=${buy_amount:.2f}")
|
||
|
|
|
||
|
|
# Check if buy quantity meets exchange minimum requirement
|
||
|
|
if estimated_base_amount < min_sz:
|
||
|
|
logger.warning(f"[{self.symbol}] Estimated buy quantity {estimated_base_amount:.10f} less than minimum trade quantity {min_sz}, canceling buy")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Check if quote currency balance is sufficient
|
||
|
|
if available_usdt < buy_amount:
|
||
|
|
logger.warning(f"[{self.symbol}] Quote currency balance insufficient: need ${buy_amount:.2f}, current ${available_usdt:.2f}")
|
||
|
|
return
|
||
|
|
|
||
|
|
logger.info(f"[{self.symbol}] Creating buy order: amount=${buy_amount:.2f}, price=${current_price:.2f}")
|
||
|
|
order_id = self.api.create_order(
|
||
|
|
symbol=self.symbol,
|
||
|
|
side='buy',
|
||
|
|
amount=buy_amount
|
||
|
|
)
|
||
|
|
|
||
|
|
if order_id:
|
||
|
|
# Wait for order completion
|
||
|
|
order_status = self.api.wait_for_order_completion(self.symbol, order_id)
|
||
|
|
if order_status is None:
|
||
|
|
logger.error(f"[{self.symbol}] Order not completed")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Get actual fill price and quantity
|
||
|
|
fill_price = order_status['avgPx']
|
||
|
|
fill_amount = order_status['accFillSz']
|
||
|
|
|
||
|
|
# Update position status
|
||
|
|
self.base_amount += fill_amount
|
||
|
|
|
||
|
|
# Update average entry price (weighted average)
|
||
|
|
if self.entry_price > 0:
|
||
|
|
# If adding position, calculate weighted average price
|
||
|
|
self.entry_price = (self.entry_price * (self.base_amount - fill_amount) + fill_price * fill_amount) / self.base_amount
|
||
|
|
logger.info(f"[{self.symbol}] Position addition operation, updated average entry price: ${self.entry_price:.2f}")
|
||
|
|
else:
|
||
|
|
self.entry_price = fill_price
|
||
|
|
|
||
|
|
logger.info(f"[{self.symbol}] Buy successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}")
|
||
|
|
logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}")
|
||
|
|
|
||
|
|
# Sync exchange position
|
||
|
|
self.sync_with_exchange()
|
||
|
|
|
||
|
|
# Set dynamic stop-loss
|
||
|
|
trailing_percent = self.risk_manager.trailing_stop_percent
|
||
|
|
self.db.set_dynamic_stop(self.symbol, fill_price, trailing_percent=trailing_percent)
|
||
|
|
logger.info(f"[{self.symbol}] Set dynamic stop-loss: initial price=${fill_price:.2f}, trailing_percent={trailing_percent:.1%}")
|
||
|
|
|
||
|
|
# Note: sync_with_exchange already saves position, no need to save again here
|
||
|
|
self.risk_manager.increment_trade_count(self.symbol)
|
||
|
|
else:
|
||
|
|
logger.error(f"[{self.symbol}] Buy order creation failed")
|
||
|
|
|
||
|
|
elif decision['action'] == 'SELL':
|
||
|
|
if actual_position <= 0:
|
||
|
|
logger.info(f"[{self.symbol}] Exchange actual position is 0, ignoring sell signal")
|
||
|
|
# Update local status
|
||
|
|
self.base_amount = 0
|
||
|
|
self.entry_price = 0
|
||
|
|
self.save_position()
|
||
|
|
return
|
||
|
|
|
||
|
|
# Prepare position information for AI analysis
|
||
|
|
position_info = self._prepare_position_info(actual_position, current_price, total_assets)
|
||
|
|
|
||
|
|
# Get market analysis data (for technical indicators)
|
||
|
|
market_analysis = self.analyze_market()
|
||
|
|
|
||
|
|
# Use DeepSeek to analyze sell proportion
|
||
|
|
try:
|
||
|
|
sell_proportion = self.deepseek.analyze_sell_proportion(
|
||
|
|
self.symbol, position_info, market_analysis, current_price
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"[{self.symbol}] DeepSeek sell proportion analysis failed: {e}, using fallback decision")
|
||
|
|
sell_proportion = self.deepseek._get_fallback_sell_proportion(position_info, decision['confidence'])
|
||
|
|
|
||
|
|
# Calculate sell quantity based on AI suggestion
|
||
|
|
sell_amount = actual_position * sell_proportion
|
||
|
|
|
||
|
|
# Ensure sell quantity doesn't exceed actual position
|
||
|
|
sell_amount = min(sell_amount, actual_position)
|
||
|
|
|
||
|
|
logger.info(f"[{self.symbol}] AI suggested sell proportion: {sell_proportion:.1%}, calculated sell quantity: {sell_amount:.10f}")
|
||
|
|
|
||
|
|
# Get exchange minimum trade quantity for sell check
|
||
|
|
min_sz, _ = self.api.get_instrument_info(self.symbol)
|
||
|
|
if min_sz is None:
|
||
|
|
min_sz = self.api.get_default_min_size(self.symbol)
|
||
|
|
|
||
|
|
# Check if sell quantity is less than exchange minimum trade quantity
|
||
|
|
if sell_amount < min_sz:
|
||
|
|
if sell_proportion < 1.0: # Partial sell
|
||
|
|
logger.info(f"[{self.symbol}] Sell quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, ignoring this partial sell")
|
||
|
|
return
|
||
|
|
else: # Full sell
|
||
|
|
logger.info(f"[{self.symbol}] Position quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, dust position, ignoring sell")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Record sell decision details
|
||
|
|
logger.info(f"[{self.symbol}] Final sell decision: proportion={sell_proportion:.1%}, quantity={sell_amount:.10f}, "
|
||
|
|
f"reason={decision.get('reason', 'AI decision')}")
|
||
|
|
|
||
|
|
# Execute sell order
|
||
|
|
order_id = self.api.create_order(
|
||
|
|
symbol=self.symbol,
|
||
|
|
side='sell',
|
||
|
|
amount=sell_amount
|
||
|
|
)
|
||
|
|
|
||
|
|
if order_id:
|
||
|
|
# Wait for order completion
|
||
|
|
order_status = self.api.wait_for_order_completion(self.symbol, order_id)
|
||
|
|
if order_status is None:
|
||
|
|
logger.error(f"[{self.symbol}] Order not completed")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Get actual fill price and quantity
|
||
|
|
fill_price = order_status['avgPx']
|
||
|
|
fill_amount = order_status['accFillSz']
|
||
|
|
|
||
|
|
# Calculate profit
|
||
|
|
profit = (fill_price - self.entry_price) * fill_amount
|
||
|
|
profit_percent = (fill_price / self.entry_price - 1) * 100 if self.entry_price > 0 else 0
|
||
|
|
|
||
|
|
# Sync exchange position
|
||
|
|
self.sync_with_exchange()
|
||
|
|
|
||
|
|
logger.info(f"[{self.symbol}] Sell successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}")
|
||
|
|
logger.info(f"[{self.symbol}] Profit: ${profit:.2f} ({profit_percent:+.2f}%)")
|
||
|
|
logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}")
|
||
|
|
|
||
|
|
# Save trade record
|
||
|
|
self.db.save_trade_record(self.symbol, 'sell', fill_amount, fill_price, order_id)
|
||
|
|
|
||
|
|
# If fully sold, reset status
|
||
|
|
if self.base_amount <= 0:
|
||
|
|
self.entry_price = 0
|
||
|
|
self.db.delete_dynamic_stop(self.symbol)
|
||
|
|
logger.info(f"[{self.symbol}] Position cleared, removed dynamic stop-loss")
|
||
|
|
else:
|
||
|
|
# After partial sell, update dynamic stop-loss
|
||
|
|
self.db.set_dynamic_stop(self.symbol, self.entry_price, trailing_percent=0.03, multiplier=2)
|
||
|
|
logger.info(f"[{self.symbol}] After partial sell, reset dynamic stop-loss")
|
||
|
|
|
||
|
|
# Note: sync_with_exchange already saves position, no need to save again here
|
||
|
|
self.risk_manager.increment_trade_count(self.symbol)
|
||
|
|
else:
|
||
|
|
logger.error(f"[{self.symbol}] Sell order creation failed")
|
||
|
|
|
||
|
|
def _prepare_position_info(self, actual_position, current_price, total_assets):
|
||
|
|
"""Prepare position information for AI analysis"""
|
||
|
|
try:
|
||
|
|
# Calculate position value
|
||
|
|
position_value = actual_position * current_price
|
||
|
|
|
||
|
|
# Calculate position ratio
|
||
|
|
position_ratio = position_value / total_assets if total_assets > 0 else 0
|
||
|
|
|
||
|
|
# Calculate profit ratio
|
||
|
|
profit_ratio = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0
|
||
|
|
|
||
|
|
# Calculate distance to stop-loss/take-profit
|
||
|
|
stop_loss_price = self.entry_price * (1 - self.risk_manager.stop_loss) if self.entry_price > 0 else 0
|
||
|
|
take_profit_price = self.entry_price * (1 + self.risk_manager.take_profit) if self.entry_price > 0 else 0
|
||
|
|
|
||
|
|
distance_to_stop_loss = (current_price - stop_loss_price) / current_price if current_price > 0 else 0
|
||
|
|
distance_to_take_profit = (take_profit_price - current_price) / current_price if current_price > 0 else 0
|
||
|
|
|
||
|
|
# Get market volatility
|
||
|
|
market_data = self.api.get_market_data(self.symbol, '1H', limit=24)
|
||
|
|
if market_data is not None and len(market_data) > 0:
|
||
|
|
market_volatility = market_data['close'].pct_change().std() * 100
|
||
|
|
else:
|
||
|
|
market_volatility = 0
|
||
|
|
|
||
|
|
# Get trend strength
|
||
|
|
market_analysis = self.analyze_market()
|
||
|
|
trend_strength = 0
|
||
|
|
if market_analysis and 'technical_indicators' in market_analysis:
|
||
|
|
trend_strength = market_analysis['technical_indicators'].get('trend_strength', 0)
|
||
|
|
|
||
|
|
position_info = {
|
||
|
|
'base_amount': actual_position,
|
||
|
|
'entry_price': self.entry_price,
|
||
|
|
'current_price': current_price,
|
||
|
|
'position_value': position_value,
|
||
|
|
'position_ratio': position_ratio,
|
||
|
|
'profit_ratio': profit_ratio,
|
||
|
|
'distance_to_stop_loss': distance_to_stop_loss,
|
||
|
|
'distance_to_take_profit': distance_to_take_profit,
|
||
|
|
'market_volatility': market_volatility,
|
||
|
|
'trend_strength': trend_strength,
|
||
|
|
'total_assets': total_assets
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.debug(f"[{self.symbol}] Position analysis information: ratio={position_ratio:.2%}, profit={profit_ratio:+.2%}, "
|
||
|
|
f"distance to stop-loss={distance_to_stop_loss:.2%}, volatility={market_volatility:.2f}%")
|
||
|
|
|
||
|
|
return position_info
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"[{self.symbol}] Error preparing position information: {e}")
|
||
|
|
# Return basic position information
|
||
|
|
return {
|
||
|
|
'base_amount': actual_position,
|
||
|
|
'entry_price': self.entry_price,
|
||
|
|
'current_price': current_price,
|
||
|
|
'position_value': actual_position * current_price,
|
||
|
|
'position_ratio': 0,
|
||
|
|
'profit_ratio': 0,
|
||
|
|
'distance_to_stop_loss': 0,
|
||
|
|
'distance_to_take_profit': 0,
|
||
|
|
'market_volatility': 0,
|
||
|
|
'trend_strength': 0,
|
||
|
|
'total_assets': total_assets
|
||
|
|
}
|
||
|
|
|
||
|
|
def start_dynamic_stop_monitor(self):
|
||
|
|
"""Start dynamic stop-loss monitoring"""
|
||
|
|
if self.monitor_running and self.monitor_thread and self.monitor_thread.is_alive():
|
||
|
|
logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring already running")
|
||
|
|
return
|
||
|
|
|
||
|
|
self.monitor_running = True
|
||
|
|
self.monitor_thread = threading.Thread(target=self._dynamic_stop_monitor_loop, name=f"StopMonitor-{self.symbol}")
|
||
|
|
self.monitor_thread.daemon = True
|
||
|
|
self.monitor_thread.start()
|
||
|
|
logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring started")
|
||
|
|
|
||
|
|
def stop_dynamic_stop_monitor(self):
|
||
|
|
"""Stop dynamic stop-loss monitoring"""
|
||
|
|
self.monitor_running = False
|
||
|
|
if self.monitor_thread and self.monitor_thread.is_alive():
|
||
|
|
self.monitor_thread.join(timeout=5)
|
||
|
|
if self.monitor_thread.is_alive():
|
||
|
|
logger.warning(f"{self.symbol} dynamic stop-loss monitoring thread didn't exit normally")
|
||
|
|
else:
|
||
|
|
logger.info(f"{self.symbol} dynamic stop-loss monitoring stopped")
|
||
|
|
|
||
|
|
def _dynamic_stop_monitor_loop(self):
|
||
|
|
"""Dynamic stop-loss monitoring loop"""
|
||
|
|
while self.monitor_running:
|
||
|
|
try:
|
||
|
|
# Only monitor when there is position
|
||
|
|
if self.base_amount > 0:
|
||
|
|
self.check_dynamic_stops()
|
||
|
|
|
||
|
|
# Monitor every 5 minutes
|
||
|
|
time.sleep(self.monitor_interval)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"[{self.symbol}] Dynamic stop-loss monitoring error: {e}")
|
||
|
|
time.sleep(60) # Wait 1 minute before continuing when error occurs
|
||
|
|
|
||
|
|
def check_dynamic_stops(self):
|
||
|
|
"""Check dynamic stop-loss conditions"""
|
||
|
|
try:
|
||
|
|
# Get current price
|
||
|
|
current_price = self.api.get_current_price(self.symbol)
|
||
|
|
if not current_price:
|
||
|
|
logger.warning(f"[{self.symbol}] Unable to get current price, skipping stop-loss check")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Get dynamic stop-loss information
|
||
|
|
stop_info = self.db.get_dynamic_stop(self.symbol)
|
||
|
|
if not stop_info:
|
||
|
|
logger.debug(f"[{self.symbol}] No dynamic stop-loss settings")
|
||
|
|
return
|
||
|
|
|
||
|
|
stop_loss_price = stop_info['current_stop_loss']
|
||
|
|
take_profit_price = stop_info['current_take_profit']
|
||
|
|
trailing_percent = stop_info['trailing_percent']
|
||
|
|
|
||
|
|
logger.debug(f"[{self.symbol}] Dynamic stop-loss check: current price=${current_price:.2f}, stop-loss=${stop_loss_price:.2f}, take-profit=${take_profit_price:.2f}")
|
||
|
|
|
||
|
|
# Check stop-loss condition
|
||
|
|
if current_price <= stop_loss_price:
|
||
|
|
logger.warning(f"⚠️ {self.symbol} triggered dynamic stop-loss! Current price=${current_price:.2f} <= stop-loss price=${stop_loss_price:.2f}")
|
||
|
|
self.execute_trade({
|
||
|
|
'action': 'SELL',
|
||
|
|
'confidence': 'HIGH',
|
||
|
|
'reason': f'Dynamic stop-loss triggered: {current_price:.2f} <= {stop_loss_price:.2f}'
|
||
|
|
}, current_price)
|
||
|
|
return
|
||
|
|
|
||
|
|
# Check take-profit condition
|
||
|
|
if current_price >= take_profit_price:
|
||
|
|
logger.info(f"🎯 {self.symbol} triggered dynamic take-profit! Current price=${current_price:.2f} >= take-profit price=${take_profit_price:.2f}")
|
||
|
|
self.execute_trade({
|
||
|
|
'action': 'SELL',
|
||
|
|
'confidence': 'HIGH',
|
||
|
|
'reason': f'Dynamic take-profit triggered: {current_price:.2f} >= {take_profit_price:.2f}'
|
||
|
|
}, current_price)
|
||
|
|
return
|
||
|
|
|
||
|
|
# Update trailing stop-loss (only moves upward) - fix logic
|
||
|
|
if stop_info and current_price > stop_info.get('highest_price', 0):
|
||
|
|
new_stop_loss = self.db.update_dynamic_stop(self.symbol, current_price, trailing_percent=trailing_percent, multiplier=2)
|
||
|
|
if new_stop_loss:
|
||
|
|
logger.info(f"[{self.symbol}] Trailing stop-loss updated: new stop-loss price=${new_stop_loss:.2f}")
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"[{self.symbol}] Error checking dynamic stop-loss: {e}")
|
||
|
|
|
||
|
|
def run(self):
|
||
|
|
"""Run strategy (using enhanced decision)"""
|
||
|
|
self.strategy_running = True
|
||
|
|
|
||
|
|
while self.strategy_running:
|
||
|
|
try:
|
||
|
|
logger.info(f"\n=== {self.symbol} Strategy Auto Execution ===")
|
||
|
|
|
||
|
|
# Market analysis
|
||
|
|
analysis = self.analyze_market()
|
||
|
|
if analysis is None:
|
||
|
|
logger.warning("Analysis failed, waiting for next execution")
|
||
|
|
time.sleep(self.config['interval'])
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Make trading decision
|
||
|
|
decision = self.make_enhanced_decision(analysis)
|
||
|
|
logger.info(f"{self.symbol} Decision: {decision['action']} (Confidence: {decision['confidence']}, Score: {decision['total_score']:.3f})")
|
||
|
|
|
||
|
|
if decision['reason']:
|
||
|
|
logger.info(f"Reason: {decision['reason']}")
|
||
|
|
|
||
|
|
# Execute trade
|
||
|
|
self.execute_trade(decision, analysis['current_price'])
|
||
|
|
|
||
|
|
# Risk management check
|
||
|
|
self.check_risk_management(analysis['current_price'])
|
||
|
|
|
||
|
|
time.sleep(self.config['interval'])
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Strategy execution error: {e}")
|
||
|
|
if "Network" in str(e) or "Connection" in str(e):
|
||
|
|
wait_time = min(300, self.config['interval'] * 2) # Network error wait longer
|
||
|
|
else:
|
||
|
|
wait_time = self.config['interval']
|
||
|
|
|
||
|
|
if self.strategy_running:
|
||
|
|
time.sleep(wait_time)
|
||
|
|
|
||
|
|
def stop_strategy(self):
|
||
|
|
"""Stop strategy running"""
|
||
|
|
with threading.Lock(): # Add thread lock
|
||
|
|
self.strategy_running = False
|
||
|
|
logger.info(f"{self.symbol} strategy stop signal sent")
|
||
|
|
|
||
|
|
def check_risk_management(self, current_price):
|
||
|
|
"""Check risk management (fixed stop-loss/take-profit)"""
|
||
|
|
if self.base_amount == 0:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Calculate profit/loss percentage
|
||
|
|
pnl_pct = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0
|
||
|
|
|
||
|
|
# Fixed stop-loss/take-profit check
|
||
|
|
if pnl_pct <= -self.risk_manager.stop_loss:
|
||
|
|
logger.warning(f"⚠️ {self.symbol} triggered fixed stop-loss ({pnl_pct:.2%})")
|
||
|
|
self.execute_trade({
|
||
|
|
'action': 'SELL',
|
||
|
|
'confidence': 'HIGH',
|
||
|
|
'reason': 'Fixed stop-loss triggered'
|
||
|
|
}, current_price)
|
||
|
|
elif pnl_pct >= self.risk_manager.take_profit:
|
||
|
|
logger.info(f"🎯 {self.symbol} triggered fixed take-profit ({pnl_pct:.2%})")
|
||
|
|
self.execute_trade({
|
||
|
|
'action': 'SELL',
|
||
|
|
'confidence': 'HIGH',
|
||
|
|
'reason': 'Fixed take-profit triggered'
|
||
|
|
}, current_price)
|
||
|
|
|
||
|
|
class MultiStrategyRunner:
|
||
|
|
"""Multi-Strategy Runner"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.running = False
|
||
|
|
self.strategies = {}
|
||
|
|
self.threads = {}
|
||
|
|
|
||
|
|
# Configure each currency strategy
|
||
|
|
self.symbol_configs = {
|
||
|
|
'ETH-USDT': {'name': 'Ethereum', 'interval': 1800, 'timeframe': '1H'},
|
||
|
|
'BTC-USDT': {'name': 'Bitcoin', 'interval': 1800, 'timeframe': '1H'},
|
||
|
|
'SOL-USDT': {'name': 'Solana', 'interval': 1800, 'timeframe': '1H'},
|
||
|
|
'XRP-USDT': {'name': 'Ripple', 'interval': 1800, 'timeframe': '1H'},
|
||
|
|
'BNB-USDT': {'name': 'Binance Coin', 'interval': 1800, 'timeframe': '1H'},
|
||
|
|
'OKB-USDT': {'name': 'OKB', 'interval': 1800, 'timeframe': '1H'},
|
||
|
|
}
|
||
|
|
|
||
|
|
# Initialize API client
|
||
|
|
self.api = OKXAPIClient()
|
||
|
|
self.deepseek = DeepSeekAnalyzer()
|
||
|
|
self.risk_manager = RiskManager(self.api)
|
||
|
|
|
||
|
|
# Initialize all strategies
|
||
|
|
for symbol, config in self.symbol_configs.items():
|
||
|
|
self.strategies[symbol] = TradingStrategy(symbol, config, self.api, self.risk_manager, self.deepseek)
|
||
|
|
|
||
|
|
# Register exit handlers
|
||
|
|
atexit.register(self.shutdown)
|
||
|
|
signal.signal(signal.SIGINT, lambda s, f: self.shutdown())
|
||
|
|
signal.signal(signal.SIGTERM, lambda s, f: self.shutdown())
|
||
|
|
|
||
|
|
def start_strategy(self, symbol):
|
||
|
|
"""Start single currency strategy"""
|
||
|
|
if symbol not in self.strategies:
|
||
|
|
logger.error(f"Unsupported {symbol} trading pair")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Check if already running
|
||
|
|
if symbol in self.threads and self.threads[symbol].is_alive():
|
||
|
|
logger.info(f"{symbol} strategy already running")
|
||
|
|
return True
|
||
|
|
|
||
|
|
def strategy_worker():
|
||
|
|
"""Strategy worker thread"""
|
||
|
|
strategy = self.strategies[symbol]
|
||
|
|
logger.info(f"Starting {symbol} strategy")
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Directly run strategy, strategy has its own loop internally
|
||
|
|
strategy.run()
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"{symbol} strategy execution error: {e}")
|
||
|
|
finally:
|
||
|
|
logger.info(f"{symbol} strategy thread ended")
|
||
|
|
# Remove from active thread list when thread ends
|
||
|
|
if symbol in self.threads:
|
||
|
|
del self.threads[symbol]
|
||
|
|
|
||
|
|
# Create and start thread
|
||
|
|
thread = threading.Thread(target=strategy_worker, name=f"Strategy-{symbol}")
|
||
|
|
thread.daemon = True
|
||
|
|
thread.start()
|
||
|
|
self.threads[symbol] = thread
|
||
|
|
|
||
|
|
logger.info(f"Started {symbol} strategy")
|
||
|
|
return True
|
||
|
|
|
||
|
|
def start_all_strategies(self):
|
||
|
|
"""Start all strategies"""
|
||
|
|
self.running = True
|
||
|
|
|
||
|
|
for symbol in self.strategies.keys():
|
||
|
|
self.start_strategy(symbol)
|
||
|
|
time.sleep(60) # Each currency starts with 60 second interval
|
||
|
|
|
||
|
|
|
||
|
|
logger.info("All strategies started")
|
||
|
|
|
||
|
|
def stop_strategy(self, symbol):
|
||
|
|
"""Stop single currency strategy"""
|
||
|
|
if symbol in self.strategies:
|
||
|
|
# First stop strategy
|
||
|
|
self.strategies[symbol].stop_strategy()
|
||
|
|
|
||
|
|
if symbol in self.threads:
|
||
|
|
thread = self.threads[symbol]
|
||
|
|
thread.join(timeout=10) # Wait 10 seconds
|
||
|
|
if thread.is_alive():
|
||
|
|
logger.warning(f"Unable to stop {symbol} strategy thread")
|
||
|
|
else:
|
||
|
|
del self.threads[symbol]
|
||
|
|
logger.info(f"Stopped {symbol} strategy")
|
||
|
|
|
||
|
|
# Stop dynamic stop-loss monitoring
|
||
|
|
if symbol in self.strategies:
|
||
|
|
self.strategies[symbol].stop_dynamic_stop_monitor()
|
||
|
|
|
||
|
|
def stop_all_strategies(self):
|
||
|
|
"""Stop all strategies"""
|
||
|
|
self.running = False
|
||
|
|
|
||
|
|
# First send stop signal to all strategies
|
||
|
|
for symbol in list(self.strategies.keys()):
|
||
|
|
self.strategies[symbol].stop_strategy()
|
||
|
|
|
||
|
|
# Then stop threads
|
||
|
|
for symbol in list(self.threads.keys()):
|
||
|
|
self.stop_strategy(symbol)
|
||
|
|
|
||
|
|
logger.info("All strategies stopped")
|
||
|
|
|
||
|
|
def get_status(self):
|
||
|
|
"""Get system status"""
|
||
|
|
status = {
|
||
|
|
'running': self.running,
|
||
|
|
'active_strategies': list(self.threads.keys()),
|
||
|
|
'strategies_detail': {}
|
||
|
|
}
|
||
|
|
|
||
|
|
for symbol, strategy in self.strategies.items():
|
||
|
|
status['strategies_detail'][symbol] = {
|
||
|
|
'base_amount': strategy.base_amount,
|
||
|
|
'entry_price': strategy.entry_price,
|
||
|
|
'config': self.symbol_configs[symbol]
|
||
|
|
}
|
||
|
|
|
||
|
|
return status
|
||
|
|
|
||
|
|
def shutdown(self):
|
||
|
|
"""System shutdown handling"""
|
||
|
|
logger.info("System shutting down, stopping all strategies and monitoring...")
|
||
|
|
self.stop_all_strategies()
|
||
|
|
|
||
|
|
# Stop all dynamic stop-loss monitoring
|
||
|
|
for symbol, strategy in self.strategies.items():
|
||
|
|
try:
|
||
|
|
strategy.stop_dynamic_stop_monitor()
|
||
|
|
logger.info(f"Stopped {symbol} dynamic stop-loss monitoring")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error stopping {symbol} dynamic stop-loss monitoring: {e}")
|
||
|
|
|
||
|
|
# Wait for all threads to end
|
||
|
|
for symbol, thread in list(self.threads.items()):
|
||
|
|
if thread.is_alive():
|
||
|
|
thread.join(timeout=5) # Wait 5 seconds
|
||
|
|
if thread.is_alive():
|
||
|
|
logger.warning(f"{symbol} strategy thread didn't exit normally")
|
||
|
|
|
||
|
|
# Ensure all resources released
|
||
|
|
time.sleep(1)
|
||
|
|
logger.info("System shutdown completed")
|
||
|
|
print("👋 System safely exited, thank you for using!")
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Main function"""
|
||
|
|
print("=" * 60)
|
||
|
|
print(" " * 20 + "Multi-Currency Intelligent Trading System")
|
||
|
|
print("=" * 60)
|
||
|
|
print("Core Features:")
|
||
|
|
print(" • DeepSeek AI Driven Decisions")
|
||
|
|
print(" • KDJ + RSI + ATR Technical Indicators")
|
||
|
|
print(" • Multi-level Risk Control")
|
||
|
|
print(" • Position Status Persistence")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
# Check environment variables
|
||
|
|
required_env_vars = ['OKX_API_KEY', 'OKX_SECRET_KEY', 'OKX_PASSWORD']
|
||
|
|
if not all(os.getenv(var) for var in required_env_vars):
|
||
|
|
print("❌ Please set OKX API environment variables")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Create strategy runner
|
||
|
|
runner = MultiStrategyRunner()
|
||
|
|
|
||
|
|
# Add debug mode switch
|
||
|
|
debug_mode = False
|
||
|
|
|
||
|
|
def toggle_debug_mode():
|
||
|
|
nonlocal debug_mode
|
||
|
|
debug_mode = not debug_mode
|
||
|
|
runner.api.log_http = debug_mode
|
||
|
|
print(f"Debug mode {'enabled' if debug_mode else 'disabled'}")
|
||
|
|
|
||
|
|
try:
|
||
|
|
while True:
|
||
|
|
print("\n" + "=" * 50)
|
||
|
|
print(" " * 15 + "Trading System Control Panel")
|
||
|
|
print("=" * 50)
|
||
|
|
print("1. Start All Strategies")
|
||
|
|
print("2. Start Specific Strategy")
|
||
|
|
print("3. Stop All Strategies")
|
||
|
|
print("4. Stop Specific Strategy")
|
||
|
|
print("5. View System Status")
|
||
|
|
print("6. Manual Position Sync")
|
||
|
|
print("7. Toggle Debug Mode")
|
||
|
|
print("8. Exit System")
|
||
|
|
print("=" * 50)
|
||
|
|
|
||
|
|
choice = input("Please enter choice (1-8): ").strip()
|
||
|
|
|
||
|
|
if choice == '1':
|
||
|
|
print("Starting all trading strategies...")
|
||
|
|
runner.start_all_strategies()
|
||
|
|
print("✅ All strategies started")
|
||
|
|
|
||
|
|
elif choice == '2':
|
||
|
|
print("\nSelect currency to start:")
|
||
|
|
symbols = list(runner.symbol_configs.keys())
|
||
|
|
for i, symbol in enumerate(symbols, 1):
|
||
|
|
config = runner.symbol_configs[symbol]
|
||
|
|
print(f"{i}. {symbol} ({config['name']})")
|
||
|
|
print(f"{len(symbols)+1}. Back")
|
||
|
|
|
||
|
|
symbol_choice = input("Please enter choice: ").strip()
|
||
|
|
if symbol_choice.isdigit():
|
||
|
|
index = int(symbol_choice) - 1
|
||
|
|
if 0 <= index < len(symbols):
|
||
|
|
symbol = symbols[index]
|
||
|
|
runner.start_strategy(symbol)
|
||
|
|
print(f"✅ Started {symbol} strategy")
|
||
|
|
elif index == len(symbols):
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
print("❌ Invalid choice")
|
||
|
|
else:
|
||
|
|
print("❌ Invalid input")
|
||
|
|
|
||
|
|
elif choice == '3':
|
||
|
|
print("Stopping all trading strategies...")
|
||
|
|
runner.stop_all_strategies()
|
||
|
|
print("✅ All strategies stopped")
|
||
|
|
|
||
|
|
elif choice == '4':
|
||
|
|
print("\nSelect currency to stop:")
|
||
|
|
symbols = list(runner.symbol_configs.keys())
|
||
|
|
for i, symbol in enumerate(symbols, 1):
|
||
|
|
config = runner.symbol_configs[symbol]
|
||
|
|
print(f"{i}. {symbol} ({config['name']})")
|
||
|
|
print(f"{len(symbols)+1}. Back")
|
||
|
|
|
||
|
|
symbol_choice = input("Please enter choice: ").strip()
|
||
|
|
if symbol_choice.isdigit():
|
||
|
|
index = int(symbol_choice) - 1
|
||
|
|
if 0 <= index < len(symbols):
|
||
|
|
symbol = symbols[index]
|
||
|
|
runner.stop_strategy(symbol)
|
||
|
|
print(f"✅ Stopped {symbol} strategy")
|
||
|
|
elif index == len(symbols):
|
||
|
|
continue
|
||
|
|
else:
|
||
|
|
print("❌ Invalid choice")
|
||
|
|
else:
|
||
|
|
print("❌ Invalid input")
|
||
|
|
|
||
|
|
elif choice == '5':
|
||
|
|
status = runner.get_status()
|
||
|
|
print(f"\nSystem Status: {'Running' if status['running'] else 'Stopped'}")
|
||
|
|
print(f"Active Strategies: {len(status['active_strategies'])}")
|
||
|
|
for symbol in status['active_strategies']:
|
||
|
|
print(f" • {symbol}")
|
||
|
|
|
||
|
|
print("\nPosition Status:")
|
||
|
|
for symbol, detail in status['strategies_detail'].items():
|
||
|
|
base_amount = detail['base_amount']
|
||
|
|
entry_price = detail['entry_price']
|
||
|
|
if base_amount > 0:
|
||
|
|
print(f" {symbol}: Position {base_amount:.10f} @ ${entry_price:.2f}")
|
||
|
|
|
||
|
|
elif choice == '6':
|
||
|
|
print("Manual sync all currency positions...")
|
||
|
|
for symbol, strategy in runner.strategies.items():
|
||
|
|
strategy.sync_with_exchange()
|
||
|
|
print("✅ Sync completed")
|
||
|
|
|
||
|
|
elif choice == '7':
|
||
|
|
toggle_debug_mode()
|
||
|
|
|
||
|
|
elif choice == '8':
|
||
|
|
print("Exiting system, please wait for all strategies to stop...")
|
||
|
|
break
|
||
|
|
|
||
|
|
else:
|
||
|
|
print("❌ Invalid choice, please re-enter")
|
||
|
|
|
||
|
|
except KeyboardInterrupt:
|
||
|
|
print("\nReceived interrupt signal, exiting...")
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|