This commit is contained in:
Oleg Sheynin 2025-07-22 18:04:23 +00:00
parent 9bb36dddd7
commit aac8b9dc50
4 changed files with 1074 additions and 176 deletions

View File

@ -27,7 +27,7 @@
# "close_outstanding_positions": false, # "close_outstanding_positions": false,
"trading_hours": { "trading_hours": {
"begin_session": "9:30:00", "begin_session": "9:30:00",
"end_session": "19:00:00", "end_session": "22:30:00",
"timezone": "America/New_York" "timezone": "America/New_York"
} }
} }

View File

@ -570,9 +570,12 @@ class BacktestResult:
symbol_trades = [trade for trade in trades if trade["symbol"] == symbol] symbol_trades = [trade for trade in trades if trade["symbol"] == symbol]
# Calculate returns for all trade combinations # Calculate returns for all trade combinations
for i in range(len(symbol_trades) - 1): for idx in range(0, len(symbol_trades), 2):
trade1 = trades[i] trade1 = trades[idx]
trade2 = trades[i + 1] trade2 = trades[idx + 1]
assert trade1["timestamp"] < trade2["timestamp"], f"Trade 1: {trade1['timestamp']} is not less than Trade 2: {trade2['timestamp']}"
assert trade1["action"] == "OPEN" and trade2["action"] == "CLOSE", f"Trade 1: {trade1['action']} and Trade 2: {trade2['action']} are the same"
# Calculate return based on action combination # Calculate return based on action combination
trade_return = 0 trade_return = 0
@ -589,6 +592,8 @@ class BacktestResult:
pair_trades.append( pair_trades.append(
( (
symbol, symbol,
trade1["timestamp"],
trade2["timestamp"],
trade1["side"], trade1["side"],
trade1["price"], trade1["price"],
trade2["side"], trade2["side"],
@ -596,7 +601,7 @@ class BacktestResult:
trade_return, trade_return,
trade1["scaled_disequilibrium"], trade1["scaled_disequilibrium"],
trade2["scaled_disequilibrium"], trade2["scaled_disequilibrium"],
i + 1, # Trade sequence number f"{idx + 1}", # Trade sequence number
) )
) )
@ -607,6 +612,8 @@ class BacktestResult:
print(f" {pair}:") print(f" {pair}:")
for ( for (
symbol, symbol,
trade1["timestamp"],
trade2["timestamp"],
trade1["side"], trade1["side"],
trade1["price"], trade1["price"],
trade2["side"], trade2["side"],
@ -625,7 +632,7 @@ class BacktestResult:
f" Close Dis-eq: {trade2["scaled_disequilibrium"]:.2f}" f" Close Dis-eq: {trade2["scaled_disequilibrium"]:.2f}"
print( print(
f" {symbol} (Trade #{trade_num}):" f" {trade2['timestamp'].time()} {symbol} (Trade #{trade_num}):"
f" {trade1["side"]} @ ${trade1["price"]:.2f}," f" {trade1["side"]} @ ${trade1["price"]:.2f},"
f" {trade2["side"]} @ ${trade2["price"]:.2f}," f" {trade2["side"]} @ ${trade2["price"]:.2f},"
f" Return: {trade_return:.2f}%{disequil_info}" f" Return: {trade_return:.2f}%{disequil_info}"
@ -633,6 +640,7 @@ class BacktestResult:
print(f" Pair Total Return: {pair_return:.2f}%") print(f" Pair Total Return: {pair_return:.2f}%")
day_return += pair_return day_return += pair_return
# Print day total return and add to global realized PnL # Print day total return and add to global realized PnL
if day_return != 0: if day_return != 0:
print(f" Day Total Return: {day_return:.2f}%") print(f" Day Total Return: {day_return:.2f}%")

View File

@ -10,15 +10,17 @@ from statsmodels.tsa.vector_ar.vecm import VECM, VECMResults
NanoPerMin = 1e9 NanoPerMin = 1e9
class RollingFit(PairsTradingFitMethod): class RollingFit(PairsTradingFitMethod):
''' """
N O T E: N O T E:
========= =========
- This class remains to be abstract - This class remains to be abstract
- The following methods are to be implemented in the subclass: - The following methods are to be implemented in the subclass:
- create_trading_pair() - create_trading_pair()
========= =========
''' """
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@ -33,16 +35,18 @@ class RollingFit(PairsTradingFitMethod):
pair.user_data_["state"] = PairState.INITIAL pair.user_data_["state"] = PairState.INITIAL
# Initialize trades DataFrame with proper dtypes to avoid concatenation warnings # Initialize trades DataFrame with proper dtypes to avoid concatenation warnings
pair.user_data_["trades"] = pd.DataFrame(columns=self.TRADES_COLUMNS).astype({ pair.user_data_["trades"] = pd.DataFrame(columns=self.TRADES_COLUMNS).astype(
"time": "datetime64[ns]", {
"symbol": "string", "time": "datetime64[ns]",
"side": "string", "symbol": "string",
"action": "string", "side": "string",
"price": "float64", "action": "string",
"disequilibrium": "float64", "price": "float64",
"scaled_disequilibrium": "float64", "disequilibrium": "float64",
"pair": "object" "scaled_disequilibrium": "float64",
}) "pair": "object",
}
)
training_minutes = config["training_minutes"] training_minutes = config["training_minutes"]
curr_predicted_row_idx = 0 curr_predicted_row_idx = 0
@ -66,7 +70,9 @@ class RollingFit(PairsTradingFitMethod):
# ================================ PREDICTION ================================ # ================================ PREDICTION ================================
self.pair_predict_result_ = pair.predict() self.pair_predict_result_ = pair.predict()
except Exception as e: except Exception as e:
raise RuntimeError(f"{pair}: TrainingPrediction failed: {str(e)}") from e raise RuntimeError(
f"{pair}: TrainingPrediction failed: {str(e)}"
) from e
# break # break
@ -93,7 +99,13 @@ class RollingFit(PairsTradingFitMethod):
pred_row = predicted_df.iloc[curr_predicted_row_idx] pred_row = predicted_df.iloc[curr_predicted_row_idx]
scaled_disequilibrium = pred_row["scaled_disequilibrium"] scaled_disequilibrium = pred_row["scaled_disequilibrium"]
if pair.user_data_["state"] in [PairState.INITIAL, PairState.CLOSE, PairState.CLOSE_POSITION]: if pair.user_data_["state"] in [
PairState.INITIAL,
PairState.CLOSE,
PairState.CLOSE_POSITION,
PairState.CLOSE_STOP_LOSS,
PairState.CLOSE_STOP_PROFIT,
]:
if scaled_disequilibrium >= open_threshold: if scaled_disequilibrium >= open_threshold:
open_trades = self._get_open_trades( open_trades = self._get_open_trades(
pair, row=pred_row, open_threshold=open_threshold pair, row=pred_row, open_threshold=open_threshold
@ -121,7 +133,9 @@ class RollingFit(PairsTradingFitMethod):
pair, row=pred_row, close_threshold=close_threshold pair, row=pred_row, close_threshold=close_threshold
) )
if close_trades is not None: if close_trades is not None:
close_trades["status"] = pair.user_data_["stop_close_state"].name close_trades["status"] = pair.user_data_[
"stop_close_state"
].name
print(f"STOP CLOSE TRADES:\n{close_trades}") print(f"STOP CLOSE TRADES:\n{close_trades}")
pair.add_trades(close_trades) pair.add_trades(close_trades)
pair.user_data_["state"] = pair.user_data_["stop_close_state"] pair.user_data_["state"] = pair.user_data_["stop_close_state"]
@ -129,15 +143,11 @@ class RollingFit(PairsTradingFitMethod):
# Outstanding positions # Outstanding positions
if pair.user_data_["state"] == PairState.OPEN: if pair.user_data_["state"] == PairState.OPEN:
print( print(f"{pair}: *** Position is NOT CLOSED. ***")
f"{pair}: *** Position is NOT CLOSED. ***"
)
# outstanding positions # outstanding positions
if config["close_outstanding_positions"]: if config["close_outstanding_positions"]:
close_position_trades = self._get_close_trades( close_position_trades = self._get_close_trades(
pair=pair, pair=pair, row=pred_row, close_threshold=close_threshold
row=pred_row,
close_threshold=close_threshold
) )
if close_position_trades is not None: if close_position_trades is not None:
close_position_trades["status"] = PairState.CLOSE_POSITION.name close_position_trades["status"] = PairState.CLOSE_POSITION.name
@ -223,15 +233,17 @@ class RollingFit(PairsTradingFitMethod):
columns=self.TRADES_COLUMNS, columns=self.TRADES_COLUMNS,
) )
# Ensure consistent dtypes # Ensure consistent dtypes
return df.astype({ return df.astype(
"time": "datetime64[ns]", {
"action": "string", "time": "datetime64[ns]",
"symbol": "string", "action": "string",
"price": "float64", "symbol": "string",
"disequilibrium": "float64", "price": "float64",
"scaled_disequilibrium": "float64", "disequilibrium": "float64",
"pair": "object" "scaled_disequilibrium": "float64",
}) "pair": "object",
}
)
def _get_close_trades( def _get_close_trades(
self, pair: TradingPair, row: pd.Series, close_threshold: float self, pair: TradingPair, row: pd.Series, close_threshold: float
@ -277,16 +289,17 @@ class RollingFit(PairsTradingFitMethod):
columns=self.TRADES_COLUMNS, columns=self.TRADES_COLUMNS,
) )
# Ensure consistent dtypes # Ensure consistent dtypes
return df.astype({ return df.astype(
"time": "datetime64[ns]", {
"action": "string", "time": "datetime64[ns]",
"symbol": "string", "action": "string",
"price": "float64", "symbol": "string",
"disequilibrium": "float64", "price": "float64",
"scaled_disequilibrium": "float64", "disequilibrium": "float64",
"pair": "object" "scaled_disequilibrium": "float64",
}) "pair": "object",
}
)
def reset(self) -> None: def reset(self) -> None:
curr_training_start_idx = 0 curr_training_start_idx = 0

File diff suppressed because one or more lines are too long