Cycles/cycles/IncStrategies/inc_trader.py

344 lines
13 KiB
Python
Raw Normal View History

"""
Incremental Trader for backtesting incremental strategies.
This module provides the IncTrader class that manages a single incremental strategy
during backtesting, handling position state, trade execution, and performance tracking.
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional, List, Any
import logging
from dataclasses import dataclass
from .base import IncStrategyBase, IncStrategySignal
from ..market_fees import MarketFees
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 IncTrader:
"""
Incremental trader that manages a single strategy during backtesting.
This class handles:
- Strategy initialization and data feeding
- Position management (USD/coin balance)
- Trade execution based on strategy signals
- Performance tracking and metrics collection
- Fee calculation and trade logging
The trader processes data points sequentially, feeding them to the strategy
and executing trades based on the generated signals.
Example:
strategy = IncRandomStrategy(params={"timeframe": "15min"})
trader = IncTrader(
strategy=strategy,
initial_usd=10000,
params={"stop_loss_pct": 0.02}
)
# Process data sequentially
for timestamp, ohlcv_data in data_stream:
trader.process_data_point(timestamp, ohlcv_data)
# Get results
results = trader.get_results()
"""
def __init__(self, strategy: IncStrategyBase, initial_usd: float = 10000,
params: Optional[Dict] = None):
"""
Initialize the incremental trader.
Args:
strategy: Incremental strategy instance
initial_usd: Initial USD balance
params: Trader parameters (stop_loss_pct, take_profit_pct, etc.)
"""
self.strategy = strategy
self.initial_usd = initial_usd
self.params = params or {}
# 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 = []
self.current_timestamp = None
self.current_price = None
# Strategy state
self.data_points_processed = 0
self.warmup_complete = False
# Parameters
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.0)
self.take_profit_pct = self.params.get("take_profit_pct", 0.0)
logger.info(f"IncTrader initialized: strategy={strategy.name}, "
f"initial_usd=${initial_usd}, stop_loss={self.stop_loss_pct*100:.1f}%")
def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None:
"""
Process a single data point through the strategy and handle trading logic.
Args:
timestamp: Data point timestamp
ohlcv_data: OHLCV data dictionary with keys: open, high, low, close, volume
"""
self.current_timestamp = timestamp
self.current_price = ohlcv_data['close']
self.data_points_processed += 1
try:
# Feed data to strategy (handles timeframe aggregation internally)
result = self.strategy.update_minute_data(timestamp, ohlcv_data)
# Check if strategy is warmed up
if not self.warmup_complete and self.strategy.is_warmed_up:
self.warmup_complete = True
logger.info(f"Strategy {self.strategy.name} warmed up after "
f"{self.data_points_processed} data points")
# Only process signals if strategy is warmed up and we have a complete timeframe bar
if self.warmup_complete and result is not None:
self._process_trading_logic()
# Update performance tracking
self._update_performance_metrics()
except Exception as e:
logger.error(f"Error processing data point at {timestamp}: {e}")
raise
def _process_trading_logic(self) -> None:
"""Process trading logic based on current position and strategy signals."""
if self.position == 0:
# No position - check for entry signals
self._check_entry_signals()
else:
# In position - check for exit signals
self._check_exit_signals()
def _check_entry_signals(self) -> None:
"""Check for entry signals when not in position."""
try:
entry_signal = self.strategy.get_entry_signal()
if entry_signal.signal_type == "ENTRY" and entry_signal.confidence > 0:
self._execute_entry(entry_signal)
except Exception as e:
logger.error(f"Error checking entry signals: {e}")
def _check_exit_signals(self) -> None:
"""Check for exit signals when in position."""
try:
# Check strategy exit signals
exit_signal = self.strategy.get_exit_signal()
if exit_signal.signal_type == "EXIT" and exit_signal.confidence > 0:
exit_reason = exit_signal.metadata.get("type", "STRATEGY_EXIT")
self._execute_exit(exit_reason, exit_signal.price)
return
# Check stop loss
if self.stop_loss_pct > 0:
stop_loss_price = self.entry_price * (1 - self.stop_loss_pct)
if self.current_price <= stop_loss_price:
self._execute_exit("STOP_LOSS", self.current_price)
return
# Check take profit
if self.take_profit_pct > 0:
take_profit_price = self.entry_price * (1 + self.take_profit_pct)
if self.current_price >= take_profit_price:
self._execute_exit("TAKE_PROFIT", self.current_price)
return
except Exception as e:
logger.error(f"Error checking exit signals: {e}")
def _execute_entry(self, signal: IncStrategySignal) -> None:
"""Execute entry trade."""
entry_price = signal.price if signal.price else self.current_price
entry_fee = MarketFees.calculate_okx_taker_maker_fee(self.usd, is_maker=False)
usd_after_fee = self.usd - entry_fee
self.coin = usd_after_fee / entry_price
self.entry_price = entry_price
self.entry_time = self.current_timestamp
self.usd = 0.0
self.position = 1
logger.info(f"ENTRY: {self.strategy.name} at ${entry_price:.2f}, "
f"confidence={signal.confidence:.2f}, fee=${entry_fee:.2f}")
def _execute_exit(self, exit_reason: str, exit_price: Optional[float] = None) -> None:
"""Execute exit trade."""
exit_price = exit_price if exit_price else self.current_price
usd_gross = self.coin * exit_price
exit_fee = MarketFees.calculate_okx_taker_maker_fee(usd_gross, is_maker=False)
self.usd = usd_gross - exit_fee
# Calculate profit
profit_pct = (exit_price - self.entry_price) / self.entry_price
# Record trade
trade_record = TradeRecord(
entry_time=self.entry_time,
exit_time=self.current_timestamp,
entry_price=self.entry_price,
exit_price=exit_price,
entry_fee=MarketFees.calculate_okx_taker_maker_fee(
self.coin * self.entry_price, is_maker=False
),
exit_fee=exit_fee,
profit_pct=profit_pct,
exit_reason=exit_reason,
strategy_name=self.strategy.name
)
self.trade_records.append(trade_record)
# Reset position
self.coin = 0.0
self.position = 0
self.entry_price = 0.0
self.entry_time = None
logger.info(f"EXIT: {self.strategy.name} at ${exit_price:.2f}, "
f"reason={exit_reason}, profit={profit_pct*100:.2f}%, fee=${exit_fee:.2f}")
def _update_performance_metrics(self) -> None:
"""Update performance tracking metrics."""
# Calculate current balance
if self.position == 0:
current_balance = self.usd
else:
current_balance = self.coin * self.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 finalize(self) -> None:
"""Finalize trading session (close any open positions)."""
if self.position == 1:
self._execute_exit("EOD", self.current_price)
logger.info(f"Closed final position for {self.strategy.name} at EOD")
def get_results(self) -> Dict[str, Any]:
"""
Get comprehensive trading results.
Returns:
Dict containing performance metrics, trade records, and 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
# 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
})
results = {
"strategy_name": self.strategy.name,
"strategy_params": self.strategy.params,
"trader_params": self.params,
"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,
"data_points_processed": self.data_points_processed,
"warmup_complete": self.warmup_complete,
"trades": trades
}
# Add first and last trade info if available
if n_trades > 0:
results["first_trade"] = {
"entry_time": self.trade_records[0].entry_time,
"entry": self.trade_records[0].entry_price
}
results["last_trade"] = {
"exit_time": self.trade_records[-1].exit_time,
"exit": self.trade_records[-1].exit_price
}
return results
def get_current_state(self) -> Dict[str, Any]:
"""Get current trader state for debugging."""
return {
"strategy": self.strategy.name,
"position": self.position,
"usd": self.usd,
"coin": self.coin,
"current_price": self.current_price,
"entry_price": self.entry_price,
"data_points_processed": self.data_points_processed,
"warmup_complete": self.warmup_complete,
"n_trades": len(self.trade_records),
"strategy_state": self.strategy.get_current_state_summary()
}
def __repr__(self) -> str:
"""String representation of the trader."""
return (f"IncTrader(strategy={self.strategy.name}, "
f"position={self.position}, usd=${self.usd:.2f}, "
f"trades={len(self.trade_records)})")