bug fix
This commit is contained in:
parent
0e83142d0a
commit
31eb9f800c
@ -2,7 +2,7 @@
|
||||
"security_type": "CRYPTO",
|
||||
"data_directory": "./data/crypto",
|
||||
"datafiles": [
|
||||
"2025*.mktdata.ohlcv.db"
|
||||
"20250602.mktdata.ohlcv.db"
|
||||
],
|
||||
"db_table_name": "md_1min_bars",
|
||||
"exchange_id": "BNBSPOT",
|
||||
@ -27,7 +27,7 @@
|
||||
# "close_outstanding_positions": false,
|
||||
"trading_hours": {
|
||||
"begin_session": "9:30:00",
|
||||
"end_session": "21:30:00",
|
||||
"end_session": "19:00:00",
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,9 @@ NanoPerMin = 1e9
|
||||
class PairsTradingFitMethod(ABC):
|
||||
TRADES_COLUMNS = [
|
||||
"time",
|
||||
"action",
|
||||
"symbol",
|
||||
"side",
|
||||
"action",
|
||||
"price",
|
||||
"disequilibrium",
|
||||
"scaled_disequilibrium",
|
||||
|
||||
@ -442,11 +442,13 @@ class BacktestResult:
|
||||
self,
|
||||
pair_nm: str,
|
||||
symbol: str,
|
||||
side: str,
|
||||
action: str,
|
||||
price: Any,
|
||||
disequilibrium: Optional[float] = None,
|
||||
scaled_disequilibrium: Optional[float] = None,
|
||||
timestamp: Optional[datetime] = None,
|
||||
status: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Add a trade to the results tracking."""
|
||||
pair_nm = str(pair_nm)
|
||||
@ -456,7 +458,15 @@ class BacktestResult:
|
||||
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)
|
||||
{"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:
|
||||
@ -493,6 +503,7 @@ class BacktestResult:
|
||||
print(result)
|
||||
|
||||
for row in result.itertuples():
|
||||
side = row.side
|
||||
action = row.action
|
||||
symbol = row.symbol
|
||||
price = row.price
|
||||
@ -502,15 +513,17 @@ class BacktestResult:
|
||||
timestamp = getattr(row, "time")
|
||||
else:
|
||||
timestamp = convert_timestamp(row.Index)
|
||||
|
||||
status = row.status
|
||||
self.add_trade(
|
||||
pair_nm=str(row.pair),
|
||||
action=str(action),
|
||||
symbol=str(symbol),
|
||||
side=str(side),
|
||||
action=str(action),
|
||||
price=float(str(price)),
|
||||
disequilibrium=disequilibrium,
|
||||
scaled_disequilibrium=scaled_disequilibrium,
|
||||
timestamp=timestamp,
|
||||
status=str(status) if status is not None else "?",
|
||||
)
|
||||
|
||||
def print_single_day_results(self) -> None:
|
||||
@ -553,42 +566,22 @@ class BacktestResult:
|
||||
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))
|
||||
symbol_trades = [trade for trade in trades if trade["symbol"] == symbol]
|
||||
|
||||
# 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
|
||||
trade1 = trades[i]
|
||||
trade2 = trades[i + 1]
|
||||
|
||||
# Calculate return based on action combination
|
||||
trade_return = 0
|
||||
if action1 == "BUY" and action2 == "SELL":
|
||||
if trade1["side"] == "BUY" and trade2["side"] == "SELL":
|
||||
# Long position
|
||||
trade_return = (price2 - price1) / price1 * 100
|
||||
elif action1 == "SELL" and action2 == "BUY":
|
||||
trade_return = (trade2["price"] - trade1["price"]) / trade1["price"] * 100
|
||||
elif trade1["side"] == "SELL" and trade2["side"] == "BUY":
|
||||
# Short position
|
||||
trade_return = (price1 - price2) / price1 * 100
|
||||
trade_return = (trade1["price"] - trade2["price"]) / trade1["price"] * 100
|
||||
|
||||
symbol_return += trade_return
|
||||
|
||||
@ -596,13 +589,13 @@ class BacktestResult:
|
||||
pair_trades.append(
|
||||
(
|
||||
symbol,
|
||||
action1,
|
||||
price1,
|
||||
action2,
|
||||
price2,
|
||||
trade1["side"],
|
||||
trade1["price"],
|
||||
trade2["side"],
|
||||
trade2["price"],
|
||||
trade_return,
|
||||
scaled_diseq1,
|
||||
scaled_diseq2,
|
||||
trade1["scaled_disequilibrium"],
|
||||
trade2["scaled_disequilibrium"],
|
||||
i + 1, # Trade sequence number
|
||||
)
|
||||
)
|
||||
@ -614,24 +607,28 @@ class BacktestResult:
|
||||
print(f" {pair}:")
|
||||
for (
|
||||
symbol,
|
||||
action1,
|
||||
price1,
|
||||
action2,
|
||||
price2,
|
||||
trade1["side"],
|
||||
trade1["price"],
|
||||
trade2["side"],
|
||||
trade2["price"],
|
||||
trade_return,
|
||||
scaled_diseq1,
|
||||
scaled_diseq2,
|
||||
trade1["scaled_disequilibrium"],
|
||||
trade2["scaled_disequilibrium"],
|
||||
trade_num,
|
||||
) in pair_trades:
|
||||
disequil_info = ""
|
||||
if (
|
||||
scaled_diseq1 is not None
|
||||
and scaled_diseq2 is not None
|
||||
trade1["scaled_disequilibrium"] 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(
|
||||
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}%")
|
||||
day_return += pair_return
|
||||
|
||||
@ -35,8 +35,9 @@ class RollingFit(PairsTradingFitMethod):
|
||||
# Initialize trades DataFrame with proper dtypes to avoid concatenation warnings
|
||||
pair.user_data_["trades"] = pd.DataFrame(columns=self.TRADES_COLUMNS).astype({
|
||||
"time": "datetime64[ns]",
|
||||
"action": "string",
|
||||
"symbol": "string",
|
||||
"side": "string",
|
||||
"action": "string",
|
||||
"price": "float64",
|
||||
"disequilibrium": "float64",
|
||||
"scaled_disequilibrium": "float64",
|
||||
@ -136,7 +137,7 @@ class RollingFit(PairsTradingFitMethod):
|
||||
close_position_trades = self._get_close_trades(
|
||||
pair=pair,
|
||||
row=pred_row,
|
||||
close_threshold=close_threshold,
|
||||
close_threshold=close_threshold
|
||||
)
|
||||
if close_position_trades is not None:
|
||||
close_position_trades["status"] = PairState.CLOSE_POSITION.name
|
||||
@ -197,8 +198,9 @@ class RollingFit(PairsTradingFitMethod):
|
||||
trd_signal_tuples = [
|
||||
(
|
||||
open_tstamp,
|
||||
open_side_a,
|
||||
pair.symbol_a_,
|
||||
open_side_a,
|
||||
"OPEN",
|
||||
open_px_a,
|
||||
open_disequilibrium,
|
||||
open_scaled_disequilibrium,
|
||||
@ -206,8 +208,9 @@ class RollingFit(PairsTradingFitMethod):
|
||||
),
|
||||
(
|
||||
open_tstamp,
|
||||
open_side_b,
|
||||
pair.symbol_b_,
|
||||
open_side_b,
|
||||
"OPEN",
|
||||
open_px_b,
|
||||
open_disequilibrium,
|
||||
open_scaled_disequilibrium,
|
||||
@ -248,8 +251,9 @@ class RollingFit(PairsTradingFitMethod):
|
||||
trd_signal_tuples = [
|
||||
(
|
||||
close_tstamp,
|
||||
close_side_a,
|
||||
pair.symbol_a_,
|
||||
close_side_a,
|
||||
"CLOSE",
|
||||
close_px_a,
|
||||
close_disequilibrium,
|
||||
close_scaled_disequilibrium,
|
||||
@ -257,8 +261,9 @@ class RollingFit(PairsTradingFitMethod):
|
||||
),
|
||||
(
|
||||
close_tstamp,
|
||||
close_side_b,
|
||||
pair.symbol_b_,
|
||||
close_side_b,
|
||||
"CLOSE",
|
||||
close_px_b,
|
||||
close_disequilibrium,
|
||||
close_scaled_disequilibrium,
|
||||
|
||||
@ -94,10 +94,6 @@ class VECMRollingFit(RollingFit):
|
||||
def __init__(self) -> None:
|
||||
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(
|
||||
self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str
|
||||
) -> TradingPair:
|
||||
|
||||
@ -20,19 +20,18 @@ class ZScoreTradingPair(TradingPair):
|
||||
|
||||
def _fit_zscore(self) -> None:
|
||||
assert self.training_df_ is not None
|
||||
a = self.training_df_[self.colnames()].iloc[:, 0]
|
||||
b = self.training_df_[self.colnames()].iloc[:, 1]
|
||||
symbol_a_px_series = self.training_df_[self.colnames()].iloc[:, 0]
|
||||
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(b)
|
||||
self.zscore_model_ = sm.OLS(a, X).fit()
|
||||
X = sm.add_constant(symbol_b_px_series)
|
||||
self.zscore_model_ = sm.OLS(symbol_a_px_series, X).fit()
|
||||
assert self.zscore_model_ is not None
|
||||
hedge_ratio = self.zscore_model_.params.iloc[1]
|
||||
|
||||
# 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()
|
||||
|
||||
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)
|
||||
# Reset index to ensure proper indexing
|
||||
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):
|
||||
def __init__(self) -> None:
|
||||
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(
|
||||
self, config: Dict, market_data: pd.DataFrame, symbol_a: str, symbol_b: str, price_column: str
|
||||
) -> TradingPair:
|
||||
|
||||
@ -72,13 +72,17 @@ def run_backtest(
|
||||
bt_result: BacktestResult = BacktestResult(config=config)
|
||||
|
||||
pairs_trades = []
|
||||
for pair in create_pairs(datafile=datafile, 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
|
||||
)
|
||||
for pair in create_pairs(
|
||||
datafile=datafile,
|
||||
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:
|
||||
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
|
||||
if len(pairs_trades) == 0:
|
||||
print("No trading signals found for any pairs")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user