maneshtrader/web_core/analytics.py

166 lines
5.4 KiB
Python

from __future__ import annotations
import pandas as pd
from .constants import TREND_BEAR, TREND_BULL
def _trade_return_pct(direction: str, entry_price: float, exit_price: float) -> float:
if direction == "LONG":
return ((exit_price - entry_price) / entry_price) * 100.0
return ((entry_price - exit_price) / entry_price) * 100.0
def backtest_signals(
df: pd.DataFrame,
slippage_bps: float = 0.0,
fee_bps: float = 0.0,
stop_loss_pct: float | None = None,
take_profit_pct: float | None = None,
max_hold_bars: int = 1,
min_hold_bars: int = 1,
) -> dict[str, float | int]:
if len(df) < 4:
return {"trades": 0, "wins": 0, "losses": 0, "win_rate": 0.0, "avg_pnl_pct": 0.0}
trend_series = df["trend_state"]
trend_change = trend_series != trend_series.shift(1)
signal_idx = df.index[trend_change & trend_series.isin([TREND_BULL, TREND_BEAR])]
wins = 0
losses = 0
pnl_values: list[float] = []
max_hold = max(1, int(max_hold_bars))
min_hold = max(1, min(int(min_hold_bars), max_hold))
total_cost_pct = ((float(slippage_bps) + float(fee_bps)) * 2.0) / 100.0
for idx in signal_idx:
pos = df.index.get_loc(idx)
if pos + 1 >= len(df):
continue
entry_close = float(df.iloc[pos]["Close"])
signal_trend = df.iloc[pos]["trend_state"]
direction = "LONG" if signal_trend == TREND_BULL else "SHORT"
exit_price = float(df.iloc[min(pos + max_hold, len(df) - 1)]["Close"])
for hold in range(min_hold, max_hold + 1):
exit_pos = min(pos + hold, len(df) - 1)
candidate_exit = float(df.iloc[exit_pos]["Close"])
raw_pnl = _trade_return_pct(direction, entry_close, candidate_exit)
if stop_loss_pct is not None and raw_pnl <= -float(stop_loss_pct):
exit_price = candidate_exit
break
if take_profit_pct is not None and raw_pnl >= float(take_profit_pct):
exit_price = candidate_exit
break
exit_price = candidate_exit
pnl_pct = _trade_return_pct(direction, entry_close, exit_price) - total_cost_pct
pnl_values.append(pnl_pct)
wins += int(pnl_pct > 0)
losses += int(pnl_pct <= 0)
trades = wins + losses
win_rate = (wins / trades * 100.0) if trades else 0.0
avg_pnl_pct = (sum(pnl_values) / len(pnl_values)) if pnl_values else 0.0
return {
"trades": trades,
"wins": wins,
"losses": losses,
"win_rate": round(win_rate, 2),
"avg_pnl_pct": round(avg_pnl_pct, 2),
}
def simulate_trend_trades(df: pd.DataFrame, max_examples: int = 20) -> pd.DataFrame:
if len(df) < 4:
return pd.DataFrame(
columns=[
"direction",
"entry_timestamp",
"exit_timestamp",
"entry_price",
"exit_price",
"bars_held",
"pnl_pct",
"outcome",
]
)
trend_series = df["trend_state"]
trend_change = trend_series != trend_series.shift(1)
signal_idx = df.index[trend_change & trend_series.isin([TREND_BULL, TREND_BEAR])]
direction_by_trend = {
TREND_BULL: "LONG",
TREND_BEAR: "SHORT",
}
open_trade: dict[str, object] | None = None
closed_trades: list[dict[str, object]] = []
for idx in signal_idx:
pos = int(df.index.get_loc(idx))
trend_now = str(df.iloc[pos]["trend_state"])
direction = direction_by_trend[trend_now]
close_price = float(df.iloc[pos]["Close"])
if open_trade is None:
open_trade = {
"direction": direction,
"entry_timestamp": idx,
"entry_pos": pos,
"entry_price": close_price,
}
continue
if open_trade["direction"] == direction:
continue
entry_price = float(open_trade["entry_price"])
if str(open_trade["direction"]) == "LONG":
pnl_pct = ((close_price - entry_price) / entry_price) * 100.0
else:
pnl_pct = ((entry_price - close_price) / entry_price) * 100.0
closed_trades.append(
{
"direction": str(open_trade["direction"]),
"entry_timestamp": open_trade["entry_timestamp"],
"exit_timestamp": idx,
"entry_price": round(entry_price, 4),
"exit_price": round(close_price, 4),
"bars_held": pos - int(open_trade["entry_pos"]),
"pnl_pct": round(pnl_pct, 2),
"outcome": "Win" if pnl_pct > 0 else ("Loss" if pnl_pct < 0 else "Flat"),
}
)
# Flip into the new direction at the same confirmed reversal bar.
open_trade = {
"direction": direction,
"entry_timestamp": idx,
"entry_pos": pos,
"entry_price": close_price,
}
if not closed_trades:
return pd.DataFrame(
columns=[
"direction",
"entry_timestamp",
"exit_timestamp",
"entry_price",
"exit_price",
"bars_held",
"pnl_pct",
"outcome",
]
)
trades_df = pd.DataFrame(closed_trades)
if max_examples > 0 and len(trades_df) > max_examples:
trades_df = trades_df.iloc[-max_examples:].copy()
return trades_df