sliding fit fix
This commit is contained in:
+102
-95
@@ -1,28 +1,30 @@
|
||||
from typing import Any, Dict, List
|
||||
import pandas as pd
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime, date
|
||||
import sqlite3
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import pandas as pd
|
||||
from pt_trading.trading_pair import TradingPair
|
||||
|
||||
|
||||
# Recommended replacement adapters and converters for Python 3.12+
|
||||
# From: https://docs.python.org/3/library/sqlite3.html#sqlite3-adapter-converter-recipes
|
||||
def adapt_date_iso(val):
|
||||
def adapt_date_iso(val: date) -> str:
|
||||
"""Adapt datetime.date to ISO 8601 date."""
|
||||
return val.isoformat()
|
||||
|
||||
|
||||
def adapt_datetime_iso(val):
|
||||
def adapt_datetime_iso(val: datetime) -> str:
|
||||
"""Adapt datetime.datetime to timezone-naive ISO 8601 date."""
|
||||
return val.isoformat()
|
||||
|
||||
|
||||
def convert_date(val):
|
||||
def convert_date(val: bytes) -> date:
|
||||
"""Convert ISO 8601 date to datetime.date object."""
|
||||
return datetime.fromisoformat(val.decode()).date()
|
||||
|
||||
|
||||
def convert_datetime(val):
|
||||
def convert_datetime(val: bytes) -> datetime:
|
||||
"""Convert ISO 8601 datetime to datetime.datetime object."""
|
||||
return datetime.fromisoformat(val.decode())
|
||||
|
||||
@@ -172,7 +174,7 @@ def store_results_in_database(
|
||||
if db_path.upper() == "NONE":
|
||||
return
|
||||
|
||||
def convert_timestamp(timestamp):
|
||||
def convert_timestamp(timestamp: Any) -> Optional[datetime]:
|
||||
"""Convert pandas Timestamp to Python datetime object for SQLite compatibility."""
|
||||
if timestamp is None:
|
||||
return None
|
||||
@@ -423,14 +425,14 @@ class BacktestResult:
|
||||
|
||||
def add_trade(
|
||||
self,
|
||||
pair_nm,
|
||||
symbol,
|
||||
action,
|
||||
price,
|
||||
disequilibrium=None,
|
||||
scaled_disequilibrium=None,
|
||||
timestamp=None,
|
||||
):
|
||||
pair_nm: str,
|
||||
symbol: str,
|
||||
action: str,
|
||||
price: Any,
|
||||
disequilibrium: Optional[float] = None,
|
||||
scaled_disequilibrium: Optional[float] = None,
|
||||
timestamp: Optional[datetime] = None,
|
||||
) -> None:
|
||||
"""Add a trade to the results tracking."""
|
||||
pair_nm = str(pair_nm)
|
||||
|
||||
@@ -442,11 +444,11 @@ class BacktestResult:
|
||||
(action, price, disequilibrium, scaled_disequilibrium, timestamp)
|
||||
)
|
||||
|
||||
def add_outstanding_position(self, position: Dict[str, Any]):
|
||||
def add_outstanding_position(self, position: Dict[str, Any]) -> None:
|
||||
"""Add an outstanding position to tracking."""
|
||||
self.outstanding_positions.append(position)
|
||||
|
||||
def add_realized_pnl(self, realized_pnl: float):
|
||||
def add_realized_pnl(self, realized_pnl: float) -> None:
|
||||
"""Add realized PnL to the total."""
|
||||
self.total_realized_pnl += realized_pnl
|
||||
|
||||
@@ -462,14 +464,12 @@ class BacktestResult:
|
||||
"""Get all trades."""
|
||||
return self.trades
|
||||
|
||||
def clear_trades(self):
|
||||
def clear_trades(self) -> None:
|
||||
"""Clear all trades (used when processing new files)."""
|
||||
self.trades.clear()
|
||||
|
||||
def collect_single_day_results(self, result):
|
||||
def collect_single_day_results(self, result: pd.DataFrame) -> None:
|
||||
"""Collect and process single day trading results."""
|
||||
if result is None:
|
||||
return
|
||||
|
||||
print("\n -------------- Suggested Trades ")
|
||||
print(result)
|
||||
@@ -482,16 +482,16 @@ class BacktestResult:
|
||||
scaled_disequilibrium = getattr(row, "scaled_disequilibrium", None)
|
||||
timestamp = getattr(row, "time", None)
|
||||
self.add_trade(
|
||||
pair_nm=row.pair,
|
||||
action=action,
|
||||
symbol=symbol,
|
||||
price=price,
|
||||
pair_nm=str(row.pair),
|
||||
action=str(action),
|
||||
symbol=str(symbol),
|
||||
price=float(str(price)),
|
||||
disequilibrium=disequilibrium,
|
||||
scaled_disequilibrium=scaled_disequilibrium,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
def print_single_day_results(self):
|
||||
def print_single_day_results(self) -> None:
|
||||
"""Print single day results summary."""
|
||||
for pair, symbols in self.trades.items():
|
||||
print(f"\n--- {pair} ---")
|
||||
@@ -501,7 +501,7 @@ class BacktestResult:
|
||||
side, price = trade_data[:2]
|
||||
print(f"{symbol} {side} at ${price}")
|
||||
|
||||
def print_results_summary(self, all_results):
|
||||
def print_results_summary(self, all_results: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""Print summary of all processed files."""
|
||||
print("\n====== Summary of All Processed Files ======")
|
||||
for filename, data in all_results.items():
|
||||
@@ -512,7 +512,7 @@ class BacktestResult:
|
||||
)
|
||||
print(f"{filename}: {trade_count} trades")
|
||||
|
||||
def calculate_returns(self, all_results: Dict):
|
||||
def calculate_returns(self, all_results: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""Calculate and print returns by day and pair."""
|
||||
print("\n====== Returns By Day and Pair ======")
|
||||
|
||||
@@ -527,80 +527,87 @@ class BacktestResult:
|
||||
|
||||
# Calculate individual symbol returns in the pair
|
||||
for symbol, trades in symbols.items():
|
||||
if len(trades) >= 2: # Need at least entry and exit
|
||||
# Get entry and exit trades - handle both old and new tuple formats
|
||||
if len(trades[0]) == 2: # Old format: (action, price)
|
||||
entry_action, entry_price = trades[0]
|
||||
exit_action, exit_price = trades[1]
|
||||
open_disequilibrium = None
|
||||
open_scaled_disequilibrium = None
|
||||
close_disequilibrium = None
|
||||
close_scaled_disequilibrium = None
|
||||
if len(trades) == 0:
|
||||
continue
|
||||
|
||||
symbol_return = 0
|
||||
symbol_trades = []
|
||||
|
||||
# Process all trades sequentially for this symbol
|
||||
for i, trade in enumerate(trades):
|
||||
# Handle both old and new tuple formats
|
||||
if len(trade) == 2: # Old format: (action, price)
|
||||
action, price = trade
|
||||
disequilibrium = None
|
||||
scaled_disequilibrium = None
|
||||
timestamp = None
|
||||
else: # New format: (action, price, disequilibrium, scaled_disequilibrium, timestamp)
|
||||
entry_action, entry_price = trades[0][:2]
|
||||
exit_action, exit_price = trades[1][:2]
|
||||
open_disequilibrium = (
|
||||
trades[0][2] if len(trades[0]) > 2 else None
|
||||
)
|
||||
open_scaled_disequilibrium = (
|
||||
trades[0][3] if len(trades[0]) > 3 else None
|
||||
)
|
||||
close_disequilibrium = (
|
||||
trades[1][2] if len(trades[1]) > 2 else None
|
||||
)
|
||||
close_scaled_disequilibrium = (
|
||||
trades[1][3] if len(trades[1]) > 3 else None
|
||||
)
|
||||
|
||||
# Calculate return based on action
|
||||
symbol_return = 0
|
||||
if entry_action == "BUY" and exit_action == "SELL":
|
||||
action, price = trade[:2]
|
||||
disequilibrium = trade[2] if len(trade) > 2 else None
|
||||
scaled_disequilibrium = trade[3] if len(trade) > 3 else None
|
||||
timestamp = trade[4] if len(trade) > 4 else None
|
||||
|
||||
symbol_trades.append((action, price, disequilibrium, scaled_disequilibrium, timestamp))
|
||||
|
||||
# Calculate returns for all trade combinations
|
||||
for i in range(len(symbol_trades) - 1):
|
||||
trade1 = symbol_trades[i]
|
||||
trade2 = symbol_trades[i + 1]
|
||||
|
||||
action1, price1, diseq1, scaled_diseq1, ts1 = trade1
|
||||
action2, price2, diseq2, scaled_diseq2, ts2 = trade2
|
||||
|
||||
# Calculate return based on action combination
|
||||
trade_return = 0
|
||||
if action1 == "BUY" and action2 == "SELL":
|
||||
# Long position
|
||||
symbol_return = (
|
||||
(exit_price - entry_price) / entry_price * 100
|
||||
)
|
||||
elif entry_action == "SELL" and exit_action == "BUY":
|
||||
trade_return = (price2 - price1) / price1 * 100
|
||||
elif action1 == "SELL" and action2 == "BUY":
|
||||
# Short position
|
||||
symbol_return = (
|
||||
(entry_price - exit_price) / entry_price * 100
|
||||
)
|
||||
|
||||
trade_return = (price1 - price2) / price1 * 100
|
||||
|
||||
symbol_return += trade_return
|
||||
|
||||
# Store trade details for reporting
|
||||
pair_trades.append(
|
||||
(
|
||||
symbol,
|
||||
entry_action,
|
||||
entry_price,
|
||||
exit_action,
|
||||
exit_price,
|
||||
symbol_return,
|
||||
open_scaled_disequilibrium,
|
||||
close_scaled_disequilibrium,
|
||||
action1,
|
||||
price1,
|
||||
action2,
|
||||
price2,
|
||||
trade_return,
|
||||
scaled_diseq1,
|
||||
scaled_diseq2,
|
||||
i + 1, # Trade sequence number
|
||||
)
|
||||
)
|
||||
pair_return += symbol_return
|
||||
|
||||
pair_return += symbol_return
|
||||
|
||||
# Print pair returns with disequilibrium information
|
||||
if pair_trades:
|
||||
print(f" {pair}:")
|
||||
for (
|
||||
symbol,
|
||||
entry_action,
|
||||
entry_price,
|
||||
exit_action,
|
||||
exit_price,
|
||||
symbol_return,
|
||||
open_scaled_disequilibrium,
|
||||
close_scaled_disequilibrium,
|
||||
action1,
|
||||
price1,
|
||||
action2,
|
||||
price2,
|
||||
trade_return,
|
||||
scaled_diseq1,
|
||||
scaled_diseq2,
|
||||
trade_num,
|
||||
) in pair_trades:
|
||||
disequil_info = ""
|
||||
if (
|
||||
open_scaled_disequilibrium is not None
|
||||
and close_scaled_disequilibrium is not None
|
||||
scaled_diseq1 is not None
|
||||
and scaled_diseq2 is not None
|
||||
):
|
||||
disequil_info = f" | Open Dis-eq: {open_scaled_disequilibrium:.2f}, Close Dis-eq: {close_scaled_disequilibrium:.2f}"
|
||||
disequil_info = f" | Open Dis-eq: {scaled_diseq1:.2f}, Close Dis-eq: {scaled_diseq2:.2f}"
|
||||
|
||||
print(
|
||||
f" {symbol}: {entry_action} @ ${entry_price:.2f}, {exit_action} @ ${exit_price:.2f}, Return: {symbol_return:.2f}%{disequil_info}"
|
||||
f" {symbol} (Trade #{trade_num}): {action1} @ ${price1:.2f}, {action2} @ ${price2:.2f}, Return: {trade_return:.2f}%{disequil_info}"
|
||||
)
|
||||
print(f" Pair Total Return: {pair_return:.2f}%")
|
||||
day_return += pair_return
|
||||
@@ -610,7 +617,7 @@ class BacktestResult:
|
||||
print(f" Day Total Return: {day_return:.2f}%")
|
||||
self.add_realized_pnl(day_return)
|
||||
|
||||
def print_outstanding_positions(self):
|
||||
def print_outstanding_positions(self) -> None:
|
||||
"""Print all outstanding positions with share quantities and current values."""
|
||||
if not self.get_outstanding_positions():
|
||||
print("\n====== NO OUTSTANDING POSITIONS ======")
|
||||
@@ -684,22 +691,22 @@ class BacktestResult:
|
||||
|
||||
print(f"{'TOTAL OUTSTANDING VALUE':<80} ${total_value:<12.2f}")
|
||||
|
||||
def print_grand_totals(self):
|
||||
def print_grand_totals(self) -> None:
|
||||
"""Print grand totals across all pairs."""
|
||||
print(f"\n====== GRAND TOTALS ACROSS ALL PAIRS ======")
|
||||
print(f"Total Realized PnL: {self.get_total_realized_pnl():.2f}%")
|
||||
|
||||
def handle_outstanding_position(
|
||||
self,
|
||||
pair,
|
||||
pair_result_df,
|
||||
last_row_index,
|
||||
open_side_a,
|
||||
open_side_b,
|
||||
open_px_a,
|
||||
open_px_b,
|
||||
open_tstamp,
|
||||
):
|
||||
pair: TradingPair,
|
||||
pair_result_df: pd.DataFrame,
|
||||
last_row_index: int,
|
||||
open_side_a: str,
|
||||
open_side_b: str,
|
||||
open_px_a: float,
|
||||
open_px_b: float,
|
||||
open_tstamp: datetime,
|
||||
) -> Tuple[float, float, float]:
|
||||
"""
|
||||
Handle calculation and tracking of outstanding positions when no close signal is found.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user