From a35320219863940bf9c0a780a1ea1f227bc9424a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:37:17 +0000 Subject: [PATCH 1/3] Initial plan From 88887e000e7473a6e596716b4b04385d7d16c504 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:41:25 +0000 Subject: [PATCH 2/3] Add Pine and Python strategy implementations --- README.md | 52 ++- .../test_netrade_dashboard.cpython-312.pyc | Bin 0 -> 25507 bytes pine/aapl_ema_rsi_trend_strategy.pine | 105 +++++ test_netrade_dashboard.py | 400 ++++++++++++++++++ 4 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 __pycache__/test_netrade_dashboard.cpython-312.pyc create mode 100644 pine/aapl_ema_rsi_trend_strategy.pine create mode 100644 test_netrade_dashboard.py diff --git a/README.md b/README.md index b5826e1..66358a6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ # Institutional-Microstructure- -My liberty of Code to my Scripts 🗽 + +Comprehensive, back-testable trading strategy implementation for: + +- TradingView Pine Script (`/pine/aapl_ema_rsi_trend_strategy.pine`) +- Python backtesting framework (`/test_netrade_dashboard.py`) + +## Strategy Summary + +- **Symbol:** AAPL +- **Timeframe:** Daily bars +- **Backtest window:** 2015-01-01 to 2024-12-31 +- **Entry:** EMA(20) cross above EMA(50), RSI in [40, 70], close > SMA(200), volume filter, ATR/close filter +- **Exit (first trigger):** 8% trailing stop, EMA cross down, 12% hard stop, 15% profit target +- **Risk sizing:** 2% equity risked per trade with hard stop distance of 12% +- **Slippage:** 0.02% per fill, commissions = 0 + +## Python Usage + +Run a backtest: + +```bash +python /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py --symbol AAPL --start 2015-01-01 --end 2024-12-31 +``` + +Run unit tests: + +```bash +python -m unittest /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py -v +``` + +Optional optimization mode: + +```bash +python /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py --optimize +``` + +Offline mode with local CSV: + +```bash +python /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py --csv /absolute/path/to/aapl_daily.csv +``` + +## Data Handling Assumptions + +- Yahoo Finance daily OHLCV through `yfinance` with `auto_adjust=True` for split/dividend-adjusted prices. +- Business-day reindexing and forward-fill for non-trading gaps. +- Slippage of 0.02% applied on entry and exit fills. + +## Risk Disclaimer + +This repository provides educational and research backtests only. Historical simulations do **not** guarantee future performance. Validate data quality, assumptions, execution constraints, and risk controls before any live deployment. diff --git a/__pycache__/test_netrade_dashboard.cpython-312.pyc b/__pycache__/test_netrade_dashboard.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3af22715bd9f0897d0d5bf01706a091522e0624b GIT binary patch literal 25507 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 rsiMin and rsiVal <= rsiMax +trendFilter = close > smaTrend +volumeFilter = volume >= volMult * volSma +volatilityFilter = atrRatio >= atrMin and atrRatio <= atrMax + +longCondition = longCross and rsiFilter and trendFilter and volumeFilter and volatilityFilter and strategy.position_size <= 0 + +// 2% risk position sizing using 12% hard stop +riskCapital = strategy.equity * riskPct +stopDistance = close * hardStopPct +qtyRaw = stopDistance > 0 ? riskCapital / stopDistance : 0.0 +qty = math.max(1, math.floor(qtyRaw)) + +if longCondition + strategy.entry("Long", strategy.long, qty=qty, alert_message="ENTRY_LONG_AAPL") + +// ===== Exit Logic ===== +var float highSinceEntry = na +if strategy.position_size > 0 + highSinceEntry := na(highSinceEntry) ? close : math.max(highSinceEntry, close) +else + highSinceEntry := na + +entryPrice = strategy.position_avg_price +trailStop = highSinceEntry * (1 - trailPct) +hardStop = entryPrice * (1 - hardStopPct) +profitTarget = entryPrice * (1 + profitTargetPct) + +exitTrail = strategy.position_size > 0 and close < trailStop +exitCross = strategy.position_size > 0 and ta.crossunder(emaFast, emaSlow) +exitHard = strategy.position_size > 0 and close < hardStop +exitProfit = strategy.position_size > 0 and close > profitTarget + +if exitTrail + strategy.close("Long", comment="trail_stop", alert_message="EXIT_TRAIL_AAPL") +else if exitCross + strategy.close("Long", comment="ema_cross_down", alert_message="EXIT_CROSS_AAPL") +else if exitHard + strategy.close("Long", comment="hard_stop", alert_message="EXIT_HARD_AAPL") +else if exitProfit + strategy.close("Long", comment="profit_target", alert_message="EXIT_PROFIT_AAPL") + +// ===== Alerts ===== +alertcondition(longCondition, title="Long Entry", message="AAPL strategy long entry") +alertcondition(exitTrail or exitCross or exitHard or exitProfit, title="Long Exit", message="AAPL strategy long exit") + +// ===== Visuals ===== +plot(emaFast, "EMA 20", color=color.new(color.teal, 0), linewidth=2) +plot(emaSlow, "EMA 50", color=color.new(color.orange, 0), linewidth=2) +plot(smaTrend, "SMA 200", color=color.new(color.blue, 0), linewidth=2) +plotshape(longCondition, title="Entry", style=shape.triangleup, color=color.new(color.green, 0), location=location.belowbar, size=size.tiny) +plotshape(exitTrail or exitCross or exitHard or exitProfit, title="Exit", style=shape.triangledown, color=color.new(color.red, 0), location=location.abovebar, size=size.tiny) + +// RSI and ATR ratio are plotted for visibility (separate pane recommended via "Move to") +plot(rsiVal, "RSI", color=color.new(color.purple, 0), display=display.none) +plot(atrRatio, "ATR/Close", color=color.new(color.fuchsia, 0), display=display.none) + +// ===== Performance Table ===== +var table perf = table.new(position.top_right, 2, 7, border_width=1) +if barstate.islast + table.cell(perf, 0, 0, "Metric", bgcolor=color.new(color.gray, 70)) + table.cell(perf, 1, 0, "Value", bgcolor=color.new(color.gray, 70)) + table.cell(perf, 0, 1, "Net Profit") + table.cell(perf, 1, 1, str.tostring(strategy.netprofit, format.mintick)) + table.cell(perf, 0, 2, "Closed Trades") + table.cell(perf, 1, 2, str.tostring(strategy.closedtrades)) + table.cell(perf, 0, 3, "Win Rate %") + table.cell(perf, 1, 3, str.tostring(strategy.closedtrades > 0 ? (strategy.wintrades / strategy.closedtrades) * 100 : 0, "#.##")) + table.cell(perf, 0, 4, "Profit Factor") + table.cell(perf, 1, 4, str.tostring(strategy.grossloss != 0 ? strategy.grossprofit / math.abs(strategy.grossloss) : na, "#.##")) + table.cell(perf, 0, 5, "Max Drawdown") + table.cell(perf, 1, 5, str.tostring(strategy.max_drawdown, format.mintick)) + table.cell(perf, 0, 6, "Equity") + table.cell(perf, 1, 6, str.tostring(strategy.equity, format.mintick)) diff --git a/test_netrade_dashboard.py b/test_netrade_dashboard.py new file mode 100644 index 0000000..2b78553 --- /dev/null +++ b/test_netrade_dashboard.py @@ -0,0 +1,400 @@ +from __future__ import annotations + +import argparse +import itertools +import math +import unittest +from dataclasses import dataclass +from typing import Dict, Iterable, List, Optional, Tuple + +import numpy as np +import pandas as pd + + +try: + import matplotlib.pyplot as plt +except Exception: # pragma: no cover - plotting is optional in test environments + plt = None + + +try: + import yfinance as yf +except Exception: # pragma: no cover - data fetch can be mocked in tests + yf = None + + +@dataclass +class StrategyParams: + ema_fast: int = 20 + ema_slow: int = 50 + sma_trend: int = 200 + rsi_len: int = 14 + rsi_min: float = 40.0 + rsi_max: float = 70.0 + volume_sma: int = 20 + volume_mult: float = 0.8 + atr_len: int = 14 + atr_min: float = 0.01 + atr_max: float = 0.04 + trail_pct: float = 0.08 + hard_stop_pct: float = 0.12 + profit_target_pct: float = 0.15 + risk_pct: float = 0.02 + slippage_pct: float = 0.0002 # 0.02% + + +def _rsi(series: pd.Series, period: int) -> pd.Series: + delta = series.diff() + gain = delta.clip(lower=0.0) + loss = -delta.clip(upper=0.0) + avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean() + avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean() + rs = avg_gain / avg_loss.replace(0, np.nan) + return 100 - (100 / (1 + rs)) + + +def _atr(df: pd.DataFrame, period: int) -> pd.Series: + prev_close = df["Close"].shift(1) + tr = pd.concat( + [ + (df["High"] - df["Low"]).abs(), + (df["High"] - prev_close).abs(), + (df["Low"] - prev_close).abs(), + ], + axis=1, + ).max(axis=1) + return tr.ewm(alpha=1 / period, adjust=False).mean() + + +def fetch_ohlcv(symbol: str, start: str, end: str) -> pd.DataFrame: + if yf is None: + raise ImportError("yfinance is required for live data fetch. Install with `pip install yfinance`.") + data = yf.download(symbol, start=start, end=end, auto_adjust=True, actions=True, progress=False) + if data.empty: + raise ValueError(f"No data returned for {symbol} between {start} and {end}.") + if isinstance(data.columns, pd.MultiIndex): + data.columns = data.columns.get_level_values(0) + required = {"Open", "High", "Low", "Close", "Volume"} + if not required.issubset(set(data.columns)): + raise ValueError(f"Missing required columns. Found {list(data.columns)}") + data = data.sort_index().copy() + business_idx = pd.date_range(data.index.min(), data.index.max(), freq="B") + data = data.reindex(business_idx) + ohlc = ["Open", "High", "Low", "Close"] + data[ohlc] = data[ohlc].ffill() + data["Volume"] = data["Volume"].fillna(0) + return data + + +def load_csv_ohlcv(path: str) -> pd.DataFrame: + df = pd.read_csv(path, parse_dates=True, index_col=0).sort_index() + required = {"Open", "High", "Low", "Close", "Volume"} + if not required.issubset(set(df.columns)): + raise ValueError(f"CSV must include columns: {sorted(required)}") + return df + + +def add_indicators(df: pd.DataFrame, p: StrategyParams) -> pd.DataFrame: + out = df.copy() + out["ema_fast"] = out["Close"].ewm(span=p.ema_fast, adjust=False).mean() + out["ema_slow"] = out["Close"].ewm(span=p.ema_slow, adjust=False).mean() + out["sma_trend"] = out["Close"].rolling(p.sma_trend).mean() + out["rsi"] = _rsi(out["Close"], p.rsi_len) + out["atr"] = _atr(out, p.atr_len) + out["vol_sma"] = out["Volume"].rolling(p.volume_sma).mean() + out["atr_ratio"] = out["atr"] / out["Close"] + return out + + +def _cross_up(a: pd.Series, b: pd.Series) -> pd.Series: + return (a > b) & (a.shift(1) <= b.shift(1)) + + +def _cross_down(a: pd.Series, b: pd.Series) -> pd.Series: + return (a < b) & (a.shift(1) >= b.shift(1)) + + +def generate_signals(df: pd.DataFrame, p: StrategyParams) -> pd.DataFrame: + out = df.copy() + out["entry_signal"] = ( + _cross_up(out["ema_fast"], out["ema_slow"]) + & out["rsi"].between(p.rsi_min, p.rsi_max, inclusive="both") + & (out["Close"] > out["sma_trend"]) + & (out["Volume"] >= p.volume_mult * out["vol_sma"]) + & out["atr_ratio"].between(p.atr_min, p.atr_max, inclusive="both") + ) + out["exit_cross"] = _cross_down(out["ema_fast"], out["ema_slow"]) + return out + + +def _max_drawdown(equity: pd.Series) -> float: + roll_max = equity.cummax() + dd = equity / roll_max - 1.0 + return float(dd.min()) + + +def _annualized_sharpe(returns: pd.Series) -> float: + if returns.std(ddof=0) == 0: + return 0.0 + return float((returns.mean() / returns.std(ddof=0)) * np.sqrt(252)) + + +def _cagr(equity: pd.Series) -> float: + if equity.empty or len(equity) < 2 or equity.iloc[0] <= 0: + return 0.0 + years = (equity.index[-1] - equity.index[0]).days / 365.25 + if years <= 0: + return 0.0 + return float((equity.iloc[-1] / equity.iloc[0]) ** (1 / years) - 1) + + +def backtest(df: pd.DataFrame, p: StrategyParams, initial_capital: float = 100_000) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[str, float]]: + data = generate_signals(add_indicators(df, p), p).dropna().copy() + if data.empty: + raise ValueError("Not enough data after indicator warm-up.") + + equity = initial_capital + shares = 0 + entry_price = np.nan + highest_close = np.nan + trades: List[Dict[str, object]] = [] + equity_curve: List[Dict[str, object]] = [] + + for dt, row in data.iterrows(): + close = float(row["Close"]) + mark_to_market = equity + shares * close + + if shares == 0 and bool(row["entry_signal"]): + stop_distance = close * p.hard_stop_pct + risk_budget = mark_to_market * p.risk_pct + qty_risk = math.floor(risk_budget / stop_distance) if stop_distance > 0 else 0 + qty_afford = math.floor(mark_to_market / (close * (1 + p.slippage_pct))) if close > 0 else 0 + qty = int(max(0, min(qty_risk, qty_afford))) + if qty > 0: + fill = close * (1 + p.slippage_pct) + shares = qty + entry_price = fill + highest_close = close + equity -= qty * fill + trades.append({"entry_date": dt, "entry_price": fill, "shares": qty}) + + elif shares > 0: + highest_close = max(highest_close, close) + trail_stop = highest_close * (1 - p.trail_pct) + hard_stop = entry_price * (1 - p.hard_stop_pct) + profit_target = entry_price * (1 + p.profit_target_pct) + + exit_reason = None + if close < trail_stop: + exit_reason = "trailing_stop" + elif bool(row["exit_cross"]): + exit_reason = "ema_cross_down" + elif close < hard_stop: + exit_reason = "hard_stop" + elif close > profit_target: + exit_reason = "profit_target" + + if exit_reason is not None: + fill = close * (1 - p.slippage_pct) + equity += shares * fill + trade = trades[-1] + trade["exit_date"] = dt + trade["exit_price"] = fill + trade["exit_reason"] = exit_reason + pnl = (fill - float(trade["entry_price"])) * int(trade["shares"]) + trade["pnl"] = pnl + shares = 0 + entry_price = np.nan + highest_close = np.nan + + total_equity = equity + shares * close + equity_curve.append({"date": dt, "equity": total_equity, "close": close, "position": shares}) + + if shares > 0: + close = float(data.iloc[-1]["Close"]) + fill = close * (1 - p.slippage_pct) + equity += shares * fill + trade = trades[-1] + trade["exit_date"] = data.index[-1] + trade["exit_price"] = fill + trade["exit_reason"] = "end_of_test" + trade["pnl"] = (fill - float(trade["entry_price"])) * int(trade["shares"]) + shares = 0 + equity_curve[-1]["equity"] = equity + equity_curve[-1]["position"] = 0 + + eq_df = pd.DataFrame(equity_curve).set_index("date") + trades_df = pd.DataFrame(trades) + if not trades_df.empty and "pnl" not in trades_df: + trades_df["pnl"] = np.nan + + closed = trades_df.dropna(subset=["pnl"]).copy() if not trades_df.empty else pd.DataFrame(columns=["pnl"]) + gross_profit = float(closed.loc[closed["pnl"] > 0, "pnl"].sum()) if not closed.empty else 0.0 + gross_loss = float(closed.loc[closed["pnl"] < 0, "pnl"].sum()) if not closed.empty else 0.0 + returns = eq_df["equity"].pct_change().fillna(0.0) + win_rate = float((closed["pnl"] > 0).mean()) if not closed.empty else 0.0 + + merged = data.join(eq_df[["equity"]], how="left") + merged["equity"] = merged["equity"].ffill().fillna(initial_capital) + merged["drawdown"] = merged["equity"] / merged["equity"].cummax() - 1.0 + + metrics = { + "trades": int(len(closed)), + "win_rate": win_rate, + "profit_factor": float(gross_profit / abs(gross_loss)) if gross_loss < 0 else float("inf") if gross_profit > 0 else 0.0, + "max_drawdown": _max_drawdown(merged["equity"]), + "sharpe": _annualized_sharpe(returns), + "cagr": _cagr(merged["equity"]), + "total_return": float(merged["equity"].iloc[-1] / merged["equity"].iloc[0] - 1.0), + } + return merged, trades_df, metrics + + +def plot_results(data: pd.DataFrame, trades: pd.DataFrame, metrics: Dict[str, float], title: str) -> None: + if plt is None: + print("matplotlib is not available; skipping plots.") + return + fig, (ax_price, ax_equity, ax_dd) = plt.subplots(3, 1, figsize=(14, 11), sharex=True, gridspec_kw={"height_ratios": [2, 1, 1]}) + ax_price.plot(data.index, data["Close"], label="Close", color="black", linewidth=1.0) + ax_price.plot(data.index, data["ema_fast"], label="EMA Fast", color="teal", linewidth=1.0) + ax_price.plot(data.index, data["ema_slow"], label="EMA Slow", color="orange", linewidth=1.0) + ax_price.plot(data.index, data["sma_trend"], label="SMA Trend", color="blue", linewidth=1.0) + + if not trades.empty: + entries = trades.dropna(subset=["entry_date"]) + exits = trades.dropna(subset=["exit_date"]) + ax_price.scatter(entries["entry_date"], entries["entry_price"], marker="^", color="green", s=35, label="Entry") + ax_price.scatter(exits["exit_date"], exits["exit_price"], marker="v", color="red", s=35, label="Exit") + ax_price.legend(loc="upper left") + ax_price.set_title(title) + + eq = data["equity"] + ax_equity.plot(eq.index, eq.values, color="navy", label="Equity") + ax_equity.legend(loc="upper left") + + dd = eq / eq.cummax() - 1.0 + ax_dd.fill_between(dd.index, dd.values, 0, color="firebrick", alpha=0.4) + ax_dd.set_ylabel("Drawdown") + + perf_text = ( + f"Trades: {metrics['trades']}\n" + f"Win rate: {metrics['win_rate']:.2%}\n" + f"Profit factor: {metrics['profit_factor']:.2f}\n" + f"Max DD: {metrics['max_drawdown']:.2%}\n" + f"Sharpe: {metrics['sharpe']:.2f}\n" + f"CAGR: {metrics['cagr']:.2%}" + ) + ax_price.text(0.99, 0.02, perf_text, transform=ax_price.transAxes, va="bottom", ha="right", bbox={"boxstyle": "round", "fc": "white", "alpha": 0.8}) + plt.tight_layout() + plt.show() + + +def optimize_parameters(df: pd.DataFrame, base: StrategyParams, grid: Dict[str, Iterable]) -> pd.DataFrame: + keys = list(grid.keys()) + values = [list(grid[k]) for k in keys] + rows = [] + for combo in itertools.product(*values): + params = StrategyParams(**{**base.__dict__, **dict(zip(keys, combo))}) + _, _, m = backtest(df, params) + row = {k: v for k, v in zip(keys, combo)} + row.update(m) + rows.append(row) + out = pd.DataFrame(rows).sort_values(["profit_factor", "sharpe"], ascending=False) + return out + + +def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="AAPL daily strategy backtest dashboard") + parser.add_argument("--symbol", default="AAPL") + parser.add_argument("--start", default="2015-01-01") + parser.add_argument("--end", default="2024-12-31") + parser.add_argument("--initial-capital", type=float, default=100_000) + parser.add_argument("--plot", action="store_true", help="Render equity/drawdown charts") + parser.add_argument("--optimize", action="store_true", help="Run small parameter grid search") + parser.add_argument("--csv", help="Path to local OHLCV CSV with Open/High/Low/Close/Volume columns") + return parser.parse_args(argv) + + +def run_cli(args: argparse.Namespace) -> int: + if args.csv: + df = load_csv_ohlcv(args.csv) + else: + try: + df = fetch_ohlcv(args.symbol, args.start, args.end) + except Exception as exc: + raise RuntimeError( + f"Data fetch failed for {args.symbol}. Use --csv for offline backtests. Original error: {exc}" + ) from exc + params = StrategyParams() + data, trades, metrics = backtest(df, params, initial_capital=args.initial_capital) + + print(f"Symbol: {args.symbol} | Timeframe: Daily | Period: {args.start} -> {args.end}") + for k in ["trades", "win_rate", "profit_factor", "max_drawdown", "sharpe", "cagr", "total_return"]: + v = metrics[k] + if k in {"win_rate", "max_drawdown", "cagr", "total_return"}: + print(f"{k:>14}: {v:.2%}") + else: + print(f"{k:>14}: {v:.4f}" if isinstance(v, float) else f"{k:>14}: {v}") + + if args.optimize: + grid = {"rsi_min": [35.0, 40.0, 45.0], "rsi_max": [65.0, 70.0], "trail_pct": [0.06, 0.08, 0.1]} + optim = optimize_parameters(df, params, grid) + print("\nTop optimization rows:") + print(optim.head(10).to_string(index=False)) + + if args.plot: + plot_results(data, trades, metrics, title=f"{args.symbol} Strategy Backtest") + return 0 + + +class StrategySignalTests(unittest.TestCase): + def setUp(self) -> None: + idx = pd.date_range("2020-01-01", periods=320, freq="B") + close = pd.Series(np.linspace(100, 140, len(idx)), index=idx) + close.iloc[250:260] += np.linspace(-8, 14, 10) + close.iloc[260:280] += 8 + self.df = pd.DataFrame( + { + "Open": close * 0.999, + "High": close * 1.005, + "Low": close * 0.995, + "Close": close, + "Volume": 1_000_000, + }, + index=idx, + ) + + def test_indicator_columns_present(self) -> None: + out = add_indicators(self.df, StrategyParams()) + for col in ["ema_fast", "ema_slow", "sma_trend", "rsi", "atr", "vol_sma", "atr_ratio"]: + self.assertIn(col, out.columns) + + def test_signal_generation_has_expected_fields(self) -> None: + out = generate_signals(add_indicators(self.df, StrategyParams()), StrategyParams()) + self.assertIn("entry_signal", out.columns) + self.assertIn("exit_cross", out.columns) + self.assertTrue(out["entry_signal"].dtype == bool) + + def test_backtest_metrics_keys(self) -> None: + _, trades, metrics = backtest(self.df, StrategyParams()) + self.assertIn("profit_factor", metrics) + self.assertIn("max_drawdown", metrics) + self.assertIn("sharpe", metrics) + self.assertIn("trades", metrics) + self.assertIsInstance(trades, pd.DataFrame) + + def test_hard_stop_position_sizing_not_overlevered(self) -> None: + p = StrategyParams(hard_stop_pct=0.12, risk_pct=0.02) + data = generate_signals(add_indicators(self.df, p), p).dropna() + row = data.iloc[0] + close = float(row["Close"]) + equity = 100_000 + stop_distance = close * p.hard_stop_pct + qty_risk = math.floor((equity * p.risk_pct) / stop_distance) + qty_afford = math.floor(equity / (close * (1 + p.slippage_pct))) + qty = min(qty_risk, qty_afford) + self.assertLessEqual(qty * close, equity) + + +if __name__ == "__main__": + raise SystemExit(run_cli(parse_args())) From 1cf0752c5bfb8a2b828f52091d78ef9abd3b16c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:45:47 +0000 Subject: [PATCH 3/3] Finalize comprehensive Pine and Python strategy delivery --- README.md | 12 ++--- .../test_netrade_dashboard.cpython-312.pyc | Bin 25507 -> 26234 bytes pine/aapl_ema_rsi_trend_strategy.pine | 14 ++++-- test_netrade_dashboard.py | 44 ++++++++++++------ 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 66358a6..f6795bf 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Comprehensive, back-testable trading strategy implementation for: - **Symbol:** AAPL - **Timeframe:** Daily bars -- **Backtest window:** 2015-01-01 to 2024-12-31 +- **Backtest window (example):** 2015-01-01 to 2024-12-31 - **Entry:** EMA(20) cross above EMA(50), RSI in [40, 70], close > SMA(200), volume filter, ATR/close filter - **Exit (first trigger):** 8% trailing stop, EMA cross down, 12% hard stop, 15% profit target - **Risk sizing:** 2% equity risked per trade with hard stop distance of 12% @@ -20,31 +20,31 @@ Comprehensive, back-testable trading strategy implementation for: Run a backtest: ```bash -python /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py --symbol AAPL --start 2015-01-01 --end 2024-12-31 +python test_netrade_dashboard.py --symbol AAPL --start 2015-01-01 --end 2024-12-31 ``` Run unit tests: ```bash -python -m unittest /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py -v +python -m unittest test_netrade_dashboard.py -v ``` Optional optimization mode: ```bash -python /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py --optimize +python test_netrade_dashboard.py --optimize ``` Offline mode with local CSV: ```bash -python /home/runner/work/Institutional-Microstructure-/Institutional-Microstructure-/test_netrade_dashboard.py --csv /absolute/path/to/aapl_daily.csv +python test_netrade_dashboard.py --csv /absolute/path/to/aapl_daily.csv ``` ## Data Handling Assumptions - Yahoo Finance daily OHLCV through `yfinance` with `auto_adjust=True` for split/dividend-adjusted prices. -- Business-day reindexing and forward-fill for non-trading gaps. +- Business-day reindexing and forward-fill for non-trading gaps (weekday approximation; market holidays are not explicitly calendared). - Slippage of 0.02% applied on entry and exit fills. ## Risk Disclaimer diff --git a/__pycache__/test_netrade_dashboard.cpython-312.pyc b/__pycache__/test_netrade_dashboard.cpython-312.pyc index 3af22715bd9f0897d0d5bf01706a091522e0624b..e6fc16c5904c2dc46be59227b0889960c94be397 100644 GIT binary patch delta 7268 zcmaJm33OCdmjBnjSCYz3mdZ{MvH$@g5X?qGkUasUY%G52{UjA?1{`@r%P1E5{-hzeN;?Kn!7P#OAkg+!(e; z9DYZ{>32q4epkfpcSlnEDZEY>PW7jOye{nVr$cE9XGAjnnWA9DUUcECNVY#`mYV0! zlPx--T_EOr1!6g=!zl>D$z&n;$MSmxVtrC1He&D90qy|Y`MBO+K#GX#UW31o6caa; zUQ$9*pe!P#Bo)eHQby9CEFtB@17#_c=}?vdKLg5gQb97ItRVAA7L@aW&W6%Qd}IO1 zx!34lKq^V@LSR*rg=8L-3rQ8JCV5a*MQTVsaH{#t3t;Ajva$b8cDBgl)hDO!uay@O z@1FUBKs`D^UJO5wma9&(vyQCresNe=BFMR=0yV;}>HIl6g+q@G3Pbvx!l3x0nmnj^ zVy4DddlkF)iJD!CMq{d^hGJ1=qfe)qB!z^6s%9mUDh0!mqJWTLv!wR1KZ!mI63wmB z@MMK9rYlN?J~VeAa6=z~0QCTuW+2F7XAPdQLPWg?iVzedC`C|?paOvp!2$p$glhnW z>%BF7v4~tl2cl7#)(pnzo|=xRqK4D~oT(J9+7JrTn4;2wpgKV1s{g0ffYt2JhFqHq zM?sLsN{kbkwKJqo-wP)u*Sq zK+`+~rR;2W&g9p|R9(sh06? z>tlnspVClBX`{EWm9~=92B@a10r(7>j`YwL7PIBg^em~A-VSvQ>~Y(~Jl^xPmH<-# z!w>|Z#cR2fLHnStSjDa7-;5bdI8IJC7&RvJ{XW)I>S2F&y4=nu#Zgm2pD^Wse19b< zU~U?ctAI@CiHozVn7B2a{lsOryOR7)!RKbwbvkt;h|vuIG{dI29Hr~oN_SNmuC0g4 zdwm8f%?MB~x*o}H0KP0X?rs&gun*jlsSCLdc1udR$&ctAESOT4$9L7D5;s^`$tF|Yo&SX=~RrgB$i}kOpesT4MB`xoi zzFmINvxPmI+9B4mZ&P!smy3e%sE`m8y9!wHBjU+ve7~s9Zfim~DY9cl#TAN6HP4Fb z62ej8nC^xKxcB(6^Bo!Pgh2GfFkzhDZ1&#v40haUsU8*kz>hs8j_MM+es{9LjZj5< z;$K4SPeU)j3Ny@X%3*b9-7rzJn!#R8D_d^aIFi%Sx!oHFEL0 z>82w972Id9*Gm_%xF@~o08jyB-w)t!;Ul~IiYt36r|yDl>6B&Z6}x-f@L>Ii_Pq1< zyo>gNE3U$+;+3acPcNJDHeYbHOj%kO^K27qC$D-o=*CdT+Q6Y1l(-b7D}bW7y>s44 z(}SgIdNe9jMa{e~77i#8NwWegK+z@o9CR{c+G;Wj~`Sp(?#l)ylo9szog9nP-d&FLxzG<_JsBLIHX%0AtS#0I1E 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 A4*&oF delta 6491 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 diff --git a/pine/aapl_ema_rsi_trend_strategy.pine b/pine/aapl_ema_rsi_trend_strategy.pine index ffa5b9a..2025327 100644 --- a/pine/aapl_ema_rsi_trend_strategy.pine +++ b/pine/aapl_ema_rsi_trend_strategy.pine @@ -3,7 +3,8 @@ strategy("AAPL EMA/RSI Trend Strategy", overlay=true, pyramiding=0, initial_capi // ===== Inputs ===== emaFastLen = input.int(20, "EMA Fast Length", minval=1) -emaSlowLen = input.int(50, "EMA Slow Length", minval=2) +emaSlowLenInput = input.int(50, "EMA Slow Length", minval=2) +emaSlowLen = math.max(emaSlowLenInput, emaFastLen + 1) smaTrendLen = input.int(200, "SMA Trend Length", minval=10) rsiLen = input.int(14, "RSI Length", minval=2) rsiMin = input.float(40.0, "RSI Min", step=0.1) @@ -40,9 +41,9 @@ longCondition = longCross and rsiFilter and trendFilter and volumeFilter and vol riskCapital = strategy.equity * riskPct stopDistance = close * hardStopPct qtyRaw = stopDistance > 0 ? riskCapital / stopDistance : 0.0 -qty = math.max(1, math.floor(qtyRaw)) +qty = math.max(0, math.floor(qtyRaw)) -if longCondition +if longCondition and qty > 0 strategy.entry("Long", strategy.long, qty=qty, alert_message="ENTRY_LONG_AAPL") // ===== Exit Logic ===== @@ -89,6 +90,11 @@ plot(atrRatio, "ATR/Close", color=color.new(color.fuchsia, 0), display=display.n // ===== Performance Table ===== var table perf = table.new(position.top_right, 2, 7, border_width=1) if barstate.islast + profitFactorText = "0" + if strategy.grossloss != 0 + profitFactorText := str.tostring(strategy.grossprofit / math.abs(strategy.grossloss), "#.##") + else if strategy.grossprofit > 0 + profitFactorText := "inf" table.cell(perf, 0, 0, "Metric", bgcolor=color.new(color.gray, 70)) table.cell(perf, 1, 0, "Value", bgcolor=color.new(color.gray, 70)) table.cell(perf, 0, 1, "Net Profit") @@ -98,7 +104,7 @@ if barstate.islast table.cell(perf, 0, 3, "Win Rate %") table.cell(perf, 1, 3, str.tostring(strategy.closedtrades > 0 ? (strategy.wintrades / strategy.closedtrades) * 100 : 0, "#.##")) table.cell(perf, 0, 4, "Profit Factor") - table.cell(perf, 1, 4, str.tostring(strategy.grossloss != 0 ? strategy.grossprofit / math.abs(strategy.grossloss) : na, "#.##")) + table.cell(perf, 1, 4, profitFactorText) table.cell(perf, 0, 5, "Max Drawdown") table.cell(perf, 1, 5, str.tostring(strategy.max_drawdown, format.mintick)) table.cell(perf, 0, 6, "Equity") diff --git a/test_netrade_dashboard.py b/test_netrade_dashboard.py index 2b78553..36a41ce 100644 --- a/test_netrade_dashboard.py +++ b/test_netrade_dashboard.py @@ -4,22 +4,24 @@ import itertools import math import unittest -from dataclasses import dataclass +from dataclasses import asdict, dataclass +from pathlib import Path from typing import Dict, Iterable, List, Optional, Tuple import numpy as np import pandas as pd +from pandas.tseries.offsets import BDay try: import matplotlib.pyplot as plt -except Exception: # pragma: no cover - plotting is optional in test environments +except ImportError: # pragma: no cover - plotting is optional in test environments plt = None try: import yfinance as yf -except Exception: # pragma: no cover - data fetch can be mocked in tests +except ImportError: # pragma: no cover - data fetch can be mocked in tests yf = None @@ -43,6 +45,9 @@ class StrategyParams: slippage_pct: float = 0.0002 # 0.02% +OPTIMIZATION_GRID = {"rsi_min": [35.0, 40.0, 45.0], "rsi_max": [65.0, 70.0], "trail_pct": [0.06, 0.08, 0.1]} + + def _rsi(series: pd.Series, period: int) -> pd.Series: delta = series.diff() gain = delta.clip(lower=0.0) @@ -78,7 +83,8 @@ def fetch_ohlcv(symbol: str, start: str, end: str) -> pd.DataFrame: if not required.issubset(set(data.columns)): raise ValueError(f"Missing required columns. Found {list(data.columns)}") data = data.sort_index().copy() - business_idx = pd.date_range(data.index.min(), data.index.max(), freq="B") + # Mon-Fri business-day approximation; exchange-specific holidays are not explicitly modeled. + business_idx = pd.date_range(data.index.min(), data.index.max(), freq=BDay()) data = data.reindex(business_idx) ohlc = ["Open", "High", "Low", "Close"] data[ohlc] = data[ohlc].ffill() @@ -87,6 +93,10 @@ def fetch_ohlcv(symbol: str, start: str, end: str) -> pd.DataFrame: def load_csv_ohlcv(path: str) -> pd.DataFrame: + if not path: + raise ValueError("CSV path cannot be empty.") + if not Path(path).exists(): + raise FileNotFoundError(f"CSV path does not exist: {path}") df = pd.read_csv(path, parse_dates=True, index_col=0).sort_index() required = {"Open", "High", "Low", "Close", "Volume"} if not required.issubset(set(df.columns)): @@ -225,8 +235,6 @@ def backtest(df: pd.DataFrame, p: StrategyParams, initial_capital: float = 100_0 eq_df = pd.DataFrame(equity_curve).set_index("date") trades_df = pd.DataFrame(trades) - if not trades_df.empty and "pnl" not in trades_df: - trades_df["pnl"] = np.nan closed = trades_df.dropna(subset=["pnl"]).copy() if not trades_df.empty else pd.DataFrame(columns=["pnl"]) gross_profit = float(closed.loc[closed["pnl"] > 0, "pnl"].sum()) if not closed.empty else 0.0 @@ -238,10 +246,17 @@ def backtest(df: pd.DataFrame, p: StrategyParams, initial_capital: float = 100_0 merged["equity"] = merged["equity"].ffill().fillna(initial_capital) merged["drawdown"] = merged["equity"] / merged["equity"].cummax() - 1.0 + if gross_loss < 0: + profit_factor = float(gross_profit / abs(gross_loss)) + elif gross_profit > 0: + profit_factor = float("inf") + else: + profit_factor = 0.0 + metrics = { "trades": int(len(closed)), "win_rate": win_rate, - "profit_factor": float(gross_profit / abs(gross_loss)) if gross_loss < 0 else float("inf") if gross_profit > 0 else 0.0, + "profit_factor": profit_factor, "max_drawdown": _max_drawdown(merged["equity"]), "sharpe": _annualized_sharpe(returns), "cagr": _cagr(merged["equity"]), @@ -276,10 +291,11 @@ def plot_results(data: pd.DataFrame, trades: pd.DataFrame, metrics: Dict[str, fl ax_dd.fill_between(dd.index, dd.values, 0, color="firebrick", alpha=0.4) ax_dd.set_ylabel("Drawdown") + profit_factor_text = "inf (no losses)" if not np.isfinite(metrics["profit_factor"]) else f"{metrics['profit_factor']:.2f}" perf_text = ( f"Trades: {metrics['trades']}\n" f"Win rate: {metrics['win_rate']:.2%}\n" - f"Profit factor: {metrics['profit_factor']:.2f}\n" + f"Profit factor: {profit_factor_text}\n" f"Max DD: {metrics['max_drawdown']:.2%}\n" f"Sharpe: {metrics['sharpe']:.2f}\n" f"CAGR: {metrics['cagr']:.2%}" @@ -294,7 +310,7 @@ def optimize_parameters(df: pd.DataFrame, base: StrategyParams, grid: Dict[str, values = [list(grid[k]) for k in keys] rows = [] for combo in itertools.product(*values): - params = StrategyParams(**{**base.__dict__, **dict(zip(keys, combo))}) + params = StrategyParams(**{**asdict(base), **dict(zip(keys, combo))}) _, _, m = backtest(df, params) row = {k: v for k, v in zip(keys, combo)} row.update(m) @@ -304,11 +320,11 @@ def optimize_parameters(df: pd.DataFrame, base: StrategyParams, grid: Dict[str, def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: - parser = argparse.ArgumentParser(description="AAPL daily strategy backtest dashboard") + parser = argparse.ArgumentParser(description="Trading strategy backtest dashboard") parser.add_argument("--symbol", default="AAPL") parser.add_argument("--start", default="2015-01-01") parser.add_argument("--end", default="2024-12-31") - parser.add_argument("--initial-capital", type=float, default=100_000) + parser.add_argument("--initial-capital", dest="initial_capital", type=float, default=100_000) parser.add_argument("--plot", action="store_true", help="Render equity/drawdown charts") parser.add_argument("--optimize", action="store_true", help="Run small parameter grid search") parser.add_argument("--csv", help="Path to local OHLCV CSV with Open/High/Low/Close/Volume columns") @@ -331,14 +347,16 @@ def run_cli(args: argparse.Namespace) -> int: print(f"Symbol: {args.symbol} | Timeframe: Daily | Period: {args.start} -> {args.end}") for k in ["trades", "win_rate", "profit_factor", "max_drawdown", "sharpe", "cagr", "total_return"]: v = metrics[k] + if isinstance(v, float) and not np.isfinite(v): + print(f"{k:>14}: inf (no losing trades)") + continue if k in {"win_rate", "max_drawdown", "cagr", "total_return"}: print(f"{k:>14}: {v:.2%}") else: print(f"{k:>14}: {v:.4f}" if isinstance(v, float) else f"{k:>14}: {v}") if args.optimize: - grid = {"rsi_min": [35.0, 40.0, 45.0], "rsi_max": [65.0, 70.0], "trail_pct": [0.06, 0.08, 0.1]} - optim = optimize_parameters(df, params, grid) + optim = optimize_parameters(df, params, OPTIMIZATION_GRID) print("\nTop optimization rows:") print(optim.head(10).to_string(index=False))