820 lines
38 KiB
Python
820 lines
38 KiB
Python
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}")
|
|
|
|
# Save trade record
|
|
self.db.save_trade_record(self.symbol, 'buy', fill_amount, fill_price, order_id)
|
|
|
|
# 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)
|