refactor for clarity

This commit is contained in:
Simon Moisy 2025-11-05 19:39:02 +08:00
parent 7e7a737134
commit a991dcc1f1
9 changed files with 2867 additions and 2828 deletions

21
apiratelimiter.py Normal file
View File

@ -0,0 +1,21 @@
import threading
import time
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

516
deepseekanalyzer.py Normal file
View File

@ -0,0 +1,516 @@
import os
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
}

View File

@ -1,6 +0,0 @@
def main():
print("Hello from okxtrading2-0!")
if __name__ == "__main__":
main()

167
multistrategyrunner.py Normal file
View File

@ -0,0 +1,167 @@
import atexit
import logging
import signal
import time
import threading
from okxapiclient import OKXAPIClient
from deepseekanalyzer import DeepSeekAnalyzer
from riskmanager import RiskManager
from tradingstrategy import TradingStrategy
logger = logging.getLogger(__name__)
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!")

398
okxapiclient.py Normal file
View File

@ -0,0 +1,398 @@
import os
import okx.Account as Account
import okx.MarketData as MarketData
import okx.Trade as Trade
from apiratelimiter import APIRateLimiter
import pandas as pd
import numpy as np
import logging
logger = logging.getLogger(__name__)
tdmode = "cross" # cross for demo account
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

File diff suppressed because it is too large Load Diff

338
riskmanager.py Normal file
View File

@ -0,0 +1,338 @@
import logging
import os
import time
from database_manager import DatabaseManager
logger = logging.getLogger(__name__)
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}")

609
technicalanalyzer.py Normal file
View File

@ -0,0 +1,609 @@
import logging
import numpy as np
logger = logging.getLogger(__name__)
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

816
tradingstrategy.py Normal file
View File

@ -0,0 +1,816 @@
import logging
import time
import threading
import pandas as pd
from technicalanalyzer import TechnicalAnalyzer
from database_manager import DatabaseManager
logger = logging.getLogger(__name__)
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)