""" Position Management for Incremental Trading This module handles position state, balance tracking, and trade calculations for the incremental trading system. """ import pandas as pd import numpy as np from typing import Dict, Optional, List, Any from dataclasses import dataclass import logging logger = logging.getLogger(__name__) @dataclass class TradeRecord: """Record of a completed trade.""" entry_time: pd.Timestamp exit_time: pd.Timestamp entry_price: float exit_price: float entry_fee: float exit_fee: float profit_pct: float exit_reason: str strategy_name: str class MarketFees: """Market fee calculations for different exchanges.""" @staticmethod def calculate_okx_taker_maker_fee(amount: float, is_maker: bool = True) -> float: """Calculate OKX trading fees.""" fee_rate = 0.0008 if is_maker else 0.0010 return amount * fee_rate @staticmethod def calculate_binance_fee(amount: float, is_maker: bool = True) -> float: """Calculate Binance trading fees.""" fee_rate = 0.001 if is_maker else 0.001 return amount * fee_rate class PositionManager: """ Manages trading position state and calculations. This class handles: - USD/coin balance tracking - Position state management - Trade execution calculations - Fee calculations - Performance metrics """ def __init__(self, initial_usd: float = 10000, fee_calculator=None): """ Initialize position manager. Args: initial_usd: Initial USD balance fee_calculator: Fee calculation function (defaults to OKX) """ self.initial_usd = initial_usd self.fee_calculator = fee_calculator or MarketFees.calculate_okx_taker_maker_fee # Position state self.usd = initial_usd self.coin = 0.0 self.position = 0 # 0 = no position, 1 = long position self.entry_price = 0.0 self.entry_time = None # Performance tracking self.max_balance = initial_usd self.drawdowns = [] self.trade_records = [] logger.debug(f"PositionManager initialized with ${initial_usd}") def is_in_position(self) -> bool: """Check if currently in a position.""" return self.position == 1 def get_current_balance(self, current_price: float) -> float: """Get current total balance value.""" if self.position == 0: return self.usd else: return self.coin * current_price def execute_entry(self, entry_price: float, timestamp: pd.Timestamp, strategy_name: str) -> Dict[str, Any]: """ Execute entry trade. Args: entry_price: Entry price timestamp: Entry timestamp strategy_name: Name of the strategy Returns: Dict with entry details """ if self.position == 1: raise ValueError("Cannot enter position: already in position") # Calculate fees entry_fee = self.fee_calculator(self.usd, is_maker=False) usd_after_fee = self.usd - entry_fee # Execute entry self.coin = usd_after_fee / entry_price self.entry_price = entry_price self.entry_time = timestamp self.usd = 0.0 self.position = 1 entry_details = { 'entry_price': entry_price, 'entry_time': timestamp, 'entry_fee': entry_fee, 'coin_amount': self.coin, 'strategy_name': strategy_name } logger.debug(f"ENTRY executed: ${entry_price:.2f}, fee=${entry_fee:.2f}") return entry_details def execute_exit(self, exit_price: float, timestamp: pd.Timestamp, exit_reason: str, strategy_name: str) -> Dict[str, Any]: """ Execute exit trade. Args: exit_price: Exit price timestamp: Exit timestamp exit_reason: Reason for exit strategy_name: Name of the strategy Returns: Dict with exit details and trade record """ if self.position == 0: raise ValueError("Cannot exit position: not in position") # Calculate exit usd_gross = self.coin * exit_price exit_fee = self.fee_calculator(usd_gross, is_maker=False) self.usd = usd_gross - exit_fee # Calculate profit profit_pct = (exit_price - self.entry_price) / self.entry_price # Calculate entry fee (for record keeping) entry_fee = self.fee_calculator(self.coin * self.entry_price, is_maker=False) # Create trade record trade_record = TradeRecord( entry_time=self.entry_time, exit_time=timestamp, entry_price=self.entry_price, exit_price=exit_price, entry_fee=entry_fee, exit_fee=exit_fee, profit_pct=profit_pct, exit_reason=exit_reason, strategy_name=strategy_name ) self.trade_records.append(trade_record) # Reset position coin_amount = self.coin self.coin = 0.0 self.position = 0 entry_price = self.entry_price entry_time = self.entry_time self.entry_price = 0.0 self.entry_time = None exit_details = { 'exit_price': exit_price, 'exit_time': timestamp, 'exit_fee': exit_fee, 'profit_pct': profit_pct, 'exit_reason': exit_reason, 'trade_record': trade_record, 'final_usd': self.usd } logger.debug(f"EXIT executed: ${exit_price:.2f}, reason={exit_reason}, " f"profit={profit_pct*100:.2f}%, fee=${exit_fee:.2f}") return exit_details def update_performance_metrics(self, current_price: float) -> None: """Update performance tracking metrics.""" current_balance = self.get_current_balance(current_price) # Update max balance and drawdown if current_balance > self.max_balance: self.max_balance = current_balance drawdown = (self.max_balance - current_balance) / self.max_balance self.drawdowns.append(drawdown) def check_stop_loss(self, current_price: float, stop_loss_pct: float) -> bool: """Check if stop loss should be triggered.""" if self.position == 0 or stop_loss_pct <= 0: return False stop_loss_price = self.entry_price * (1 - stop_loss_pct) return current_price <= stop_loss_price def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool: """Check if take profit should be triggered.""" if self.position == 0 or take_profit_pct <= 0: return False take_profit_price = self.entry_price * (1 + take_profit_pct) return current_price >= take_profit_price def get_performance_summary(self) -> Dict[str, Any]: """Get performance summary statistics.""" final_balance = self.usd n_trades = len(self.trade_records) # Calculate statistics if n_trades > 0: profits = [trade.profit_pct for trade in self.trade_records] wins = [p for p in profits if p > 0] win_rate = len(wins) / n_trades avg_trade = np.mean(profits) total_fees = sum(trade.entry_fee + trade.exit_fee for trade in self.trade_records) else: win_rate = 0.0 avg_trade = 0.0 total_fees = 0.0 max_drawdown = max(self.drawdowns) if self.drawdowns else 0.0 profit_ratio = (final_balance - self.initial_usd) / self.initial_usd return { "initial_usd": self.initial_usd, "final_usd": final_balance, "profit_ratio": profit_ratio, "n_trades": n_trades, "win_rate": win_rate, "max_drawdown": max_drawdown, "avg_trade": avg_trade, "total_fees_usd": total_fees } def get_trades_as_dicts(self) -> List[Dict[str, Any]]: """Convert trade records to dictionaries.""" trades = [] for trade in self.trade_records: trades.append({ 'entry_time': trade.entry_time, 'exit_time': trade.exit_time, 'entry': trade.entry_price, 'exit': trade.exit_price, 'profit_pct': trade.profit_pct, 'type': trade.exit_reason, 'fee_usd': trade.entry_fee + trade.exit_fee, 'strategy': trade.strategy_name }) return trades def get_current_state(self) -> Dict[str, Any]: """Get current position state.""" return { "position": self.position, "usd": self.usd, "coin": self.coin, "entry_price": self.entry_price, "entry_time": self.entry_time, "n_trades": len(self.trade_records), "max_balance": self.max_balance } def reset(self) -> None: """Reset position manager to initial state.""" self.usd = self.initial_usd self.coin = 0.0 self.position = 0 self.entry_price = 0.0 self.entry_time = None self.max_balance = self.initial_usd self.drawdowns.clear() self.trade_records.clear() logger.debug("PositionManager reset to initial state") def __repr__(self) -> str: """String representation of position manager.""" return (f"PositionManager(position={self.position}, " f"usd=${self.usd:.2f}, coin={self.coin:.6f}, " f"trades={len(self.trade_records)})")