793 lines
30 KiB
Python
793 lines
30 KiB
Python
import os
|
|
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: date) -> str:
|
|
"""Adapt datetime.date to ISO 8601 date."""
|
|
return val.isoformat()
|
|
|
|
|
|
def adapt_datetime_iso(val: datetime) -> str:
|
|
"""Adapt datetime.datetime to timezone-naive ISO 8601 date."""
|
|
return val.isoformat()
|
|
|
|
|
|
def convert_date(val: bytes) -> date:
|
|
"""Convert ISO 8601 date to datetime.date object."""
|
|
return datetime.fromisoformat(val.decode()).date()
|
|
|
|
|
|
def convert_datetime(val: bytes) -> datetime:
|
|
"""Convert ISO 8601 datetime to datetime.datetime object."""
|
|
return datetime.fromisoformat(val.decode())
|
|
|
|
|
|
# Register the adapters and converters
|
|
sqlite3.register_adapter(date, adapt_date_iso)
|
|
sqlite3.register_adapter(datetime, adapt_datetime_iso)
|
|
sqlite3.register_converter("date", convert_date)
|
|
sqlite3.register_converter("datetime", convert_datetime)
|
|
|
|
|
|
def create_result_database(db_path: str) -> None:
|
|
"""
|
|
Create the SQLite database and required tables if they don't exist.
|
|
"""
|
|
try:
|
|
# Create directory if it doesn't exist
|
|
db_dir = os.path.dirname(db_path)
|
|
if db_dir and not os.path.exists(db_dir):
|
|
os.makedirs(db_dir, exist_ok=True)
|
|
print(f"Created directory: {db_dir}")
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# Create the pt_bt_results table for completed trades
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS pt_bt_results (
|
|
date DATE,
|
|
pair TEXT,
|
|
symbol TEXT,
|
|
open_time DATETIME,
|
|
open_side TEXT,
|
|
open_price REAL,
|
|
open_quantity INTEGER,
|
|
open_disequilibrium REAL,
|
|
close_time DATETIME,
|
|
close_side TEXT,
|
|
close_price REAL,
|
|
close_quantity INTEGER,
|
|
close_disequilibrium REAL,
|
|
symbol_return REAL,
|
|
pair_return REAL
|
|
)
|
|
"""
|
|
)
|
|
cursor.execute("DELETE FROM pt_bt_results;")
|
|
|
|
# Create the outstanding_positions table for open positions
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS outstanding_positions (
|
|
date DATE,
|
|
pair TEXT,
|
|
symbol TEXT,
|
|
position_quantity REAL,
|
|
last_price REAL,
|
|
unrealized_return REAL,
|
|
open_price REAL,
|
|
open_side TEXT
|
|
)
|
|
"""
|
|
)
|
|
cursor.execute("DELETE FROM outstanding_positions;")
|
|
|
|
# Create the config table for storing configuration JSON for reference
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
run_timestamp DATETIME,
|
|
config_file_path TEXT,
|
|
config_json TEXT,
|
|
fit_method_class TEXT,
|
|
datafiles TEXT,
|
|
instruments TEXT
|
|
)
|
|
"""
|
|
)
|
|
cursor.execute("DELETE FROM config;")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
except Exception as e:
|
|
print(f"Error creating result database: {str(e)}")
|
|
raise
|
|
|
|
|
|
def store_config_in_database(
|
|
db_path: str,
|
|
config_file_path: str,
|
|
config: Dict,
|
|
fit_method_class: str,
|
|
datafiles: List[str],
|
|
instruments: List[str],
|
|
) -> None:
|
|
"""
|
|
Store configuration information in the database for reference.
|
|
"""
|
|
import json
|
|
|
|
if db_path.upper() == "NONE":
|
|
return
|
|
|
|
try:
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# Convert config to JSON string
|
|
config_json = json.dumps(config, indent=2, default=str)
|
|
|
|
# Convert lists to comma-separated strings for storage
|
|
datafiles_str = ", ".join(datafiles)
|
|
instruments_str = ", ".join(instruments)
|
|
|
|
# Insert configuration record
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO config (
|
|
run_timestamp, config_file_path, config_json, fit_method_class, datafiles, instruments
|
|
) VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
datetime.now(),
|
|
config_file_path,
|
|
config_json,
|
|
fit_method_class,
|
|
datafiles_str,
|
|
instruments_str,
|
|
),
|
|
)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
print(f"Configuration stored in database")
|
|
|
|
except Exception as e:
|
|
print(f"Error storing configuration in database: {str(e)}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
def store_results_in_database(
|
|
db_path: str, datafile: str, bt_result: "BacktestResult"
|
|
) -> None:
|
|
"""
|
|
Store backtest results in the SQLite database.
|
|
"""
|
|
if db_path.upper() == "NONE":
|
|
return
|
|
|
|
def convert_timestamp(timestamp: Any) -> Optional[datetime]:
|
|
"""Convert pandas Timestamp to Python datetime object for SQLite compatibility."""
|
|
if timestamp is None:
|
|
return None
|
|
if hasattr(timestamp, "to_pydatetime"):
|
|
return timestamp.to_pydatetime()
|
|
return timestamp
|
|
|
|
try:
|
|
# Extract date from datafile name (assuming format like 20250528.mktdata.ohlcv.db)
|
|
filename = os.path.basename(datafile)
|
|
date_str = filename.split(".")[0] # Extract date part
|
|
|
|
# Convert to proper date format
|
|
try:
|
|
date_obj = datetime.strptime(date_str, "%Y%m%d").date()
|
|
except ValueError:
|
|
# If date parsing fails, use current date
|
|
date_obj = datetime.now().date()
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# Process each trade from bt_result
|
|
trades = bt_result.get_trades()
|
|
|
|
for pair_name, symbols in trades.items():
|
|
# Calculate pair return for this pair
|
|
pair_return = 0.0
|
|
pair_trades = []
|
|
|
|
# First pass: collect all trades and calculate returns
|
|
for symbol, symbol_trades in symbols.items():
|
|
if len(symbol_trades) == 0: # No trades for this symbol
|
|
print(
|
|
f"Warning: No trades found for symbol {symbol} in pair {pair_name}"
|
|
)
|
|
continue
|
|
|
|
elif len(symbol_trades) >= 2: # Completed trades (entry + exit)
|
|
# Handle both old and new tuple formats
|
|
if len(symbol_trades[0]) == 2: # Old format: (action, price)
|
|
entry_action, entry_price = symbol_trades[0]
|
|
exit_action, exit_price = symbol_trades[1]
|
|
open_disequilibrium = 0.0 # Fallback for old format
|
|
open_scaled_disequilibrium = 0.0
|
|
close_disequilibrium = 0.0
|
|
close_scaled_disequilibrium = 0.0
|
|
open_time = datetime.now()
|
|
close_time = datetime.now()
|
|
else: # New format: (action, price, disequilibrium, scaled_disequilibrium, timestamp)
|
|
(
|
|
entry_action,
|
|
entry_price,
|
|
open_disequilibrium,
|
|
open_scaled_disequilibrium,
|
|
open_time,
|
|
) = symbol_trades[0]
|
|
(
|
|
exit_action,
|
|
exit_price,
|
|
close_disequilibrium,
|
|
close_scaled_disequilibrium,
|
|
close_time,
|
|
) = symbol_trades[1]
|
|
|
|
# Handle None values
|
|
open_disequilibrium = (
|
|
open_disequilibrium
|
|
if open_disequilibrium is not None
|
|
else 0.0
|
|
)
|
|
open_scaled_disequilibrium = (
|
|
open_scaled_disequilibrium
|
|
if open_scaled_disequilibrium is not None
|
|
else 0.0
|
|
)
|
|
close_disequilibrium = (
|
|
close_disequilibrium
|
|
if close_disequilibrium is not None
|
|
else 0.0
|
|
)
|
|
close_scaled_disequilibrium = (
|
|
close_scaled_disequilibrium
|
|
if close_scaled_disequilibrium is not None
|
|
else 0.0
|
|
)
|
|
|
|
# Convert pandas Timestamps to Python datetime objects
|
|
open_time = convert_timestamp(open_time) or datetime.now()
|
|
close_time = convert_timestamp(close_time) or datetime.now()
|
|
|
|
# Calculate actual share quantities based on funding per pair
|
|
# Split funding equally between the two positions
|
|
funding_per_position = bt_result.config["funding_per_pair"] / 2
|
|
shares = funding_per_position / entry_price
|
|
|
|
# Calculate symbol return
|
|
symbol_return = 0.0
|
|
if entry_action == "BUY" and exit_action == "SELL":
|
|
symbol_return = (exit_price - entry_price) / entry_price * 100
|
|
elif entry_action == "SELL" and exit_action == "BUY":
|
|
symbol_return = (entry_price - exit_price) / entry_price * 100
|
|
|
|
pair_return += symbol_return
|
|
|
|
pair_trades.append(
|
|
{
|
|
"symbol": symbol,
|
|
"entry_action": entry_action,
|
|
"entry_price": entry_price,
|
|
"exit_action": exit_action,
|
|
"exit_price": exit_price,
|
|
"symbol_return": symbol_return,
|
|
"open_disequilibrium": open_disequilibrium,
|
|
"open_scaled_disequilibrium": open_scaled_disequilibrium,
|
|
"close_disequilibrium": close_disequilibrium,
|
|
"close_scaled_disequilibrium": close_scaled_disequilibrium,
|
|
"open_time": open_time,
|
|
"close_time": close_time,
|
|
"shares": shares,
|
|
"is_completed": True,
|
|
}
|
|
)
|
|
|
|
# Skip one-sided trades - they will be handled by outstanding_positions table
|
|
elif len(symbol_trades) == 1:
|
|
print(
|
|
f"Skipping one-sided trade for {symbol} in pair {pair_name} - will be stored in outstanding_positions table"
|
|
)
|
|
continue
|
|
|
|
else:
|
|
# This should not happen, but handle unexpected cases
|
|
print(
|
|
f"Warning: Unexpected number of trades ({len(symbol_trades)}) for symbol {symbol} in pair {pair_name}"
|
|
)
|
|
continue
|
|
|
|
# Second pass: insert completed trade records into database
|
|
for trade in pair_trades:
|
|
# Only store completed trades in pt_bt_results table
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO pt_bt_results (
|
|
date, pair, symbol, open_time, open_side, open_price,
|
|
open_quantity, open_disequilibrium, close_time, close_side,
|
|
close_price, close_quantity, close_disequilibrium,
|
|
symbol_return, pair_return
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
date_obj,
|
|
pair_name,
|
|
trade["symbol"],
|
|
trade["open_time"],
|
|
trade["entry_action"],
|
|
trade["entry_price"],
|
|
trade["shares"],
|
|
trade["open_scaled_disequilibrium"],
|
|
trade["close_time"],
|
|
trade["exit_action"],
|
|
trade["exit_price"],
|
|
trade["shares"],
|
|
trade["close_scaled_disequilibrium"],
|
|
trade["symbol_return"],
|
|
pair_return,
|
|
),
|
|
)
|
|
|
|
# Store outstanding positions in separate table
|
|
outstanding_positions = bt_result.get_outstanding_positions()
|
|
for pos in outstanding_positions:
|
|
# Calculate position quantity (negative for SELL positions)
|
|
position_qty_a = (
|
|
pos["shares_a"] if pos["side_a"] == "BUY" else -pos["shares_a"]
|
|
)
|
|
position_qty_b = (
|
|
pos["shares_b"] if pos["side_b"] == "BUY" else -pos["shares_b"]
|
|
)
|
|
|
|
# Calculate unrealized returns
|
|
# For symbol A: (current_price - open_price) / open_price * 100 * position_direction
|
|
unrealized_return_a = (
|
|
(pos["current_px_a"] - pos["open_px_a"]) / pos["open_px_a"] * 100
|
|
) * (1 if pos["side_a"] == "BUY" else -1)
|
|
unrealized_return_b = (
|
|
(pos["current_px_b"] - pos["open_px_b"]) / pos["open_px_b"] * 100
|
|
) * (1 if pos["side_b"] == "BUY" else -1)
|
|
|
|
# Store outstanding position for symbol A
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO outstanding_positions (
|
|
date, pair, symbol, position_quantity, last_price, unrealized_return, open_price, open_side
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
date_obj,
|
|
pos["pair"],
|
|
pos["symbol_a"],
|
|
position_qty_a,
|
|
pos["current_px_a"],
|
|
unrealized_return_a,
|
|
pos["open_px_a"],
|
|
pos["side_a"],
|
|
),
|
|
)
|
|
|
|
# Store outstanding position for symbol B
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO outstanding_positions (
|
|
date, pair, symbol, position_quantity, last_price, unrealized_return, open_price, open_side
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
date_obj,
|
|
pos["pair"],
|
|
pos["symbol_b"],
|
|
position_qty_b,
|
|
pos["current_px_b"],
|
|
unrealized_return_b,
|
|
pos["open_px_b"],
|
|
pos["side_b"],
|
|
),
|
|
)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
except Exception as e:
|
|
print(f"Error storing results in database: {str(e)}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
class BacktestResult:
|
|
"""
|
|
Class to handle backtest results, trades tracking, PnL calculations, and reporting.
|
|
"""
|
|
|
|
def __init__(self, config: Dict[str, Any]):
|
|
self.config = config
|
|
self.trades: Dict[str, Dict[str, Any]] = {}
|
|
self.total_realized_pnl = 0.0
|
|
self.outstanding_positions: List[Dict[str, Any]] = []
|
|
|
|
def add_trade(
|
|
self,
|
|
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)
|
|
|
|
if pair_nm not in self.trades:
|
|
self.trades[pair_nm] = {symbol: []}
|
|
if symbol not in self.trades[pair_nm]:
|
|
self.trades[pair_nm][symbol] = []
|
|
self.trades[pair_nm][symbol].append(
|
|
(action, price, disequilibrium, scaled_disequilibrium, timestamp)
|
|
)
|
|
|
|
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) -> None:
|
|
"""Add realized PnL to the total."""
|
|
self.total_realized_pnl += realized_pnl
|
|
|
|
def get_total_realized_pnl(self) -> float:
|
|
"""Get total realized PnL."""
|
|
return self.total_realized_pnl
|
|
|
|
def get_outstanding_positions(self) -> List[Dict[str, Any]]:
|
|
"""Get all outstanding positions."""
|
|
return self.outstanding_positions
|
|
|
|
def get_trades(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Get all trades."""
|
|
return self.trades
|
|
|
|
def clear_trades(self) -> None:
|
|
"""Clear all trades (used when processing new files)."""
|
|
self.trades.clear()
|
|
|
|
def collect_single_day_results(self, result: pd.DataFrame) -> None:
|
|
"""Collect and process single day trading results."""
|
|
|
|
print("\n -------------- Suggested Trades ")
|
|
print(result)
|
|
|
|
for row in result.itertuples():
|
|
action = row.action
|
|
symbol = row.symbol
|
|
price = row.price
|
|
disequilibrium = getattr(row, "disequilibrium", None)
|
|
scaled_disequilibrium = getattr(row, "scaled_disequilibrium", None)
|
|
timestamp = getattr(row, "time", None)
|
|
self.add_trade(
|
|
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) -> None:
|
|
"""Print single day results summary."""
|
|
for pair, symbols in self.trades.items():
|
|
print(f"\n--- {pair} ---")
|
|
for symbol, trades in symbols.items():
|
|
for trade_data in trades:
|
|
if len(trade_data) >= 2:
|
|
side, price = trade_data[:2]
|
|
print(f"{symbol} {side} at ${price}")
|
|
|
|
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():
|
|
trade_count = sum(
|
|
len(trades)
|
|
for symbol_trades in data["trades"].values()
|
|
for trades in symbol_trades.values()
|
|
)
|
|
print(f"{filename}: {trade_count} trades")
|
|
|
|
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 ======")
|
|
|
|
for filename, data in all_results.items():
|
|
day_return = 0
|
|
print(f"\n--- {filename} ---")
|
|
|
|
self.outstanding_positions = data["outstanding_positions"]
|
|
|
|
# Process each pair
|
|
for pair, symbols in data["trades"].items():
|
|
pair_return = 0
|
|
pair_trades = []
|
|
|
|
# Calculate individual symbol returns in the pair
|
|
for symbol, trades in symbols.items():
|
|
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)
|
|
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
|
|
trade_return = (price2 - price1) / price1 * 100
|
|
elif action1 == "SELL" and action2 == "BUY":
|
|
# Short position
|
|
trade_return = (price1 - price2) / price1 * 100
|
|
|
|
symbol_return += trade_return
|
|
|
|
# Store trade details for reporting
|
|
pair_trades.append(
|
|
(
|
|
symbol,
|
|
action1,
|
|
price1,
|
|
action2,
|
|
price2,
|
|
trade_return,
|
|
scaled_diseq1,
|
|
scaled_diseq2,
|
|
i + 1, # Trade sequence number
|
|
)
|
|
)
|
|
|
|
pair_return += symbol_return
|
|
|
|
# Print pair returns with disequilibrium information
|
|
if pair_trades:
|
|
print(f" {pair}:")
|
|
for (
|
|
symbol,
|
|
action1,
|
|
price1,
|
|
action2,
|
|
price2,
|
|
trade_return,
|
|
scaled_diseq1,
|
|
scaled_diseq2,
|
|
trade_num,
|
|
) in pair_trades:
|
|
disequil_info = ""
|
|
if (
|
|
scaled_diseq1 is not None
|
|
and scaled_diseq2 is not None
|
|
):
|
|
disequil_info = f" | Open Dis-eq: {scaled_diseq1:.2f}, Close Dis-eq: {scaled_diseq2:.2f}"
|
|
|
|
print(
|
|
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
|
|
|
|
# Print day total return and add to global realized PnL
|
|
if day_return != 0:
|
|
print(f" Day Total Return: {day_return:.2f}%")
|
|
self.add_realized_pnl(day_return)
|
|
|
|
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 ======")
|
|
return
|
|
|
|
print(f"\n====== OUTSTANDING POSITIONS ======")
|
|
print(
|
|
f"{'Pair':<15}"
|
|
f" {'Symbol':<10}"
|
|
f" {'Side':<4}"
|
|
f" {'Shares':<10}"
|
|
f" {'Open $':<8}"
|
|
f" {'Current $':<10}"
|
|
f" {'Value $':<12}"
|
|
f" {'Disequilibrium':<15}"
|
|
)
|
|
print("-" * 100)
|
|
|
|
total_value = 0.0
|
|
|
|
for pos in self.get_outstanding_positions():
|
|
# Print position A
|
|
print(
|
|
f"{pos['pair']:<15}"
|
|
f" {pos['symbol_a']:<10}"
|
|
f" {pos['side_a']:<4}"
|
|
f" {pos['shares_a']:<10.2f}"
|
|
f" {pos['open_px_a']:<8.2f}"
|
|
f" {pos['current_px_a']:<10.2f}"
|
|
f" {pos['current_value_a']:<12.2f}"
|
|
f" {'':<15}"
|
|
)
|
|
|
|
# Print position B
|
|
print(
|
|
f"{'':<15}"
|
|
f" {pos['symbol_b']:<10}"
|
|
f" {pos['side_b']:<4}"
|
|
f" {pos['shares_b']:<10.2f}"
|
|
f" {pos['open_px_b']:<8.2f}"
|
|
f" {pos['current_px_b']:<10.2f}"
|
|
f" {pos['current_value_b']:<12.2f}"
|
|
)
|
|
|
|
# Print pair totals with disequilibrium info
|
|
print(
|
|
f"{'':<15}"
|
|
f" {'PAIR TOTAL':<10}"
|
|
f" {'':<4}"
|
|
f" {'':<10}"
|
|
f" {'':<8}"
|
|
f" {'':<10}"
|
|
f" {pos['total_current_value']:<12.2f}"
|
|
)
|
|
|
|
# Print disequilibrium details
|
|
print(
|
|
f"{'':<15}"
|
|
f" {'DISEQUIL':<10}"
|
|
f" {'':<4}"
|
|
f" {'':<10}"
|
|
f" {'':<8}"
|
|
f" {'':<10}"
|
|
f" Raw: {pos['current_disequilibrium']:<6.4f}"
|
|
f" Scaled: {pos['current_scaled_disequilibrium']:<6.4f}"
|
|
)
|
|
|
|
print("-" * 100)
|
|
|
|
total_value += pos["total_current_value"]
|
|
|
|
print(f"{'TOTAL OUTSTANDING VALUE':<80} ${total_value:<12.2f}")
|
|
|
|
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: 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.
|
|
|
|
Args:
|
|
pair: TradingPair object
|
|
pair_result_df: DataFrame with pair results
|
|
last_row_index: Index of the last row in the data
|
|
open_side_a, open_side_b: Trading sides for symbols A and B
|
|
open_px_a, open_px_b: Opening prices for symbols A and B
|
|
open_tstamp: Opening timestamp
|
|
"""
|
|
if pair_result_df is None or pair_result_df.empty:
|
|
return 0, 0, 0
|
|
|
|
last_row = pair_result_df.loc[last_row_index]
|
|
last_tstamp = last_row["tstamp"]
|
|
colname_a, colname_b = pair.colnames()
|
|
last_px_a = last_row[colname_a]
|
|
last_px_b = last_row[colname_b]
|
|
|
|
# Calculate share quantities based on funding per pair
|
|
# Split funding equally between the two positions
|
|
funding_per_position = self.config["funding_per_pair"] / 2
|
|
shares_a = funding_per_position / open_px_a
|
|
shares_b = funding_per_position / open_px_b
|
|
|
|
# Calculate current position values (shares * current price)
|
|
current_value_a = shares_a * last_px_a
|
|
current_value_b = shares_b * last_px_b
|
|
total_current_value = current_value_a + current_value_b
|
|
|
|
# Get disequilibrium information
|
|
current_disequilibrium = last_row["disequilibrium"]
|
|
current_scaled_disequilibrium = last_row["scaled_disequilibrium"]
|
|
|
|
# Store outstanding positions
|
|
self.add_outstanding_position(
|
|
{
|
|
"pair": str(pair),
|
|
"symbol_a": pair.symbol_a_,
|
|
"symbol_b": pair.symbol_b_,
|
|
"side_a": open_side_a,
|
|
"side_b": open_side_b,
|
|
"shares_a": shares_a,
|
|
"shares_b": shares_b,
|
|
"open_px_a": open_px_a,
|
|
"open_px_b": open_px_b,
|
|
"current_px_a": last_px_a,
|
|
"current_px_b": last_px_b,
|
|
"current_value_a": current_value_a,
|
|
"current_value_b": current_value_b,
|
|
"total_current_value": total_current_value,
|
|
"open_time": open_tstamp,
|
|
"last_time": last_tstamp,
|
|
"current_abs_term": current_scaled_disequilibrium,
|
|
"current_disequilibrium": current_disequilibrium,
|
|
"current_scaled_disequilibrium": current_scaled_disequilibrium,
|
|
}
|
|
)
|
|
|
|
# Print position details
|
|
print(f"{pair}: NO CLOSE SIGNAL FOUND - Position held until end of session")
|
|
print(f" Open: {open_tstamp} | Last: {last_tstamp}")
|
|
print(
|
|
f" {pair.symbol_a_}: {open_side_a} {shares_a:.2f} shares @ ${open_px_a:.2f} -> ${last_px_a:.2f} | Value: ${current_value_a:.2f}"
|
|
)
|
|
print(
|
|
f" {pair.symbol_b_}: {open_side_b} {shares_b:.2f} shares @ ${open_px_b:.2f} -> ${last_px_b:.2f} | Value: ${current_value_b:.2f}"
|
|
)
|
|
print(f" Total Value: ${total_current_value:.2f}")
|
|
print(
|
|
f" Disequilibrium: {current_disequilibrium:.4f} | Scaled: {current_scaled_disequilibrium:.4f}"
|
|
)
|
|
|
|
return current_value_a, current_value_b, total_current_value
|