- Introduced a comprehensive framework for incremental trading strategies, including modules for strategy execution, backtesting, and data processing. - Added key components such as `IncTrader`, `IncBacktester`, and various trading strategies (e.g., `MetaTrendStrategy`, `BBRSStrategy`, `RandomStrategy`) to facilitate real-time trading and backtesting. - Implemented a robust backtesting framework with configuration management, parallel execution, and result analysis capabilities. - Developed an incremental indicators framework to support real-time data processing with constant memory usage. - Enhanced documentation to provide clear usage examples and architecture overview, ensuring maintainability and ease of understanding for future development. - Ensured compatibility with existing strategies and maintained a focus on performance and scalability throughout the implementation.
301 lines
9.9 KiB
Python
301 lines
9.9 KiB
Python
"""
|
|
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)})") |