agentsclimarketplace

Backtesting sim

Skill mahmoud20138/Tradecraft/plugins/tradecraft/skills/backtesting-sim

102 Claude Code skills across 7 categories -- trading strategies, Azure, VSCode extensions, AI prompts, and custom automation skills

Install
npx -y skills add mahmoud20138/Tradecraft --skill backtesting-sim

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Backtesting and simulation: vectorized backtesting, paper trading simulation, strategy A/B testing, automated strategy building, natural language to strategy, and trading plan generation. USE FOR: backtest, backtesting, paper trading, simulation, strategy builder, A/B test strategies, natural language strategy, trading plan, equity curve, drawdown analysis, walk-forward, Monte Carlo simulation, performance metrics, Sharpe, Sortino, Calmar, win rate, profit factor, expectancy, strategy validation, overfitting prevention, survivorship bias, look-ahead bias.

SKILL.md

45.2 KB, as published. Nobody here has run it

Skill: Backtesting Sim | Domain: trading | Category: quantitative | Level: intermediate Tags: trading, quant, backtesting, simulation, paper-trading, vectorized

Vectorized Backtester

Vectorized Backtester

import pandas as pd
import numpy as np
from typing import Callable, Optional

# ── Shared helpers ──────────────────────────────────────────────────────────

def _sharpe(r: np.ndarray, rfr: float = 0.04 / 252) -> float:
    std = r.std(ddof=1)
    return float((r.mean() - rfr) / std * np.sqrt(252)) if std > 0 else 0.0

def _sortino(r: np.ndarray, rfr: float = 0.04 / 252) -> float:
    down = r[r < rfr]
    std_dn = down.std(ddof=1) if len(down) > 1 else 0.0
    return float((r.mean() - rfr) / std_dn * np.sqrt(252)) if std_dn > 0 else 0.0

def _calmar(r: np.ndarray) -> float:
    eq      = np.cumprod(1 + r)
    peak    = np.maximum.accumulate(eq)
    max_dd  = abs(((eq - peak) / peak).min())
    ann_ret = eq[-1] ** (252 / len(r)) - 1
    return float(ann_ret / max_dd) if max_dd > 0 else 0.0

def _profit_factor(r: np.ndarray) -> float:
    gains  = r[r > 0].sum()
    losses = abs(r[r < 0].sum())
    return float(gains / losses) if losses > 0 else float("inf")


class VectorizedBacktester:
    """
    Production-quality vectorised backtester with full performance metrics.

    Signals must be pre-shifted (no look-ahead bias).
    Supports transaction costs, position sizing, and walk-forward validation.

    Example
    -------
    >>> df["signal"] = np.sign(df["close"].pct_change(5))  # 5-bar momentum
    >>> result = VectorizedBacktester.backtest(df, spread_bps=2.0)
    >>> print(result["sharpe"], result["max_drawdown_pct"])
    1.34  -12.5
    """

    @staticmethod
    def backtest(
        df: pd.DataFrame,
        signal_col: str = "signal",
        spread_bps: float = 2.0,
        slippage_bps: float = 1.0,
        initial_capital: float = 10_000.0,
        position_size: float = 1.0,
        risk_free_annual: float = 0.04,
    ) -> dict:
        """
        Vectorised backtest engine.

        Parameters
        ----------
        df              : OHLCV DataFrame with a signal column
        signal_col      : column name for signal (1=long, -1=short, 0=flat)
        spread_bps      : round-trip spread cost in basis points
        slippage_bps    : round-trip slippage estimate in basis points
        initial_capital : starting capital
        position_size   : fraction of capital at risk (1.0 = fully invested)
        risk_free_annual: used for Sharpe / Sortino calculations

        Returns
        -------
        Comprehensive dict including Sharpe, Sortino, Calmar, max DD,
        win rate, profit factor, equity curve, and full annotated DataFrame.

        Notes
        -----
        ALL signals are shifted by 1 bar to prevent look-ahead.
        Costs are charged on position changes (not every bar).
        """
        if signal_col not in df.columns:
            raise KeyError(f"Column '{signal_col}' not found in DataFrame")
        if len(df) < 10:
            raise ValueError("Need at least 10 bars to backtest")

        out = df.copy()
        cost_rt = (spread_bps + slippage_bps) / 10_000.0   # Round-trip cost fraction

        out["returns"]      = out["close"].pct_change()
        out["position"]     = out[signal_col].shift(1).fillna(0) * position_size
        out["trade"]        = out["position"].diff().abs().fillna(0)
        out["gross_return"] = out["position"] * out["returns"]
        out["cost"]         = out["trade"] * cost_rt
        out["net_return"]   = out["gross_return"] - out["cost"]

        # Equity curve (multiplicative)
        out["equity"]       = initial_capital * (1 + out["net_return"]).cumprod()
        out["peak"]         = out["equity"].cummax()
        out["drawdown"]     = (out["equity"] - out["peak"]) / out["peak"]

        # Trade-level stats
        out["trade_entry"]  = (out["position"] != 0) & (out["position"].shift(1) == 0)
        out["trade_exit"]   = (out["position"] == 0) & (out["position"].shift(1) != 0)
        n_trades            = int(out["trade_entry"].sum())

        net     = out["net_return"].dropna().values
        rfr_d   = risk_free_annual / 252

        # Compute comprehensive metrics from net returns
        total_return = float((out["equity"].iloc[-1] / initial_capital - 1) * 100)
        max_dd       = float(out["drawdown"].min() * 100)
        sharpe       = _sharpe(net, rfr_d)
        sortino      = _sortino(net, rfr_d)
        calmar       = _calmar(net)
        pf           = _profit_factor(net)

        # Win rate on closed trades (more accurate than bar-level)
        exit_returns = out.loc[out["trade_exit"], "net_return"]
        win_rate     = float((exit_returns > 0).mean() * 100) if len(exit_returns) > 0 else 0.0

        # Consecutive loss streak
        signs = np.sign(net)
        streak = 0
        max_streak = 0
        for s in signs:
            if s < 0:
                streak += 1
                max_streak = max(max_streak, streak)
            else:
                streak = 0

        return {
            "total_return_pct":   round(total_return, 2),
            "ann_return_pct":     round(float((out["equity"].iloc[-1] / initial_capital)
                                              ** (252 / max(len(net), 1)) - 1) * 100, 2),
            "sharpe":             round(sharpe, 3),
            "sortino":            round(sortino, 3),
            "calmar":             round(calmar, 3),
            "max_drawdown_pct":   round(max_dd, 2),
            "profit_factor":      round(pf, 3),
            "n_trades":           n_trades,
            "win_rate":           round(win_rate, 1),
            "max_consec_losses":  int(max_streak),
            "total_costs_pct":    round(float(out["cost"].sum() * 100), 2),
            "equity_curve":       out["equity"],
            "drawdown_series":    out["drawdown"],
            "df":                 out,
        }

    @staticmethod
    def walk_forward_backtest(
        df: pd.DataFrame,
        signal_fn: Callable,
        optimize_fn: Callable,
        train_bars: int = 500,
        test_bars: int = 100,
        min_folds: int = 3,
        **kwargs,
    ) -> dict:
        """
        Anchored walk-forward validation.

        Parameters
        ----------
        signal_fn   : callable(df, params) → signal Series
        optimize_fn : callable(train_df) → params dict
        train_bars  : in-sample training window
        test_bars   : out-of-sample test window per fold
        min_folds   : minimum folds required for a valid result

        Returns
        -------
        dict with per-fold and aggregate OOS statistics.
        """
        results:    list[dict] = []
        all_equity: list[pd.Series] = []

        for start in range(0, len(df) - train_bars - test_bars, test_bars):
            train = df.iloc[start: start + train_bars]
            test  = df.iloc[start + train_bars: start + train_bars + test_bars].copy()

            try:
                params          = optimize_fn(train)
                test["signal"]  = signal_fn(test, params)
                bt              = VectorizedBacktester.backtest(test, **kwargs)
                results.append({
                    "fold":    len(results),
                    "sharpe":  bt["sharpe"],
                    "sortino": bt["sortino"],
                    "return":  bt["total_return_pct"],
                    "max_dd":  bt["max_drawdown_pct"],
                    "params":  params,
                })
                all_equity.append(bt["equity_curve"])
            except Exception as e:
                results.append({"fold": len(results), "error": str(e)})

        valid = [r for r in results if "sharpe" in r]
        if len(valid) < min_folds:
            return {
                "method":  "walk_forward",
                "error":   f"Only {len(valid)} valid folds (need {min_folds})",
                "n_folds": len(results),
            }

        sharpes  = [r["sharpe"]  for r in valid]
        sortinos = [r["sortino"] for r in valid]
        returns  = [r["return"]  for r in valid]

        # Concatenate OOS equity curves for a continuous equity line
        oos_equity = pd.concat(all_equity).sort_index() if all_equity else pd.Series(dtype=float)

        return {
            "method":            "walk_forward",
            "n_folds":           len(results),
            "n_valid_folds":     len(valid),
            "avg_sharpe":        round(float(np.mean(sharpes)), 3),
            "median_sharpe":     round(float(np.median(sharpes)), 3),
            "std_sharpe":        round(float(np.std(sharpes, ddof=1)), 3),
            "pct_folds_positive": round(float(np.mean([r > 0 for r in returns])) * 100, 1),
            "avg_return":        round(float(np.mean(returns)), 2),
            "avg_sortino":       round(float(np.mean(sortinos)), 3),
            "fold_results":      valid,
            "oos_equity":        oos_equity,
            "WARNING":           "Past performance ≠ future results. OOS validation required.",
        }

    @staticmethod
    def compare_strategies(
        df: pd.DataFrame,
        strategies: dict[str, pd.Series],
        spread_bps: float = 2.0,
        slippage_bps: float = 1.0,
        initial_capital: float = 10_000.0,
    ) -> pd.DataFrame:
        """
        Run multiple strategies on the same data and compare side-by-side.

        Parameters
        ----------
        strategies : {"name": signal_series, ...}

        Returns
        -------
        DataFrame ranked by Sharpe ratio.
        """
        results: list[dict] = []
        for name, signal_series in strategies.items():
            df_copy = df.copy()
            df_copy["signal"] = signal_series
            try:
                bt = VectorizedBacktester.backtest(
                    df_copy,
                    spread_bps=spread_bps,
                    slippage_bps=slippage_bps,
                    initial_capital=initial_capital,
                )
                results.append({
                    "strategy":     name,
                    "return":       bt["total_return_pct"],
                    "sharpe":       bt["sharpe"],
                    "sortino":      bt["sortino"],
                    "calmar":       bt["calmar"],
                    "max_dd":       bt["max_drawdown_pct"],
                    "n_trades":     bt["n_trades"],
                    "win_rate":     bt["win_rate"],
                    "profit_factor": bt["profit_factor"],
                })
            except Exception as e:
                results.append({"strategy": name, "error": str(e)})

        return pd.DataFrame(results).sort_values("sharpe", ascending=False)

Trade Simulator Paper

Trade Simulator Paper

import pandas as pd
import numpy as np
from datetime import datetime
from typing import Optional

class PaperTradeSimulator:
    """
    Realistic paper trading simulator with proper spread/slippage modelling,
    margin tracking, and full trade history for post-session analysis.

    Uses a reproducible RNG (np.random.default_rng) for slippage simulation.

    Example
    -------
    >>> sim = PaperTradeSimulator(initial_balance=10_000, seed=42)
    >>> sim.open_trade("EURUSD", "buy", lots=0.1, price=1.0850, sl=1.0810, tp=1.0920)
    >>> closed = sim.check_positions({"EURUSD": 1.0920})
    >>> print(closed[0]["reason"], closed[0]["pnl_usd"])
    'TP'  70.0
    """

    PIP_VALUES: dict[str, float] = {
        "default":  10.0,       # USD per pip per standard lot
        "USDJPY":   9.09,       # approximate (varies with rate)
        "XAUUSD":   10.0,
    }

    def __init__(
        self,
        initial_balance: float = 10_000.0,
        leverage: float = 30.0,
        pip_factor: float = 10_000.0,
        seed: int = 42,
    ):
        """
        Parameters
        ----------
        initial_balance : starting account balance in USD
        leverage        : maximum leverage (used for margin check)
        pip_factor      : price-to-pip conversion (10000 for most FX, 100 for JPY pairs)
        seed            : RNG seed for reproducible slippage
        """
        if initial_balance <= 0:
            raise ValueError("initial_balance must be positive")
        self.balance         = initial_balance
        self.equity          = initial_balance
        self.leverage        = leverage
        self.pip_factor      = pip_factor
        self._rng            = np.random.default_rng(seed)
        self.positions:  list[dict] = []
        self.history:    list[dict] = []
        self._trade_id   = 0

    # ── Position management ────────────────────────────────────────────────

    def open_trade(
        self,
        symbol: str,
        direction: str,
        lots: float,
        price: float,
        sl: float,
        tp: float,
        spread: float = 0.0002,
        max_slippage: float = 0.00005,
        comment: str = "",
    ) -> dict:
        """
        Open a simulated position with spread and random slippage applied.

        Parameters
        ----------
        symbol       : instrument (e.g. 'EURUSD')
        direction    : 'buy' or 'sell'
        lots         : position size in standard lots
        price        : requested entry price
        sl           : stop-loss price
        tp           : take-profit price
        spread       : half-spread to apply (price units, not pips)
        max_slippage : maximum slippage magnitude (price units)
        comment      : free-text label for this trade

        Returns
        -------
        dict with status, actual_entry, slippage_pips, and trade metadata.
        """
        direction = direction.lower()
        if direction not in ("buy", "sell"):
            raise ValueError(f"direction must be 'buy' or 'sell', got '{direction}'")
        if lots <= 0:
            raise ValueError("lots must be positive")
        if sl <= 0 or tp <= 0:
            raise ValueError("sl and tp must be positive price levels")

        slippage = float(self._rng.uniform(0, max_slippage))
        if direction == "buy":
            actual_entry = price + spread + slippage
        else:
            actual_entry = price - spread - slippage

        # Margin check
        required_margin = actual_entry * lots * 100_000 / self.leverage
        if required_margin > self.equity:
            return {"status": "REJECTED", "reason": "Insufficient margin",
                    "required_margin": round(required_margin, 2), "equity": round(self.equity, 2)}

        self._trade_id += 1
        pos = {
            "id":          self._trade_id,
            "symbol":      symbol,
            "direction":   direction,
            "lots":        lots,
            "entry":       round(actual_entry, 5),
            "requested":   round(price, 5),
            "sl":          round(sl, 5),
            "tp":          round(tp, 5),
            "spread_pips": round(spread * self.pip_factor, 1),
            "slippage_pips": round(slippage * self.pip_factor, 1),
            "open_time":   datetime.utcnow().isoformat(),
            "comment":     comment,
        }
        self.positions.append(pos)
        return {"status": "OPENED", **pos}

    def check_positions(self, current_prices: dict[str, float]) -> list[dict]:
        """
        Check all open positions against current prices and close those
        that have hit SL or TP.

        Parameters
        ----------
        current_prices : {symbol: bid_price} — use bid for sells, ask for buys
                         (simplified: pass mid-price for paper trading)

        Returns
        -------
        list of closed trade dicts (empty if nothing closed this tick).
        """
        closed: list[dict] = []
        remaining: list[dict] = []

        for pos in self.positions:
            price = current_prices.get(pos["symbol"])
            if price is None:
                remaining.append(pos)
                continue

            hit_sl = hit_tp = False
            if pos["direction"] == "buy":
                hit_sl = price <= pos["sl"]
                hit_tp = price >= pos["tp"]
            else:
                hit_sl = price >= pos["sl"]
                hit_tp = price <= pos["tp"]

            if hit_sl or hit_tp:
                exit_price  = pos["sl"] if hit_sl else pos["tp"]
                reason      = "SL" if hit_sl else "TP"
                pip_val     = self.PIP_VALUES.get(pos["symbol"], self.PIP_VALUES["default"])
                if pos["direction"] == "buy":
                    pnl_pips = (exit_price - pos["entry"]) * self.pip_factor
                else:
                    pnl_pips = (pos["entry"] - exit_price) * self.pip_factor
                pnl_usd = pnl_pips * pip_val * pos["lots"]

                self.balance += pnl_usd
                self.equity   = self.balance   # simplified (no floating P&L)

                trade_result = {
                    **pos,
                    "exit":       round(exit_price, 5),
                    "reason":     reason,
                    "pnl_pips":   round(pnl_pips, 1),
                    "pnl_usd":    round(pnl_usd, 2),
                    "close_time": datetime.utcnow().isoformat(),
                }
                closed.append(trade_result)
            else:
                remaining.append(pos)

        self.positions = remaining
        self.history.extend(closed)
        return closed

    def close_all(self, current_prices: dict[str, float]) -> list[dict]:
        """Force-close all open positions at current prices (market close)."""
        for pos in self.positions:
            price = current_prices.get(pos["symbol"])
            if price:
                current_prices[pos["symbol"]] = pos["sl"] if pos["direction"] == "buy" else pos["tp"]
        return self.check_positions(current_prices)

    # ── Analytics ──────────────────────────────────────────────────────────

    def account_summary(self) -> dict:
        """Return current account state and session statistics."""
        wins   = [t for t in self.history if t.get("pnl_usd", 0) > 0]
        losses = [t for t in self.history if t.get("pnl_usd", 0) < 0]
        total_pnl = sum(t.get("pnl_usd", 0) for t in self.history)
        return {
            "balance":       round(self.balance, 2),
            "equity":        round(self.equity, 2),
            "open_trades":   len(self.positions),
            "closed_trades": len(self.history),
            "win_rate":      round(len(wins) / max(len(self.history), 1) * 100, 1),
            "total_pnl":     round(total_pnl, 2),
            "avg_win_usd":   round(float(np.mean([t["pnl_usd"] for t in wins])), 2)   if wins   else 0.0,
            "avg_loss_usd":  round(float(np.mean([t["pnl_usd"] for t in losses])), 2) if losses else 0.0,
            "profit_factor": round(
                sum(t["pnl_usd"] for t in wins) / max(abs(sum(t["pnl_usd"] for t in losses)), 0.01), 3
            ) if losses else float("inf"),
        }

Strategy Ab Tester

Strategy A/B Tester

import pandas as pd
import numpy as np
from scipy import stats

class StrategyABTester:
    """
    Head-to-head strategy comparison with parametric and non-parametric
    significance tests, plus rolling performance analysis.

    Example
    -------
    >>> result = StrategyABTester.compare(returns_a, returns_b)
    >>> print(result["winner"], result["statistically_significant"])
    'Strategy A'  True
    """

    @staticmethod
    def _metrics(r: pd.Series, name: str, rfr: float = 0.04 / 252) -> dict:
        """Compute a full metric set for one strategy."""
        r    = r.dropna()
        std  = float(r.std(ddof=1))
        equity = (1 + r).cumprod()
        max_dd = float(((equity / equity.cummax()) - 1).min())

        sharpe  = float((r.mean() - rfr) / std * np.sqrt(252)) if std > 0 else 0.0
        # Sortino: penalise downside vol only
        dn      = r[r < rfr]
        dn_std  = float(dn.std(ddof=1)) if len(dn) > 1 else 0.0
        sortino = float((r.mean() - rfr) / dn_std * np.sqrt(252)) if dn_std > 0 else 0.0
        # Calmar: annualised return / max drawdown
        ann_ret = float(equity.iloc[-1] ** (252 / max(len(r), 1)) - 1)
        calmar  = float(ann_ret / abs(max_dd)) if max_dd < 0 else 0.0

        return {
            "name":           name,
            "total_return":   round(float((equity.iloc[-1] - 1) * 100), 2),
            "ann_return":     round(ann_ret * 100, 2),
            "sharpe":         round(sharpe, 3),
            "sortino":        round(sortino, 3),
            "calmar":         round(calmar, 3),
            "max_drawdown":   round(max_dd * 100, 2),
            "volatility_ann": round(std * np.sqrt(252) * 100, 2),
            "win_rate":       round(float((r > 0).mean() * 100), 1),
            "skewness":       round(float(r.skew()), 3),
            "kurtosis":       round(float(r.kurtosis()), 3),
            "n_periods":      len(r),
        }

    @staticmethod
    def compare(
        returns_a: pd.Series,
        returns_b: pd.Series,
        name_a: str = "Strategy A",
        name_b: str = "Strategy B",
        risk_free: float = 0.04 / 252,
        alpha: float = 0.05,
    ) -> dict:
        """
        Comprehensive head-to-head comparison with four significance tests.

        Parameters
        ----------
        returns_a / b : daily return Series for each strategy
        alpha         : significance level (default 5 %)

        Returns
        -------
        dict with metrics for both strategies, statistical test results,
             winner, and actionable recommendation.
        """
        m_a = StrategyABTester._metrics(returns_a, name_a, risk_free)
        m_b = StrategyABTester._metrics(returns_b, name_b, risk_free)

        n   = min(len(returns_a.dropna()), len(returns_b.dropna()))
        ra  = returns_a.dropna().values[:n]
        rb  = returns_b.dropna().values[:n]

        # 1. Paired t-test
        t_stat, t_pval = stats.ttest_rel(ra, rb)

        # 2. Wilcoxon signed-rank (non-parametric)
        try:
            w_stat, w_pval = stats.wilcoxon(ra, rb)
        except Exception:
            w_stat, w_pval = 0.0, 1.0

        # 3. Kolmogorov-Smirnov (distribution difference)
        ks_stat, ks_pval = stats.ks_2samp(ra, rb)

        # 4. Jobson-Korkie Sharpe difference test
        sharpe_diff = m_a["sharpe"] - m_b["sharpe"]
        corr        = float(pd.Series(ra).corr(pd.Series(rb)))
        se_diff     = np.sqrt(
            (2 / n) * (1 - corr + 0.5 * m_a["sharpe"]**2 + 0.5 * m_b["sharpe"]**2)
        )
        jk_z    = sharpe_diff / max(se_diff, 1e-10)
        jk_pval = float(2 * (1 - stats.norm.cdf(abs(jk_z))))

        # Determine winner and significance (require at least t-test significant)
        better      = name_a if m_a["sharpe"] > m_b["sharpe"] else name_b
        significant = float(t_pval) < alpha

        return {
            "strategy_a":               m_a,
            "strategy_b":               m_b,
            "winner":                   better,
            "sharpe_advantage":         round(abs(sharpe_diff), 3),
            "statistically_significant": significant,
            "n_shared_periods":         n,
            "return_correlation":       round(corr, 3),
            "tests": {
                "paired_ttest":      {"t_stat": round(float(t_stat), 3),  "p_value": round(float(t_pval), 4),  "significant": float(t_pval) < alpha},
                "wilcoxon":          {"w_stat": round(float(w_stat), 3),  "p_value": round(float(w_pval), 4),  "significant": float(w_pval) < alpha},
                "ks_test":           {"ks_stat": round(float(ks_stat), 3), "p_value": round(float(ks_pval), 4), "significant": float(ks_pval) < alpha},
                "sharpe_difference": {"z_stat": round(jk_z, 3),          "p_value": round(jk_pval, 4),         "significant": jk_pval < alpha},
            },
            "verdict": (
                f"{better} is SIGNIFICANTLY better (t-test p={t_pval:.4f}). Confidence is high."
                if significant else
                f"{better} leads but NOT significantly (p={t_pval:.4f}). Difference may be noise."
            ),
            "recommendation": (
                f"Deploy {better}."
                if significant else
                "Run both over a longer sample or run walk-forward validation before deciding."
            ),
        }

    @staticmethod
    def rolling_comparison(
        returns_a: pd.Series,
        returns_b: pd.Series,
        window: int = 60,
    ) -> pd.DataFrame:
        """
        Rolling Sharpe comparison to identify regime-dependent leadership.

        Returns
        -------
        DataFrame with columns: sharpe_a, sharpe_b, difference, a_leads (bool),
                                 rolling_alpha (A return - B return, annualised %).
        """
        def _rolling_sharpe(r: pd.Series) -> pd.Series:
            mu  = r.rolling(window).mean()
            std = r.rolling(window).std(ddof=1)
            return (mu / std.replace(0, np.nan)) * np.sqrt(252)

        sa = _rolling_sharpe(returns_a).rename("sharpe_a")
        sb = _rolling_sharpe(returns_b).rename("sharpe_b")

        # Rolling alpha: annualised outperformance of A over B
        rolling_alpha = (
            (returns_a - returns_b).rolling(window).mean() * 252 * 100
        ).rename("rolling_alpha_pct")

        return pd.DataFrame({
            "sharpe_a":          sa,
            "sharpe_b":          sb,
            "difference":        sa - sb,
            "a_leads":           sa > sb,
            "rolling_alpha_pct": rolling_alpha,
        })

Automated Strategy Builder

Automated Strategy Builder

class AutoStrategyBuilder:
    """
    Parse natural-language strategy descriptions and generate executable Python signal code.

    Maps trigger phrases to pandas-based code snippets, extracts numeric parameters
    from context, infers trade direction, and outputs a ready-to-run signal function.

    Example
    -------
    >>> result = AutoStrategyBuilder.parse_description(
    ...     "Buy when RSI is below 30 and price crosses above the 50 EMA"
    ... )
    >>> print(result["direction"], result["can_auto_generate"])
    'long'  True
    >>> print(result["generated_code"])
    """

    CONDITION_MAP: dict[str, str] = {
        "rsi above":           "df['rsi_{p}'] > {value}",
        "rsi below":           "df['rsi_{p}'] < {value}",
        "price above sma":     "df['close'] > df['close'].rolling({value}).mean()",
        "price below sma":     "df['close'] < df['close'].rolling({value}).mean()",
        "price above ema":     "df['close'] > df['close'].ewm(span={value}).mean()",
        "price below ema":     "df['close'] < df['close'].ewm(span={value}).mean()",
        "macd cross up":       "(df['macd'] > df['signal']) & (df['macd'].shift(1) <= df['signal'].shift(1))",
        "macd cross down":     "(df['macd'] < df['signal']) & (df['macd'].shift(1) >= df['signal'].shift(1))",
        "bollinger lower":     "df['close'] < df['close'].rolling(20).mean() - 2 * df['close'].rolling(20).std()",
        "bollinger upper":     "df['close'] > df['close'].rolling(20).mean() + 2 * df['close'].rolling(20).std()",
        "volume above average": "df['volume'] > df['volume'].rolling(20).mean() * 1.5",
        "new high":            "df['close'] > df['close'].rolling({value}).max().shift(1)",
        "new low":             "df['close'] < df['close'].rolling({value}).min().shift(1)",
        "bullish engulfing":   "(df['close'] > df['open']) & (df['close'] > df['open'].shift(1)) & (df['open'] < df['close'].shift(1))",
        "bearish engulfing":   "(df['close'] < df['open']) & (df['close'] < df['open'].shift(1)) & (df['open'] > df['close'].shift(1))",
        "above cloud":         "df['close'] > df[['senkou_a','senkou_b']].max(axis=1)",
        "below cloud":         "df['close'] < df[['senkou_a','senkou_b']].min(axis=1)",
        "golden cross":        "(df['close'].rolling(50).mean() > df['close'].rolling(200).mean()) & (df['close'].rolling(50).mean().shift(1) <= df['close'].rolling(200).mean().shift(1))",
        "death cross":         "(df['close'].rolling(50).mean() < df['close'].rolling(200).mean()) & (df['close'].rolling(50).mean().shift(1) >= df['close'].rolling(200).mean().shift(1))",
        "atr breakout":        "df['high'] - df['low'] > {value} * df['atr']",
    }

    _LONG_WORDS  = {"buy", "long", "bullish", "uptrend", "above", "breakout up"}
    _SHORT_WORDS = {"sell", "short", "bearish", "downtrend", "below", "breakdown"}

    @staticmethod
    def _extract_number(text: str, default: int = 14) -> int:
        """Extract the first integer from a text string."""
        import re
        m = re.search(r"\b(\d{1,3})\b", text)
        return int(m.group(1)) if m else default

    @staticmethod
    def parse_description(text: str) -> dict:
        """
        Parse a natural-language strategy description into structured conditions
        and optionally generate executable Python signal code.

        Parameters
        ----------
        text : free-form strategy description

        Returns
        -------
        dict with parsed_conditions, direction, generated_code, and completeness assessment.
        """
        import re
        text_lower = text.lower()

        conditions: list[dict] = []
        for trigger, code_template in AutoStrategyBuilder.CONDITION_MAP.items():
            if trigger in text_lower:
                # Extract a numeric parameter from the surrounding context
                context = text_lower[max(text_lower.find(trigger) - 20, 0):
                                     text_lower.find(trigger) + 40]
                param = AutoStrategyBuilder._extract_number(context)
                code  = code_template.replace("{value}", str(param)).replace("{p}", str(param))
                conditions.append({
                    "natural":   trigger,
                    "code":      code,
                    "param":     param,
                })

        # Direction inference
        long_hits  = sum(w in text_lower for w in AutoStrategyBuilder._LONG_WORDS)
        short_hits = sum(w in text_lower for w in AutoStrategyBuilder._SHORT_WORDS)
        direction  = "long" if long_hits > short_hits else "short" if short_hits > long_hits else "unknown"

        # Generate executable signal code when conditions are available
        generated_code = ""
        if len(conditions) >= 1:
            signal_val = "1" if direction == "long" else "-1" if direction == "short" else "1"
            cond_lines = "\n    & ".join(f"({c['code']})" for c in conditions)
            generated_code = (
                "import numpy as np\n\n"
                "def generate_signal(df: pd.DataFrame) -> pd.Series:\n"
                f"    mask = (\n    {cond_lines}\n    )\n"
                f"    signal = pd.Series(0, index=df.index)\n"
                f"    signal[mask] = {signal_val}\n"
                "    return signal.shift(1).fillna(0)  # Shift to prevent look-ahead\n"
            )

        return {
            "parsed_conditions":  conditions,
            "direction":          direction,
            "n_conditions":       len(conditions),
            "can_auto_generate":  len(conditions) >= 2,
            "generated_code":     generated_code,
            "next_step": (
                "Generated code ready — add to a DataFrame and backtest with VectorizedBacktester."
                if len(conditions) >= 2 else
                "Add more specific conditions (e.g., 'RSI below 30', 'price above 50 EMA') for code generation."
            ),
        }

Natural Language To Strategy

Natural Language To Strategy

class NLStrategyParser:
    """
    Parse a free-text strategy description into structured entry, exit, risk,
    and filter rules — ready for review or downstream code generation.

    Example
    -------
    >>> result = NLStrategyParser.parse(
    ...     "Buy when RSI crosses above 30. Exit when RSI exceeds 70 or SL at 1%. "
    ...     "Only trade during London session. Avoid Fridays."
    ... )
    >>> print(result["completeness"])
    'COMPLETE'
    """

    _ENTRY_WORDS  = {"enter", "buy", "sell", "open", "go long", "go short", "long when", "short when"}
    _EXIT_WORDS   = {"exit", "close", "take profit", "stop loss", "stop-loss", "tp", "sl at"}
    _RISK_WORDS   = {"risk", "lot size", "position size", "max loss", "2r", "1r", "r:r", "risk reward"}
    _FILTER_WORDS = {"only", "filter", "avoid", "when", "must", "unless", "not on", "session", "news"}

    @staticmethod
    def parse(text: str) -> dict:
        """
        Parse strategy text into structured rule categories.

        Parameters
        ----------
        text : multi-sentence strategy description

        Returns
        -------
        dict with categorised rules, completeness score, and a gap analysis
             highlighting what is missing before the strategy can be coded.
        """
        import re
        rules: dict[str, list[str]] = {"entry": [], "exit": [], "risk": [], "filters": []}

        sentences = re.split(r"[.;!\n]", text)
        for s in sentences:
            s_stripped = s.strip()
            s_lower    = s_stripped.lower()
            if not s_lower:
                continue

            # Categorise by keyword presence (in order of specificity)
            if any(w in s_lower for w in NLStrategyParser._RISK_WORDS):
                rules["risk"].append(s_stripped)
            elif any(w in s_lower for w in NLStrategyParser._EXIT_WORDS):
                rules["exit"].append(s_stripped)
            elif any(w in s_lower for w in NLStrategyParser._ENTRY_WORDS):
                rules["entry"].append(s_stripped)
            elif any(w in s_lower for w in NLStrategyParser._FILTER_WORDS):
                rules["filters"].append(s_stripped)

        # Gap analysis
        gaps: list[str] = []
        if not rules["entry"]:   gaps.append("No entry rule — when do you enter?")
        if not rules["exit"]:    gaps.append("No exit rule — when do you close?")
        if not rules["risk"]:    gaps.append("No risk rule — what % do you risk per trade?")

        has_entry  = bool(rules["entry"])
        has_exit   = bool(rules["exit"])
        has_risk   = bool(rules["risk"])
        score      = sum([has_entry, has_exit, has_risk])

        completeness = (
            "COMPLETE"             if score == 3 else
            "MOSTLY COMPLETE"      if score == 2 else
            "INCOMPLETE"
        )

        return {
            "parsed_rules":   rules,
            "n_entry_rules":  len(rules["entry"]),
            "n_exit_rules":   len(rules["exit"]),
            "n_risk_rules":   len(rules["risk"]),
            "n_filters":      len(rules["filters"]),
            "completeness":   completeness,
            "completeness_score": score,
            "gaps":           gaps,
            "ready_to_code":  score == 3,
            "next_step": (
                "Strategy is well-defined. Use AutoStrategyBuilder to generate signal code."
                if score == 3 else
                f"Fill in missing rules: {'; '.join(gaps)}"
            ),
        }

Trading Plan Builder

Trading Plan Builder

from datetime import datetime, timedelta
from dataclasses import dataclass, field

@dataclass
class DailyTradingPlan:
    """Structured daily trading plan with session context, risk budget, and rules."""
    date:           str
    session_focus:  str
    watchlist:      list[dict] = field(default_factory=list)
    macro_context:  str = ""
    key_events:     list[str] = field(default_factory=list)
    bias:           dict = field(default_factory=dict)
    risk_budget:    dict = field(default_factory=dict)
    rules_today:    list[str] = field(default_factory=list)
    checklist:      dict = field(default_factory=dict)
    notes:          str = ""

    def to_markdown(self) -> str:
        """Export plan as a readable markdown string for journaling."""
        event_lines   = [f"- {e}" for e in self.key_events] or ["- None"]
        budget_lines  = [f"- **{k}:** {v}" for k, v in self.risk_budget.items()]
        rule_lines    = [f"- {r}" for r in self.rules_today]
        watch_lines   = [
            f"- {w['pair']} | Bias: {w.get('bias','?')} | Priority: {w.get('priority','?')} | Confluence: {w.get('confluence_pct', 0)}%"
            for w in self.watchlist
        ]
        lines = (
            [f"# Trading Plan \u2014 {self.date}",
             f"**Session:** {self.session_focus.upper()}",
             f"**Macro Context:** {self.macro_context or 'N/A'}",
             "",
             "## Key Events Today"]
            + event_lines
            + ["", "## Risk Budget"]
            + budget_lines
            + ["", "## Rules"]
            + rule_lines
            + ["", "## Watchlist"]
            + watch_lines
        )
        return "\n".join(lines)


class TradingPlanBuilder:
    """
    Build, structure, and export daily trading plans with pre-market checklists,
    watchlist generation, and end-of-day review templates.

    Example
    -------
    >>> plan = TradingPlanBuilder.build_plan()
    >>> print(plan.session_focus)
    'london'
    >>> md = plan.to_markdown()
    """

    SESSION_HOURS = {
        "tokyo":    (0, 7),
        "london":   (7, 13),
        "overlap":  (13, 16),
        "new_york": (13, 22),
        "off":      (22, 24),
    }

    @staticmethod
    def current_session(hour: Optional[int] = None) -> str:
        """Return the current or given-hour trading session name."""
        h = hour if hour is not None else datetime.utcnow().hour
        if 0 <= h < 7:   return "tokyo"
        if 7 <= h < 13:  return "london"
        if 13 <= h < 16: return "overlap"
        if 16 <= h < 22: return "new_york"
        return "off"

    @staticmethod
    def pre_market_checklist() -> dict:
        """Return a structured pre-market checklist with estimated times."""
        return {
            "1_macro_scan": {
                "task":  "Check DXY, VIX, yield curves, equity futures",
                "tools": ["macro-economic-dashboard"],
                "time":  "5 min",
                "why":   "Sets the risk-on/risk-off context for the day",
            },
            "2_news_check": {
                "task":  "Review upcoming high-impact events and overnight news",
                "tools": ["market-news-impact", "risk-calendar-trade-filter"],
                "time":  "5 min",
                "why":   "Avoid entering before news that can spike against position",
            },
            "3_htf_analysis": {
                "task":  "D1 and H4 analysis on watchlist pairs — set daily directional bias",
                "tools": ["mt5-chart-browser", "mtf-confluence-scorer"],
                "time":  "10 min",
                "why":   "Higher timeframe bias defines where the order flow is likely going",
            },
            "4_key_levels": {
                "task":  "Mark S/R, order blocks, FVGs, and liquidity pools on H1 charts",
                "tools": ["trendline-sr-vision", "liquidity-order-flow-mapper"],
                "time":  "10 min",
                "why":   "These are the decision points where institutional orders cluster",
            },
            "5_risk_budget": {
                "task":  "Set max daily loss, max trades, and check portfolio heat",
                "tools": ["risk-and-portfolio"],
                "time":  "2 min",
                "why":   "Pre-defined loss limits prevent emotional overtrading",
            },
            "6_session_check": {
                "task":  "Verify current session quality — is today suitable for trading?",
                "tools": ["session-profiler", "risk-calendar-trade-filter"],
                "time":  "2 min",
                "why":   "Thin liquidity sessions (e.g., bank holidays) produce false signals",
            },
            "7_tilt_check": {
                "task":  "Run tilt_detector() on last 10 trades — check emotional state",
                "tools": ["TradePsychologyCoach.tilt_detector"],
                "time":  "1 min",
                "why":   "Emotional residue from prior losses causes the worst trading decisions",
            },
            "_total_time": "~35 minutes",
        }

    @staticmethod
    def generate_watchlist(
        pairs: list[str],
        confluence_scores: dict,
        max_pairs: int = 6,
    ) -> list[dict]:
        """
        Rank pairs by confluence score and return a focused watchlist.

        Parameters
        ----------
        pairs             : all candidate pairs
        confluence_scores : {pair: {"overall_score": float, "direction": str, "confluence_pct": float}}
        max_pairs         : maximum number of pairs to include

        Returns
        -------
        List of dicts sorted by confluence_pct descending, with priority grades.
        """
        watchlist: list[dict] = []
        for pair in pairs:
            score = confluence_scores.get(pair, {})
            conf  = float(score.get("confluence_pct", 0))
            watchlist.append({
                "pair":           pair,
                "mtf_score":      score.get("overall_score", 0),
                "bias":           score.get("direction", "NEUTRAL"),
                "confluence_pct": round(conf, 1),
                "priority":       "A+" if conf >= 85 else "A" if conf >= 75 else "B" if conf >= 50 else "C",
                "tradeable":      conf >= 50,
            })

        return sorted(watchlist, key=lambda w: w["confluence_pct"], reverse=True)[:max_pairs]

    @staticmethod
    def end_of_day_review() -> dict:
        """Return a structured end-of-day review template."""
        return {
            "1_log_trades":        "Journal all trades: setup type, entry/exit reasoning, R-multiple achieved",
            "2_plan_adherence":    "Did you follow the plan exactly? Rate 1–10. Identify deviations.",
            "3_what_worked":       "List setups that worked and why (be specific about the edge)",
            "4_what_failed":       "List setups that failed and the true reason (execution vs setup vs bad luck)",
            "5_lessons":           "One concrete lesson to apply in tomorrow's session",
            "6_emotional_state":   "Rate emotional discipline 1–10. Flag any tilt episodes.",
            "7_plan_adjustments":  "Any rule changes for tomorrow? Document and date them.",
            "8_equity_review":     "Update equity curve. Compare to plan expectations.",
        }

    @staticmethod
    def build_plan(
        now: Optional[datetime] = None,
        max_risk_pct: float = 4.0,
        max_trades: int = 3,
        risk_per_trade_pct: float = 1.5,
        extra_rules: Optional[list[str]] = None,
    ) -> DailyTradingPlan:
        """
        Build a complete daily trading plan for the current (or given) time.

        Parameters
        ----------
        now                : UTC datetime (defaults to now)
        max_risk_pct       : maximum total risk allowed today (% of account)
        max_trades         : maximum number of trades
        risk_per_trade_pct : risk per individual trade (% of account)
        extra_rules        : additional trader-specific rules to include

        Returns
        -------
        DailyTradingPlan dataclass with all fields populated.
        """
        now     = now or datetime.utcnow()
        session = TradingPlanBuilder.current_session(now.hour)

        base_rules = [
            "Follow the plan. No improvised trades.",
            "Only A and B grade setups — wait for confluence.",
            "No revenge trades after a loss. Take a break.",
            f"Stop after {max_trades} trades (win or lose) — protect the day.",
            "No trading 30 minutes before or after high-impact news.",
            "If daily loss exceeds 50% of risk budget — stop for the day."]
        if extra_rules:
            base_rules.extend(extra_rules)

        return DailyTradingPlan(
            date          = now.strftime("%Y-%m-%d"),
            session_focus = session,
            risk_budget   = {
                "max_risk_today_pct":   f"{max_risk_pct}%",
                "max_trades":           max_trades,
                "risk_per_trade_pct":   f"{risk_per_trade_pct}%",
                "max_loss_stop_pct":    f"{max_risk_pct / 2:.1f}%",
            },
            rules_today   = base_rules,
            checklist     = TradingPlanBuilder.pre_market_checklist(),
            macro_context = f"Built at {now.strftime('%H:%M')} UTC — {session.upper()} session",
        )

Related Skills

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.