{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Backtesting with ferro-ta\n", "\n", "This notebook demonstrates the minimal backtesting harness and the\n", "indicator pipeline, together with the configuration defaults API.\n", "\n", "Install:\n", "```bash\n", "pip install ferro-ta\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import ferro_ta.config as config\n", "import numpy as np\n", "from ferro_ta.backtest import backtest\n", "from ferro_ta.pipeline import Pipeline\n", "\n", "from ferro_ta import BBANDS, EMA, RSI, SMA\n", "\n", "# Synthetic data\n", "np.random.seed(42)\n", "n = 300\n", "close = np.cumprod(1 + np.random.randn(n) * 0.01) * 100\n", "volume = np.random.randint(1000, 10000, n).astype(float)\n", "print(f\"Generated {n} bars, final price: {close[-1]:.2f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RSI 30/70 Strategy" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "result = backtest(close, strategy=\"rsi_30_70\", timeperiod=14)\n", "print(\"Strategy: RSI 30/70\")\n", "print(f\"Final equity: {result.final_equity:.4f}\")\n", "print(f\"Number of trades: {result.n_trades}\")\n", "print(f\"Return: {(result.final_equity - 1.0) * 100:.2f}%\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## SMA Crossover Strategy" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "result2 = backtest(close, strategy=\"sma_crossover\", fast=10, slow=30)\n", "print(\"Strategy: SMA Crossover (10/30)\")\n", "print(f\"Final equity: {result2.final_equity:.4f}\")\n", "print(f\"Number of trades: {result2.n_trades}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Configuration Defaults\n", "\n", "Set global defaults for indicator parameters to avoid repeating them." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Set global defaults\n", "config.set_default(\"timeperiod\", 20) # global default for all indicators\n", "config.set_default(\"RSI.timeperiod\", 14) # RSI-specific override\n", "\n", "print(\"Current defaults:\", config.list_defaults())\n", "print(\"RSI defaults:\", config.get_defaults_for(\"RSI\"))\n", "print(\"SMA defaults:\", config.get_defaults_for(\"SMA\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Context manager for temporary overrides\n", "with config.Config(timeperiod=5):\n", " temp_default = config.get_default(\"timeperiod\")\n", " print(f\"Inside context: timeperiod={temp_default}\")\n", "\n", "print(f\"After context: timeperiod={config.get_default('timeperiod')}\") # back to 20\n", "\n", "# Clean up\n", "config.reset()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Multi-indicator Pipeline for Feature Engineering" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pipe = (\n", " Pipeline()\n", " .add(\"sma_10\", SMA, timeperiod=10)\n", " .add(\"sma_30\", SMA, timeperiod=30)\n", " .add(\"ema_10\", EMA, timeperiod=10)\n", " .add(\"rsi_14\", RSI, timeperiod=14)\n", " .add(\n", " \"bb\",\n", " BBANDS,\n", " output_keys=[\"bb_upper\", \"bb_mid\", \"bb_lower\"],\n", " timeperiod=20,\n", " nbdevup=2.0,\n", " nbdevdn=2.0,\n", " )\n", ")\n", "\n", "features = pipe.run(close)\n", "print(\"Feature columns:\", list(features.keys()))\n", "\n", "# Build a simple feature matrix (last 5 complete rows)\n", "valid_start = 30 # warmup\n", "feature_matrix = np.column_stack([v[valid_start:] for v in features.values()])\n", "print(f\"Feature matrix shape: {feature_matrix.shape}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Simple Manual Backtest Using the Pipeline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Signal: long when RSI < 40 AND close > SMA_30; flat otherwise\n", "rsi_vals = features[\"rsi_14\"]\n", "sma30_vals = features[\"sma_30\"]\n", "\n", "signal = np.where((rsi_vals < 40) & (close > sma30_vals), 1.0, 0.0)\n", "position = np.roll(signal, 1) # trade on next bar open\n", "position[0] = 0.0\n", "\n", "returns = np.diff(close) / close[:-1]\n", "strategy_returns = returns * position[1:]\n", "\n", "equity = np.cumprod(1 + strategy_returns)\n", "print(f\"Final equity: {equity[-1]:.4f}\")\n", "print(f\"Number of signal bars: {int(signal.sum())}\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 4 }