bug fix
This commit is contained in:
parent
0e83142d0a
commit
31eb9f800c
@ -2,7 +2,7 @@
|
|||||||
"security_type": "CRYPTO",
|
"security_type": "CRYPTO",
|
||||||
"data_directory": "./data/crypto",
|
"data_directory": "./data/crypto",
|
||||||
"datafiles": [
|
"datafiles": [
|
||||||
"2025*.mktdata.ohlcv.db"
|
"20250602.mktdata.ohlcv.db"
|
||||||
],
|
],
|
||||||
"db_table_name": "md_1min_bars",
|
"db_table_name": "md_1min_bars",
|
||||||
"exchange_id": "BNBSPOT",
|
"exchange_id": "BNBSPOT",
|
||||||
@ -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": "21:30:00",
|
"end_session": "19:00:00",
|
||||||
"timezone": "America/New_York"
|
"timezone": "America/New_York"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -12,8 +12,9 @@ NanoPerMin = 1e9
|
|||||||
class PairsTradingFitMethod(ABC):
|
class PairsTradingFitMethod(ABC):
|
||||||
TRADES_COLUMNS = [
|
TRADES_COLUMNS = [
|
||||||
"time",
|
"time",
|
||||||
"action",
|
|
||||||
"symbol",
|
"symbol",
|
||||||
|
"side",
|
||||||
|
"action",
|
||||||
"price",
|
"price",
|
||||||
"disequilibrium",
|
"disequilibrium",
|
||||||
"scaled_disequilibrium",
|
"scaled_disequilibrium",
|
||||||
|
|||||||
@ -442,11 +442,13 @@ class BacktestResult:
|
|||||||
self,
|
self,
|
||||||
pair_nm: str,
|
pair_nm: str,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
|
side: str,
|
||||||
action: str,
|
action: str,
|
||||||
price: Any,
|
price: Any,
|
||||||
disequilibrium: Optional[float] = None,
|
disequilibrium: Optional[float] = None,
|
||||||
scaled_disequilibrium: Optional[float] = None,
|
scaled_disequilibrium: Optional[float] = None,
|
||||||
timestamp: Optional[datetime] = None,
|
timestamp: Optional[datetime] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a trade to the results tracking."""
|
"""Add a trade to the results tracking."""
|
||||||
pair_nm = str(pair_nm)
|
pair_nm = str(pair_nm)
|
||||||
@ -456,7 +458,15 @@ class BacktestResult:
|
|||||||
if symbol not in self.trades[pair_nm]:
|
if symbol not in self.trades[pair_nm]:
|
||||||
self.trades[pair_nm][symbol] = []
|
self.trades[pair_nm][symbol] = []
|
||||||
self.trades[pair_nm][symbol].append(
|
self.trades[pair_nm][symbol].append(
|
||||||
(action, price, disequilibrium, scaled_disequilibrium, timestamp)
|
{"symbol":symbol,
|
||||||
|
"side":side,
|
||||||
|
"action":action,
|
||||||
|
"price":price,
|
||||||
|
"disequilibrium":disequilibrium,
|
||||||
|
"scaled_disequilibrium":scaled_disequilibrium,
|
||||||
|
"timestamp":timestamp,
|
||||||
|
"status":status
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_outstanding_position(self, position: Dict[str, Any]) -> None:
|
def add_outstanding_position(self, position: Dict[str, Any]) -> None:
|
||||||
@ -493,6 +503,7 @@ class BacktestResult:
|
|||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
for row in result.itertuples():
|
for row in result.itertuples():
|
||||||
|
side = row.side
|
||||||
action = row.action
|
action = row.action
|
||||||
symbol = row.symbol
|
symbol = row.symbol
|
||||||
price = row.price
|
price = row.price
|
||||||
@ -502,15 +513,17 @@ class BacktestResult:
|
|||||||
timestamp = getattr(row, "time")
|
timestamp = getattr(row, "time")
|
||||||
else:
|
else:
|
||||||
timestamp = convert_timestamp(row.Index)
|
timestamp = convert_timestamp(row.Index)
|
||||||
|
status = row.status
|
||||||
self.add_trade(
|
self.add_trade(
|
||||||
pair_nm=str(row.pair),
|
pair_nm=str(row.pair),
|
||||||
action=str(action),
|
|
||||||
symbol=str(symbol),
|
symbol=str(symbol),
|
||||||
|
side=str(side),
|
||||||
|
action=str(action),
|
||||||
price=float(str(price)),
|
price=float(str(price)),
|
||||||
disequilibrium=disequilibrium,
|
disequilibrium=disequilibrium,
|
||||||
scaled_disequilibrium=scaled_disequilibrium,
|
scaled_disequilibrium=scaled_disequilibrium,
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
|
status=str(status) if status is not None else "?",
|
||||||
)
|
)
|
||||||
|
|
||||||
def print_single_day_results(self) -> None:
|
def print_single_day_results(self) -> None:
|
||||||
@ -553,42 +566,22 @@ class BacktestResult:
|
|||||||
for symbol, trades in symbols.items():
|
for symbol, trades in symbols.items():
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
symbol_return = 0
|
symbol_return = 0
|
||||||
symbol_trades = []
|
symbol_trades = [trade for trade in trades if trade["symbol"] == symbol]
|
||||||
|
|
||||||
# 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
|
# Calculate returns for all trade combinations
|
||||||
for i in range(len(symbol_trades) - 1):
|
for i in range(len(symbol_trades) - 1):
|
||||||
trade1 = symbol_trades[i]
|
trade1 = trades[i]
|
||||||
trade2 = symbol_trades[i + 1]
|
trade2 = trades[i + 1]
|
||||||
|
|
||||||
action1, price1, diseq1, scaled_diseq1, ts1 = trade1
|
|
||||||
action2, price2, diseq2, scaled_diseq2, ts2 = trade2
|
|
||||||
|
|
||||||
# Calculate return based on action combination
|
# Calculate return based on action combination
|
||||||
trade_return = 0
|
trade_return = 0
|
||||||
if action1 == "BUY" and action2 == "SELL":
|
if trade1["side"] == "BUY" and trade2["side"] == "SELL":
|
||||||
# Long position
|
# Long position
|
||||||
trade_return = (price2 - price1) / price1 * 100
|
trade_return = (trade2["price"] - trade1["price"]) / trade1["price"] * 100
|
||||||
elif action1 == "SELL" and action2 == "BUY":
|
elif trade1["side"] == "SELL" and trade2["side"] == "BUY":
|
||||||
# Short position
|
# Short position
|
||||||
trade_return = (price1 - price2) / price1 * 100
|
trade_return = (trade1["price"] - trade2["price"]) / trade1["price"] * 100
|
||||||
|
|
||||||
symbol_return += trade_return
|
symbol_return += trade_return
|
||||||
|
|
||||||
@ -596,13 +589,13 @@ class BacktestResult:
|
|||||||
pair_trades.append(
|
pair_trades.append(
|
||||||
(
|
(
|
||||||
symbol,
|
symbol,
|
||||||
action1,
|
trade1["side"],
|
||||||
price1,
|
trade1["price"],
|
||||||
action2,
|
trade2["side"],
|
||||||
price2,
|
trade2["price"],
|
||||||
trade_return,
|
trade_return,
|
||||||
scaled_diseq1,
|
trade1["scaled_disequilibrium"],
|
||||||
scaled_diseq2,
|
trade2["scaled_disequilibrium"],
|
||||||
i + 1, # Trade sequence number
|
i + 1, # Trade sequence number
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -614,24 +607,28 @@ class BacktestResult:
|
|||||||
print(f" {pair}:")
|
print(f" {pair}:")
|
||||||
for (
|
for (
|
||||||
symbol,
|
symbol,
|
||||||
action1,
|
trade1["side"],
|
||||||
price1,
|
trade1["price"],
|
||||||
action2,
|
trade2["side"],
|
||||||
price2,
|
trade2["price"],
|
||||||
trade_return,
|
trade_return,
|
||||||
scaled_diseq1,
|
trade1["scaled_disequilibrium"],
|
||||||
scaled_diseq2,
|
trade2["scaled_disequilibrium"],
|
||||||
trade_num,
|
trade_num,
|
||||||
) in pair_trades:
|
) in pair_trades:
|
||||||
disequil_info = ""
|
disequil_info = ""
|
||||||
if (
|
if (
|
||||||
scaled_diseq1 is not None
|
trade1["scaled_disequilibrium"] is not None
|
||||||
and scaled_diseq2 is not None
|
and trade2["scaled_disequilibrium"] is not None
|
||||||
):
|
):
|
||||||
disequil_info = f" | Open Dis-eq: {scaled_diseq1:.2f}, Close Dis-eq: {scaled_diseq2:.2f}"
|
disequil_info = f" | Open Dis-eq: {trade1["scaled_disequilibrium"]:.2f},"
|
||||||
|
f" Close Dis-eq: {trade2["scaled_disequilibrium"]:.2f}"
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f" {symbol} (Trade #{trade_num}): {action1} @ ${price1:.2f}, {action2} @ ${price2:.2f}, Return: {trade_return:.2f}%{disequil_info}"
|
f" {symbol} (Trade #{trade_num}):"
|
||||||
|
f" {trade1["side"]} @ ${trade1["price"]:.2f},"
|
||||||
|
f" {trade2["side"]} @ ${trade2["price"]:.2f},"
|
||||||
|
f" Return: {trade_return:.2f}%{disequil_info}"
|
||||||
)
|
)
|
||||||
print(f" Pair Total Return: {pair_return:.2f}%")
|
print(f" Pair Total Return: {pair_return:.2f}%")
|
||||||
day_return += pair_return
|
day_return += pair_return
|
||||||
|
|||||||
@ -35,8 +35,9 @@ class RollingFit(PairsTradingFitMethod):
|
|||||||
# 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]",
|
"time": "datetime64[ns]",
|
||||||
"action": "string",
|
|
||||||
"symbol": "string",
|
"symbol": "string",
|
||||||
|
"side": "string",
|
||||||
|
"action": "string",
|
||||||
"price": "float64",
|
"price": "float64",
|
||||||
"disequilibrium": "float64",
|
"disequilibrium": "float64",
|
||||||
"scaled_disequilibrium": "float64",
|
"scaled_disequilibrium": "float64",
|
||||||
@ -136,7 +137,7 @@ class RollingFit(PairsTradingFitMethod):
|
|||||||
close_position_trades = self._get_close_trades(
|
close_position_trades = self._get_close_trades(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
row=pred_row,
|
row=pred_row,
|
||||||
close_threshold=close_threshold,
|
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
|
||||||
@ -197,8 +198,9 @@ class RollingFit(PairsTradingFitMethod):
|
|||||||
trd_signal_tuples = [
|
trd_signal_tuples = [
|
||||||
(
|
(
|
||||||
open_tstamp,
|
open_tstamp,
|
||||||
open_side_a,
|
|
||||||
pair.symbol_a_,
|
pair.symbol_a_,
|
||||||
|
open_side_a,
|
||||||
|
"OPEN",
|
||||||
open_px_a,
|
open_px_a,
|
||||||
open_disequilibrium,
|
open_disequilibrium,
|
||||||
open_scaled_disequilibrium,
|
open_scaled_disequilibrium,
|
||||||
@ -206,8 +208,9 @@ class RollingFit(PairsTradingFitMethod):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
open_tstamp,
|
open_tstamp,
|
||||||
open_side_b,
|
|
||||||
pair.symbol_b_,
|
pair.symbol_b_,
|
||||||
|
open_side_b,
|
||||||
|
"OPEN",
|
||||||
open_px_b,
|
open_px_b,
|
||||||
open_disequilibrium,
|
open_disequilibrium,
|
||||||
open_scaled_disequilibrium,
|
open_scaled_disequilibrium,
|
||||||
@ -248,8 +251,9 @@ class RollingFit(PairsTradingFitMethod):
|
|||||||
trd_signal_tuples = [
|
trd_signal_tuples = [
|
||||||
(
|
(
|
||||||
close_tstamp,
|
close_tstamp,
|
||||||
close_side_a,
|
|
||||||
pair.symbol_a_,
|
pair.symbol_a_,
|
||||||
|
close_side_a,
|
||||||
|
"CLOSE",
|
||||||
close_px_a,
|
close_px_a,
|
||||||
close_disequilibrium,
|
close_disequilibrium,
|
||||||
close_scaled_disequilibrium,
|
close_scaled_disequilibrium,
|
||||||
@ -257,8 +261,9 @@ class RollingFit(PairsTradingFitMethod):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
close_tstamp,
|
close_tstamp,
|
||||||
close_side_b,
|
|
||||||
pair.symbol_b_,
|
pair.symbol_b_,
|
||||||
|
close_side_b,
|
||||||
|
"CLOSE",
|
||||||
close_px_b,
|
close_px_b,
|
||||||
close_disequilibrium,
|
close_disequilibrium,
|
||||||
close_scaled_disequilibrium,
|
close_scaled_disequilibrium,
|
||||||
|
|||||||
@ -94,10 +94,6 @@ class VECMRollingFit(RollingFit):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def run_pair(
|
|
||||||
self, pair: TradingPair, bt_result: BacktestResult
|
|
||||||
) -> Optional[pd.DataFrame]:
|
|
||||||
return super().run_pair(pair, bt_result)
|
|
||||||
def create_trading_pair(
|
def create_trading_pair(
|
||||||
self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str
|
self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str
|
||||||
) -> TradingPair:
|
) -> TradingPair:
|
||||||
|
|||||||
@ -20,19 +20,18 @@ class ZScoreTradingPair(TradingPair):
|
|||||||
|
|
||||||
def _fit_zscore(self) -> None:
|
def _fit_zscore(self) -> None:
|
||||||
assert self.training_df_ is not None
|
assert self.training_df_ is not None
|
||||||
a = self.training_df_[self.colnames()].iloc[:, 0]
|
symbol_a_px_series = self.training_df_[self.colnames()].iloc[:, 0]
|
||||||
b = self.training_df_[self.colnames()].iloc[:, 1]
|
symbol_b_px_series = self.training_df_[self.colnames()].iloc[:, 1]
|
||||||
|
|
||||||
a,b = a.align(b, axis=0)
|
symbol_a_px_series,symbol_b_px_series = symbol_a_px_series.align(symbol_b_px_series, axis=0)
|
||||||
|
|
||||||
|
X = sm.add_constant(symbol_b_px_series)
|
||||||
X = sm.add_constant(b)
|
self.zscore_model_ = sm.OLS(symbol_a_px_series, X).fit()
|
||||||
self.zscore_model_ = sm.OLS(a, X).fit()
|
|
||||||
assert self.zscore_model_ is not None
|
assert self.zscore_model_ is not None
|
||||||
hedge_ratio = self.zscore_model_.params.iloc[1]
|
hedge_ratio = self.zscore_model_.params.iloc[1]
|
||||||
|
|
||||||
# Calculate spread and Z-score
|
# Calculate spread and Z-score
|
||||||
spread = a - hedge_ratio * b
|
spread = symbol_a_px_series - hedge_ratio * symbol_b_px_series
|
||||||
self.zscore_df_ = (spread - spread.mean()) / spread.std()
|
self.zscore_df_ = (spread - spread.mean()) / spread.std()
|
||||||
|
|
||||||
def predict(self) -> pd.DataFrame:
|
def predict(self) -> pd.DataFrame:
|
||||||
@ -55,17 +54,13 @@ class ZScoreTradingPair(TradingPair):
|
|||||||
self.pair_predict_result_ = pd.concat([self.pair_predict_result_, predicted_df], ignore_index=True)
|
self.pair_predict_result_ = pd.concat([self.pair_predict_result_, predicted_df], ignore_index=True)
|
||||||
# Reset index to ensure proper indexing
|
# Reset index to ensure proper indexing
|
||||||
self.pair_predict_result_ = self.pair_predict_result_.reset_index(drop=True)
|
self.pair_predict_result_ = self.pair_predict_result_.reset_index(drop=True)
|
||||||
return self.pair_predict_result_
|
return self.pair_predict_result_.dropna()
|
||||||
|
|
||||||
|
|
||||||
class ZScoreRollingFit(RollingFit):
|
class ZScoreRollingFit(RollingFit):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def run_pair(
|
|
||||||
self, pair: TradingPair, bt_result: BacktestResult
|
|
||||||
) -> Optional[pd.DataFrame]:
|
|
||||||
return super().run_pair(pair, bt_result)
|
|
||||||
def create_trading_pair(
|
def create_trading_pair(
|
||||||
self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str
|
self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str
|
||||||
) -> TradingPair:
|
) -> TradingPair:
|
||||||
|
|||||||
@ -72,13 +72,17 @@ def run_backtest(
|
|||||||
bt_result: BacktestResult = BacktestResult(config=config)
|
bt_result: BacktestResult = BacktestResult(config=config)
|
||||||
|
|
||||||
pairs_trades = []
|
pairs_trades = []
|
||||||
for pair in create_pairs(datafile=datafile, fit_method=fit_method, price_column=price_column, config=config, instruments=instruments):
|
for pair in create_pairs(
|
||||||
single_pair_trades = fit_method.run_pair(
|
datafile=datafile,
|
||||||
pair=pair, bt_result=bt_result
|
fit_method=fit_method,
|
||||||
)
|
price_column=price_column,
|
||||||
|
config=config,
|
||||||
|
instruments=instruments,
|
||||||
|
):
|
||||||
|
single_pair_trades = fit_method.run_pair(pair=pair, bt_result=bt_result)
|
||||||
if single_pair_trades is not None and len(single_pair_trades) > 0:
|
if single_pair_trades is not None and len(single_pair_trades) > 0:
|
||||||
pairs_trades.append(single_pair_trades)
|
pairs_trades.append(single_pair_trades)
|
||||||
print(f"pairs_trades: {pairs_trades}")
|
print(f"pairs_trades:\n{pairs_trades}")
|
||||||
# Check if result_list has any data before concatenating
|
# Check if result_list has any data before concatenating
|
||||||
if len(pairs_trades) == 0:
|
if len(pairs_trades) == 0:
|
||||||
print("No trading signals found for any pairs")
|
print("No trading signals found for any pairs")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user