{"id":110680,"date":"2026-08-11T20:51:29","date_gmt":"2026-08-11T20:51:29","guid":{"rendered":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/"},"modified":"2026-08-11T20:51:29","modified_gmt":"2026-08-11T20:51:29","slug":"building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis","status":"publish","type":"post","link":"https:\/\/youzum.net\/fr\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/","title":{"rendered":"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis"},"content":{"rendered":"<p class=\"wp-block-paragraph\">In this tutorial, we build a complete quantitative backtesting workflow with<a href=\"https:\/\/github.com\/Drakkar-Software\/OctoBot\"> <strong>OctoBot<\/strong><\/a> and OctoBot-Script while keeping the environment isolated from Colab\u2019s preinstalled dependencies. We configure a rule-based trading strategy that combines RSI-based oversold signals, EMA trend confirmation, and ATR-driven adaptive stop-loss and take-profit levels, and we execute it through OctoBot\u2019s native market-order and backtesting APIs. We also retrieve historical OHLCV data through OctoBot\u2019s data layer with automatic exchange fallback, perform a multi-parameter grid search over an in-sample period, and select the strongest configuration based on its excess return relative to buy-and-hold. We then validate the selected parameters on a completely separate out-of-sample period to assess generalization and identify potential overfitting. Finally, we extract OctoBot\u2019s backtest report data and use Pandas and Plotly to analyze parameter sensitivity, portfolio performance, price action, indicators, and execution results in an interactive Colab environment.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">SYMBOL          = \"BTC\/USDT\"\nTIME_FRAME      = \"1d\"\nEXCHANGES       = [\"binance\", \"kucoin\", \"okx\", \"bybit\", \"mexc\", \"kraken\"]\nIN_SAMPLE       = (\"2019-01-01\", \"2023-01-01\")\nOUT_OF_SAMPLE   = (\"2023-01-01\", \"2025-06-01\")\nGRID = {\n   \"rsi_period\":     [7, 14, 21],\n   \"rsi_threshold\":  [25, 30, 35],\n   \"tp_atr_mult\":    [3.0, 5.0],\n}\nFIXED = {\n   \"ema_fast\":      50,\n   \"ema_slow\":      200,\n   \"atr_period\":    14,\n   \"sl_atr_mult\":   2.0,\n   \"position_size\": \"20%\",\n   \"min_offset_pct\": 1.0,\n   \"max_offset_pct\": 40.0,\n}\nVENV_DIR   = \"\/content\/octobot_env\"\nWORK_DIR   = \"\/content\/octobot_lab\"\nOCTOBOT_V  = \"2.1.1\"\nPY_VERSION = \"3.12\"\nimport json, os, subprocess, sys, textwrap, time, itertools, shutil\nos.makedirs(WORK_DIR, exist_ok=True)\nPY     = os.path.join(VENV_DIR, \"bin\", \"python\")\nMARKER = os.path.join(VENV_DIR, \".octobot_ready\")\ndef sh(cmd, **kw):\n   \"\"\"Run a command, streaming its output live into the Colab cell.\"\"\"\n   print(f\"$ {' '.join(cmd)}\")\n   p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n                        text=True, bufsize=1, **kw)\n   for line in p.stdout:\n       print(\"   \" + line.rstrip())\n   p.wait()\n   if p.returncode != 0:\n       raise RuntimeError(f\"command failed ({p.returncode}): {' '.join(cmd)}\")\nif not os.path.exists(MARKER):\n   print(\"=\" * 90, \"n  BUILDING OCTOBOT ENVIRONMENT (one-off, ~2 min)n\", \"=\" * 90)\n   subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"uv\"], check=True)\n   UV = [sys.executable, \"-m\", \"uv\"]\n   sh(UV + [\"venv\", \"--python\", PY_VERSION, VENV_DIR])\n   sh(UV + [\"pip\", \"install\", \"--python\", PY, \"-q\",\n            f\"OctoBot=={OCTOBOT_V}\", \"wheel\", \"setuptools\", \"appdirs==1.4.4\"])\n   sh(UV + [\"pip\", \"install\", \"--python\", PY, \"-q\", \"--no-build-isolation\", \"octobot-script\"])\n   sh([PY, \"-m\", \"octobot_script.cli\", \"install_tentacles\", \"--quite\"])\n   sh([PY, \"-c\", textwrap.dedent(\"\"\"\n       import os, shutil, octobot_script.resources as r\n       base = r.get_report_resource_path(\"\")\n       src, dst_dir = os.path.join(base, \"index.html\"), os.path.join(base, \"dist\")\n       os.makedirs(dst_dir, exist_ok=True)\n       dst = os.path.join(dst_dir, \"index.html\")\n       if os.path.exists(src) and not os.path.exists(dst):\n           shutil.copy2(src, dst); print(\"patched report template -&gt;\", dst)\n       else:\n           print(\"report template already fine\")\n   \"\"\")])\n   open(MARKER, \"w\").write(\"ok\")\n   print(\"n<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" alt=\"\u2705\" class=\"wp-smiley\" \/> environment readyn\")\nelse:\n   print(\"<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" alt=\"\u2705\" class=\"wp-smiley\" \/> environment already built (delete\", VENV_DIR, \"to rebuild)n\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We define the core trading configuration, including the symbol, timeframe, exchange fallback list, backtesting windows, parameter grid, and fixed strategy settings. We then create an isolated Python environment with uv and install the pinned OctoBot and OctoBot-Script dependencies required for the workflow. We also install the OctoBot tentacles package and patch the report-template path so later backtest reporting works correctly inside the Colab environment.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">WORKER = os.path.join(WORK_DIR, \"octobot_worker.py\")\nWORKER_SRC = r'''\nimport asyncio, itertools, json, os, sys, time, traceback\nimport numpy as np\nimport tulipy\nimport octobot_script as obs\nCFG  = json.load(open(os.environ[\"OBS_CONFIG\"]))\nOUT  = os.environ[\"OBS_OUT\"]\nFIX  = CFG[\"fixed\"]\nfor kw in (\"Close\", \"High\", \"Low\", \"Time\", \"market\", \"current_live_time\", \"plot_indicator\"):\n   if not hasattr(obs, kw):\n       raise RuntimeError(\n           f\"octobot_script.{kw} missing -&gt; tentacles are not installed. \"\n           \"Run: python -m octobot_script.cli install_tentacles\"\n       )\ndef tail(*arrays):\n   \"\"\"tulipy indicators return different lengths; right-align them all.\"\"\"\n   n = min(len(a) for a in arrays)\n   return [np.asarray(a)[-n:] for a in arrays]\ndef clamp(v):\n   return float(min(max(v, FIX[\"min_offset_pct\"]), FIX[\"max_offset_pct\"]))\ndef build_callbacks(params, run_data):\n   \"\"\"\n   OctoBot-Script splits a strategy into:\n     initialize(ctx) -&gt; runs once on the first candle. Do vectorised work here.\n     strategy(ctx)   -&gt; runs on EVERY closed candle. Keep it cheap.\n   \"\"\"\n   async def initialize(ctx):\n       closes = await obs.Close(ctx, max_history=True)\n       highs  = await obs.High(ctx,  max_history=True)\n       lows   = await obs.Low(ctx,   max_history=True)\n       times  = await obs.Time(ctx,  max_history=True, use_close_time=True)\n       rsi  = tulipy.rsi(closes, period=params[\"rsi_period\"])\n       ema_f = tulipy.ema(closes, period=FIX[\"ema_fast\"])\n       ema_s = tulipy.ema(closes, period=FIX[\"ema_slow\"])\n       atr   = tulipy.atr(highs, lows, closes, period=FIX[\"atr_period\"])\n       t, c, rsi, ema_f, ema_s, atr = tail(times, closes, rsi, ema_f, ema_s, atr)\n       atr_pct = np.where(c &gt; 0, atr \/ c * 100.0, 0.0)\n       entries, offsets = set(), {}\n       for i in range(len(t)):\n           oversold = rsi[i] &lt; params[\"rsi_threshold\"]\n           uptrend  = ema_f[i] &gt; ema_s[i]\n           if oversold and uptrend and atr_pct[i] &gt; 0:\n               ts = float(t[i])\n               entries.add(ts)\n               offsets[ts] = (\n                   clamp(FIX[\"sl_atr_mult\"]     * atr_pct[i]),\n                   clamp(params[\"tp_atr_mult\"]  * atr_pct[i]),\n               )\n       run_data[\"entries\"] = entries\n       run_data[\"offsets\"] = offsets\n       if run_data.get(\"plot\"):\n           await obs.plot_indicator(ctx, f\"RSI({params['rsi_period']})\", t, rsi, entries)\n           await obs.plot_indicator(ctx, f\"EMA{FIX['ema_fast']}\",  t, ema_f)\n           await obs.plot_indicator(ctx, f\"EMA{FIX['ema_slow']}\",  t, ema_s)\n           await obs.plot_indicator(ctx, \"ATR %\", t, atr_pct)\n   async def strategy(ctx):\n       now = obs.current_live_time(ctx)\n       if now not in run_data[\"entries\"]:\n           return\n       sl, tp = run_data[\"offsets\"][now]\n       await obs.market(\n           ctx, \"buy\",\n           amount=FIX[\"position_size\"],\n           stop_loss_offset=f\"-{sl:.2f}%\",\n           take_profit_offset=f\"{tp:.2f}%\",\n       )\n   return initialize, strategy\ndef metrics(res):\n   br = res.report.get(\"bot_report\", {})\n   first = lambda d: float(list(d.values())[0]) if isinstance(d, dict) and d else float(\"nan\")\n   return {\n       \"profitability\":  first(br.get(\"profitability\", {})),\n       \"market\":         first(br.get(\"market_average_profitability\", {})),\n       \"reference\":      br.get(\"reference_market\"),\n       \"start_portfolio\": str(br.get(\"starting_portfolio\")),\n       \"end_portfolio\":   str(br.get(\"end_portfolio\")),\n       \"candles\":        res.candles_count,\n       \"duration_s\":     round(res.duration or 0, 2),\n       \"errors\":         res.report.get(\"errors_count\"),\n   }\nasync def load_data(window):\n   \"\"\"Try each exchange until one serves data (Binance blocks many datacenter IPs).\"\"\"\n   start, end = window\n   last = None\n   for ex in CFG[\"exchanges\"]:\n       try:\n           print(f\"  \u2193 fetching {CFG['symbol']} {CFG['time_frame']} from {ex} \"\n                 f\"[{time.strftime('%Y-%m-%d', time.gmtime(start))} \u2192 \"\n                 f\"{time.strftime('%Y-%m-%d', time.gmtime(end))}]\", flush=True)\n           data = await obs.get_data(\n               CFG[\"symbol\"], CFG[\"time_frame\"],\n               exchange=ex, exchange_type=\"spot\",\n               start_timestamp=start, end_timestamp=end,\n               social_services=[],\n           )\n           print(f\"    \u2713 {ex} ok -&gt; {data.data_files}\", flush=True)\n           return data, ex\n       except Exception as e:\n           last = e\n           print(f\"    \u2717 {ex}: {type(e).__name__}: {e}\", flush=True)\n   raise RuntimeError(f\"no exchange served data; last error: {last}\")\nasync def backtest(data, params, plot=False, storage=False):\n   run_data = {\"entries\": None, \"offsets\": {}, \"plot\": plot}\n   init_f, strat_f = build_callbacks(params, run_data)\n   res = await obs.run(\n       data, params,\n       strategy_func=strat_f,\n       initialize_func=init_f,\n       enable_logs=False,\n       enable_storage=storage,\n   )\n   return res, len(run_data[\"entries\"] or ())\nasync def main():\n   out = {\"grid\": [], \"best\": None, \"oos\": None, \"errors\": []}\n   print(\"n\" + \"=\" * 78 + \"n  IN-SAMPLE GRID SEARCHn\" + \"=\" * 78, flush=True)\n   is_data, ex_used = await load_data(CFG[\"in_sample\"])\n   out[\"exchange\"] = ex_used\n   keys  = list(CFG[\"grid\"].keys())\n   combos = [dict(zip(keys, v)) for v in itertools.product(*CFG[\"grid\"].values())]\n   print(f\"  {len(combos)} configurations to evaluaten\", flush=True)\n   for i, params in enumerate(combos, 1):\n       try:\n           res, n_sig = await backtest(is_data, params)\n           m = metrics(res)\n           m.update(params); m[\"signals\"] = n_sig\n           m[\"edge\"] = m[\"profitability\"] - m[\"market\"]\n           out[\"grid\"].append(m)\n           print(f\"  [{i:&gt;2}\/{len(combos)}] {params}  \"\n                 f\"P&amp;L {m['profitability']:+.2f}%  vs market {m['market']:+.2f}%  \"\n                 f\"edge {m['edge']:+.2f}%  ({n_sig} signals, {m['duration_s']}s)\", flush=True)\n       except Exception as e:\n           out[\"errors\"].append(f\"{params}: {e}\")\n           print(f\"  [{i:&gt;2}\/{len(combos)}] {params} FAILED: {e}\", flush=True)\n           traceback.print_exc()\n   await is_data.stop()\n   if not out[\"grid\"]:\n       json.dump(out, open(OUT, \"w\")); raise SystemExit(\"no successful runs\")\n   best = max(out[\"grid\"], key=lambda r: r[\"edge\"])\n   out[\"best\"] = {k: best[k] for k in keys}\n   print(f\"n  <img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/1f3c6.png\" alt=\"\ud83c\udfc6\" class=\"wp-smiley\" \/> best in-sample config: {out['best']}  (edge {best['edge']:+.2f}%)\", flush=True)\n   print(\"n\" + \"=\" * 78 + \"n  OUT-OF-SAMPLE VALIDATION (never optimised on)n\" + \"=\" * 78,\n         flush=True)\n   oos_data, _ = await load_data(CFG[\"out_of_sample\"])\n   res, n_sig = await backtest(oos_data, out[\"best\"], plot=True, storage=True)\n   m = metrics(res); m.update(out[\"best\"])\n   m[\"signals\"] = n_sig; m[\"edge\"] = m[\"profitability\"] - m[\"market\"]\n   out[\"oos\"] = m\n   print(f\"  OOS P&amp;L {m['profitability']:+.2f}%  vs market {m['market']:+.2f}%  \"\n         f\"edge {m['edge']:+.2f}%  ({n_sig} signals)\", flush=True)\n   print(\"  \" + res.describe(), flush=True)\n   report_dir = os.path.join(os.getcwd(), \"report\")\n   os.makedirs(report_dir, exist_ok=True)\n   try:\n       plot = await res.plot(report_file=os.path.join(report_dir, \"report.html\"), show=False)\n       out[\"bundle\"] = os.path.join(os.path.dirname(os.path.abspath(plot.report_file)),\n                                    \"report.json\")\n       print(f\"  \u2713 report bundle: {out['bundle']}\", flush=True)\n   except Exception as e:\n       out[\"errors\"].append(f\"report: {e}\")\n       print(f\"  \u2717 report generation failed: {e}\", flush=True)\n   await oos_data.stop()\n   json.dump(out, open(OUT, \"w\"), indent=2, default=str)\n   print(\"n\u2713 results written to\", OUT, flush=True)\nasyncio.run(main())\n'''\nwith open(WORKER, \"w\") as f:\n   f.write(WORKER_SRC)\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We build the standalone OctoBot worker that contains the strategy logic and executes inside the isolated virtual environment. We calculate RSI, fast and slow EMAs, and ATR values, generate entry signals when oversold conditions align with an upward trend, and derive volatility-adjusted stop-loss and take-profit offsets. We also define the historical data loader, backtest runner, grid-search loop, out-of-sample validation, performance metrics, and report generation process.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">import datetime as _dt\ndef ts(d):\n   return int(_dt.datetime.strptime(d, \"%Y-%m-%d\")\n              .replace(tzinfo=_dt.timezone.utc).timestamp())\nCONFIG_PATH  = os.path.join(WORK_DIR, \"config.json\")\nRESULTS_PATH = os.path.join(WORK_DIR, \"results.json\")\njson.dump({\n   \"symbol\": SYMBOL, \"time_frame\": TIME_FRAME, \"exchanges\": EXCHANGES,\n   \"in_sample\":     [ts(IN_SAMPLE[0]),     ts(IN_SAMPLE[1])],\n   \"out_of_sample\": [ts(OUT_OF_SAMPLE[0]), ts(OUT_OF_SAMPLE[1])],\n   \"grid\": GRID, \"fixed\": FIXED,\n}, open(CONFIG_PATH, \"w\"), indent=2)\nenv = dict(os.environ, OBS_CONFIG=CONFIG_PATH, OBS_OUT=RESULTS_PATH,\n          PYTHONUNBUFFERED=\"1\")\nt0 = time.time()\nproc = subprocess.Popen([PY, WORKER], cwd=WORK_DIR, env=env, text=True,\n                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1)\nfor line in proc.stdout:\n   print(line.rstrip())\nproc.wait()\nprint(f\"n<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/23f1.png\" alt=\"\u23f1\" class=\"wp-smiley\" \/>  total backtesting time: {time.time() - t0:.1f}s  (exit {proc.returncode})\")\nif not os.path.exists(RESULTS_PATH):\n   raise SystemExit(\"No results produced \u2014 read the log above. \"\n                    \"Most common cause: every exchange refused the data request.\")\nR = json.load(open(RESULTS_PATH))\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We convert the selected in-sample and out-of-sample dates into UTC timestamps and serialize the complete experiment configuration into a JSON file. We launch the OctoBot worker as a separate subprocess so its dependency environment remains isolated from the main Colab kernel while its logs stream directly into the notebook. We then verify that the run produces a results file and load the generated JSON output for downstream analysis.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">import pandas as pd\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\npd.set_option(\"display.width\", 160)\ngrid = pd.DataFrame(R[\"grid\"]).sort_values(\"edge\", ascending=False)\ncols = [c for c in [\"rsi_period\", \"rsi_threshold\", \"tp_atr_mult\", \"signals\",\n                   \"profitability\", \"market\", \"edge\", \"duration_s\"] if c in grid.columns]\nprint(\"n=== IN-SAMPLE GRID (ranked by edge over buy &amp; hold) ===\")\nprint(grid[cols].to_string(index=False, float_format=lambda v: f\"{v:,.2f}\"))\nif R.get(\"oos\"):\n   o = R[\"oos\"]\n   print(\"n=== OUT-OF-SAMPLE ===\")\n   print(f\"  config          : { {k: o[k] for k in GRID} }\")\n   print(f\"  strategy return : {o['profitability']:+.2f}%\")\n   print(f\"  buy &amp; hold      : {o['market']:+.2f}%\")\n   print(f\"  edge            : {o['edge']:+.2f}%   \u2190 the only number that matters\")\n   print(f\"  entries taken   : {o['signals']}\")\n   print(f\"  end portfolio   : {o['end_portfolio']}\")\n   is_edge = grid.iloc[0][\"edge\"]\n   decay = o[\"edge\"] - is_edge\n   print(f\"n  edge decay IS\u2192OOS: {decay:+.2f} pts \"\n         f\"({'holds up' if decay &gt; -5 else 'likely overfit \u2014 treat with suspicion'})\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We move back into the Colab environment and organize the grid-search results with Pandas for easier comparison and interpretation. We rank every parameter configuration according to its excess return over the market and print the key performance metrics for both the in-sample search and out-of-sample validation. We also calculate the change in strategy edge between the two periods to obtain a simple indication of whether the optimized parameters generalize or show signs of overfitting.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">if {\"rsi_period\", \"rsi_threshold\"} &lt;= set(grid.columns):\n   pivot = grid.pivot_table(index=\"rsi_threshold\", columns=\"rsi_period\",\n                            values=\"edge\", aggfunc=\"mean\")\n   fig = go.Figure(go.Heatmap(z=pivot.values, x=pivot.columns, y=pivot.index,\n                              colorscale=\"RdYlGn\", zmid=0,\n                              colorbar=dict(title=\"edge %\"),\n                              text=pivot.round(1).values, texttemplate=\"%{text}\"))\n   fig.update_layout(title=\"In-sample edge vs buy &amp; hold \u2014 a broad plateau is trustworthy, \"\n                           \"an isolated hot cell is noise\",\n                     xaxis_title=\"RSI period\", yaxis_title=\"RSI buy threshold\",\n                     height=380, template=\"plotly_dark\")\n   fig.show()\ndef harvest(node, found):\n   \"\"\"The report bundle nests display elements arbitrarily; walk it and grab\n   anything that looks like a plottable series.\"\"\"\n   if isinstance(node, dict):\n       if isinstance(node.get(\"x\"), list) and len(node[\"x\"]) &gt; 1:\n           if all(k in node for k in (\"open\", \"high\", \"low\", \"close\")):\n               found[\"candles\"].append(node)\n           elif isinstance(node.get(\"y\"), list) and len(node[\"y\"]) == len(node[\"x\"]):\n               found[\"series\"].append(node)\n       for v in node.values():\n           harvest(v, found)\n   elif isinstance(node, list):\n       for v in node:\n           harvest(v, found)\n   return found\nbundle_path = R.get(\"bundle\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We visualize the parameter-search surface by plotting the average strategy edge across RSI periods and entry thresholds as an interactive Plotly heatmap. We use this surface to inspect whether strong performance appears across a broad parameter region or only around an isolated configuration that may represent noise. We also define a recursive report-harvesting function that searches OctoBot\u2019s nested report structure for candle data and other plottable time-series elements.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">if bundle_path and os.path.exists(bundle_path):\n   bundle = json.load(open(bundle_path))\n   f = harvest(bundle, {\"candles\": [], \"series\": []})\n   print(f\"n=== REPORT BUNDLE === {len(f['candles'])} candle set(s), \"\n         f\"{len(f['series'])} series\")\n   def norm_x(xs):\n       xs = [float(v) for v in xs]\n       unit = \"ms\" if (xs and max(xs) &gt; 1e11) else \"s\"\n       return pd.to_datetime(xs, unit=unit)\n   fig = make_subplots(rows=2, cols=1, shared_xaxes=True,\n                       row_heights=[0.62, 0.38], vertical_spacing=0.06,\n                       subplot_titles=(\"Price &amp; executed trades\",\n                                       \"Portfolio value \/ indicators\"))\n   if f[\"candles\"]:\n       c = max(f[\"candles\"], key=lambda d: len(d[\"x\"]))\n       fig.add_trace(go.Candlestick(x=norm_x(c[\"x\"]), open=c[\"open\"], high=c[\"high\"],\n                                    low=c[\"low\"], close=c[\"close\"], name=SYMBOL),\n                     row=1, col=1)\n   portfolio_kw = (\"portfolio\", \"value\", \"wallet\", \"balance\")\n   for s in f[\"series\"]:\n       title = str(s.get(\"title\") or s.get(\"name\") or \"series\")\n       n = len(s[\"x\"])\n       if n &lt; 3:\n           continue\n       mode = s.get(\"mode\") or (\"markers\" if n &lt; 60 else \"lines\")\n       row = 2 if any(k in title.lower() for k in portfolio_kw) or \"rsi\" in title.lower() \n               or \"atr\" in title.lower() else 1\n       fig.add_trace(go.Scatter(x=norm_x(s[\"x\"]), y=s[\"y\"], name=title[:38],\n                                mode=mode, opacity=0.9), row=row, col=1)\n   fig.update_layout(height=760, template=\"plotly_dark\", xaxis_rangeslider_visible=False,\n                     title=f\"OctoBot out-of-sample run \u2014 {SYMBOL} {TIME_FRAME} \"\n                           f\"({R.get('exchange', '?')}) \u2014 {OUT_OF_SAMPLE[0]} \u2192 {OUT_OF_SAMPLE[1]}\",\n                     legend=dict(orientation=\"h\", y=-0.08))\n   fig.show()\nelse:\n   print(\"n(no report bundle \u2014 charts skipped; the numeric results above are still valid)\")\nif R.get(\"errors\"):\n   print(\"n<img decoding=\"async\" src=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/26a0.png\" alt=\"\u26a0\" class=\"wp-smiley\" \/> non-fatal errors during the run:\")\n   for e in R[\"errors\"]:\n       print(\"  -\", e)\nprint(\"\"\"\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nWHERE TO GO NEXT\n \u2022 Edit GRID \/ FIXED at the top and re-run \u2014 the env is cached, only backtests rerun.\n \u2022 obs.limit(ctx, \"sell\", amount=\"50%\", offset=\"2%\")  \u2192 limit orders\n \u2022 obs.set_leverage(ctx, 3) + exchange_type=\"future\"  \u2192 futures \/ shorts\n \u2022 get_data() accepts LISTS for symbol and time_frame \u2192 multi-asset, multi-TF strategies\n \u2022 Swap tulipy for pandas-ta \/ your own ML model: initialize() just needs to fill a\n   set of entry timestamps, so a trained classifier drops straight in.\n \u2022 Docs: https:\/\/www.octobot.cloud\/en\/guides\/octobot-script\n \u2022 For live\/paper trading use the full OctoBot app, not this scripting layer.\nReminder: past performance in a backtest tells you about the past. Slippage, fees\nbeyond the simulator's model, liquidity and regime change all bite in live markets.\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\"\"\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We load the generated OctoBot report bundle and reconstruct the out-of-sample trading results as interactive price and indicator charts. We normalize timestamps, display candlestick data, and dynamically add available portfolio, RSI, ATR, trade, and other report series to a multi-panel Plotly visualization. We finally surface any non-fatal execution errors and outline several directions for extending the workflow, including limit orders, futures, multi-asset strategies, and machine-learning-based signals.<\/p>\n<p class=\"wp-block-paragraph\">In conclusion, we implemented an end-to-end OctoBot quantitative research pipeline that moves beyond a simple single-run backtest and introduces a more disciplined strategy-development process. We isolated OctoBot\u2019s dependency stack, retrieved exchange data through its native infrastructure, defined a volatility-aware RSI and EMA strategy, optimized its parameters on historical in-sample data, and evaluated the winning configuration on an untouched out-of-sample window. By comparing strategy profitability against buy-and-hold performance and examining parameter surfaces and out-of-sample edge decay, we gained a clearer view of whether our results represent a robust trading signal or merely an overfitted historical pattern. We also transformed OctoBot\u2019s generated report bundle into interactive visualizations that make strategy behavior, market movements, indicators, and portfolio dynamics easier to inspect.<\/p>\n<p class=\"wp-block-paragraph\">\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<\/p><p class=\"wp-block-paragraph\">\n<\/p><p class=\"wp-block-paragraph\">Check out the<strong>\u00a0<a href=\"https:\/\/github.com\/MARKTECHPOST-AI-MEDIA-INC\/AI-Agents-Projects-Tutorials\/blob\/main\/Data%20Science\/OctoBot_Advanced_Quant_Strategy_Backtesting_Marktechpost.ipynb\" target=\"_blank\" rel=\"noreferrer noopener\">Full Codes here<\/a><\/strong>.<strong>\u00a0<\/strong>Also,\u00a0feel free to follow us on\u00a0<strong><a href=\"https:\/\/x.com\/intent\/follow?screen_name=marktechpost\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Twitter<\/mark><\/a><\/strong>\u00a0and don\u2019t forget to join our\u00a0<strong><a href=\"https:\/\/www.reddit.com\/r\/machinelearningnews\/\" target=\"_blank\" rel=\"noreferrer noopener\">150k+ML SubReddit<\/a><\/strong>\u00a0and Subscribe to\u00a0<strong><a href=\"https:\/\/magic.beehiiv.com\/v1\/f5e63dd4-5653-4f09-83e2-321a8b1ba526?email=%7B%7Bemail%7D%7D\" target=\"_blank\" rel=\"noreferrer noopener\">our Newsletter<\/a><\/strong>. Wait! are you on telegram?\u00a0<strong><a href=\"https:\/\/t.me\/machinelearningresearchnews\" target=\"_blank\" rel=\"noreferrer noopener\">now you can join us on telegram as well.<\/a><\/strong><\/p>\n<p class=\"wp-block-paragraph\">Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.?\u00a0<strong><a href=\"https:\/\/forms.gle\/wbash1wF6efRj8G58\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Connect with us<\/mark><\/a><\/strong><\/p>\n<p>The post <a href=\"https:\/\/www.marktechpost.com\/2026\/08\/11\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\">Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis<\/a> appeared first on <a href=\"https:\/\/www.marktechpost.com\/\">MarkTechPost<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we build a complete quantitative backtesting workflow with OctoBot and OctoBot-Script while keeping the environment isolated from Colab\u2019s preinstalled dependencies. We configure a rule-based trading strategy that combines RSI-based oversold signals, EMA trend confirmation, and ATR-driven adaptive stop-loss and take-profit levels, and we execute it through OctoBot\u2019s native market-order and backtesting APIs. We also retrieve historical OHLCV data through OctoBot\u2019s data layer with automatic exchange fallback, perform a multi-parameter grid search over an in-sample period, and select the strongest configuration based on its excess return relative to buy-and-hold. We then validate the selected parameters on a completely separate out-of-sample period to assess generalization and identify potential overfitting. Finally, we extract OctoBot\u2019s backtest report data and use Pandas and Plotly to analyze parameter sensitivity, portfolio performance, price action, indicators, and execution results in an interactive Colab environment. Copy CodeCopiedUse a different Browser SYMBOL = &#8220;BTC\/USDT&#8221; TIME_FRAME = &#8220;1d&#8221; EXCHANGES = [&#8220;binance&#8221;, &#8220;kucoin&#8221;, &#8220;okx&#8221;, &#8220;bybit&#8221;, &#8220;mexc&#8221;, &#8220;kraken&#8221;] IN_SAMPLE = (&#8220;2019-01-01&#8221;, &#8220;2023-01-01&#8221;) OUT_OF_SAMPLE = (&#8220;2023-01-01&#8221;, &#8220;2025-06-01&#8221;) GRID = { &#8220;rsi_period&#8221;: [7, 14, 21], &#8220;rsi_threshold&#8221;: [25, 30, 35], &#8220;tp_atr_mult&#8221;: [3.0, 5.0], } FIXED = { &#8220;ema_fast&#8221;: 50, &#8220;ema_slow&#8221;: 200, &#8220;atr_period&#8221;: 14, &#8220;sl_atr_mult&#8221;: 2.0, &#8220;position_size&#8221;: &#8220;20%&#8221;, &#8220;min_offset_pct&#8221;: 1.0, &#8220;max_offset_pct&#8221;: 40.0, } VENV_DIR = &#8220;\/content\/octobot_env&#8221; WORK_DIR = &#8220;\/content\/octobot_lab&#8221; OCTOBOT_V = &#8220;2.1.1&#8221; PY_VERSION = &#8220;3.12&#8221; import json, os, subprocess, sys, textwrap, time, itertools, shutil os.makedirs(WORK_DIR, exist_ok=True) PY = os.path.join(VENV_DIR, &#8220;bin&#8221;, &#8220;python&#8221;) MARKER = os.path.join(VENV_DIR, &#8220;.octobot_ready&#8221;) def sh(cmd, **kw): &#8220;&#8221;&#8221;Run a command, streaming its output live into the Colab cell.&#8221;&#8221;&#8221; print(f&#8221;$ {&#8216; &#8216;.join(cmd)}&#8221;) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, **kw) for line in p.stdout: print(&#8221; &#8221; + line.rstrip()) p.wait() if p.returncode != 0: raise RuntimeError(f&#8221;command failed ({p.returncode}): {&#8216; &#8216;.join(cmd)}&#8221;) if not os.path.exists(MARKER): print(&#8220;=&#8221; * 90, &#8220;n BUILDING OCTOBOT ENVIRONMENT (one-off, ~2 min)n&#8221;, &#8220;=&#8221; * 90) subprocess.run([sys.executable, &#8220;-m&#8221;, &#8220;pip&#8221;, &#8220;install&#8221;, &#8220;-q&#8221;, &#8220;uv&#8221;], check=True) UV = [sys.executable, &#8220;-m&#8221;, &#8220;uv&#8221;] sh(UV + [&#8220;venv&#8221;, &#8220;&#8211;python&#8221;, PY_VERSION, VENV_DIR]) sh(UV + [&#8220;pip&#8221;, &#8220;install&#8221;, &#8220;&#8211;python&#8221;, PY, &#8220;-q&#8221;, f&#8221;OctoBot=={OCTOBOT_V}&#8221;, &#8220;wheel&#8221;, &#8220;setuptools&#8221;, &#8220;appdirs==1.4.4&#8221;]) sh(UV + [&#8220;pip&#8221;, &#8220;install&#8221;, &#8220;&#8211;python&#8221;, PY, &#8220;-q&#8221;, &#8220;&#8211;no-build-isolation&#8221;, &#8220;octobot-script&#8221;]) sh([PY, &#8220;-m&#8221;, &#8220;octobot_script.cli&#8221;, &#8220;install_tentacles&#8221;, &#8220;&#8211;quite&#8221;]) sh([PY, &#8220;-c&#8221;, textwrap.dedent(&#8220;&#8221;&#8221; import os, shutil, octobot_script.resources as r base = r.get_report_resource_path(&#8220;&#8221;) src, dst_dir = os.path.join(base, &#8220;index.html&#8221;), os.path.join(base, &#8220;dist&#8221;) os.makedirs(dst_dir, exist_ok=True) dst = os.path.join(dst_dir, &#8220;index.html&#8221;) if os.path.exists(src) and not os.path.exists(dst): shutil.copy2(src, dst); print(&#8220;patched report template -&gt;&#8221;, dst) else: print(&#8220;report template already fine&#8221;) &#8220;&#8221;&#8221;)]) open(MARKER, &#8220;w&#8221;).write(&#8220;ok&#8221;) print(&#8220;n environment readyn&#8221;) else: print(&#8221; environment already built (delete&#8221;, VENV_DIR, &#8220;to rebuild)n&#8221;) We define the core trading configuration, including the symbol, timeframe, exchange fallback list, backtesting windows, parameter grid, and fixed strategy settings. We then create an isolated Python environment with uv and install the pinned OctoBot and OctoBot-Script dependencies required for the workflow. We also install the OctoBot tentacles package and patch the report-template path so later backtest reporting works correctly inside the Colab environment. Copy CodeCopiedUse a different Browser WORKER = os.path.join(WORK_DIR, &#8220;octobot_worker.py&#8221;) WORKER_SRC = r&#8221;&#8217; import asyncio, itertools, json, os, sys, time, traceback import numpy as np import tulipy import octobot_script as obs CFG = json.load(open(os.environ[&#8220;OBS_CONFIG&#8221;])) OUT = os.environ[&#8220;OBS_OUT&#8221;] FIX = CFG[&#8220;fixed&#8221;] for kw in (&#8220;Close&#8221;, &#8220;High&#8221;, &#8220;Low&#8221;, &#8220;Time&#8221;, &#8220;market&#8221;, &#8220;current_live_time&#8221;, &#8220;plot_indicator&#8221;): if not hasattr(obs, kw): raise RuntimeError( f&#8221;octobot_script.{kw} missing -&gt; tentacles are not installed. &#8221; &#8220;Run: python -m octobot_script.cli install_tentacles&#8221; ) def tail(*arrays): &#8220;&#8221;&#8221;tulipy indicators return different lengths; right-align them all.&#8221;&#8221;&#8221; n = min(len(a) for a in arrays) return [np.asarray(a)[-n:] for a in arrays] def clamp(v): return float(min(max(v, FIX[&#8220;min_offset_pct&#8221;]), FIX[&#8220;max_offset_pct&#8221;])) def build_callbacks(params, run_data): &#8220;&#8221;&#8221; OctoBot-Script splits a strategy into: initialize(ctx) -&gt; runs once on the first candle. Do vectorised work here. strategy(ctx) -&gt; runs on EVERY closed candle. Keep it cheap. &#8220;&#8221;&#8221; async def initialize(ctx): closes = await obs.Close(ctx, max_history=True) highs = await obs.High(ctx, max_history=True) lows = await obs.Low(ctx, max_history=True) times = await obs.Time(ctx, max_history=True, use_close_time=True) rsi = tulipy.rsi(closes, period=params[&#8220;rsi_period&#8221;]) ema_f = tulipy.ema(closes, period=FIX[&#8220;ema_fast&#8221;]) ema_s = tulipy.ema(closes, period=FIX[&#8220;ema_slow&#8221;]) atr = tulipy.atr(highs, lows, closes, period=FIX[&#8220;atr_period&#8221;]) t, c, rsi, ema_f, ema_s, atr = tail(times, closes, rsi, ema_f, ema_s, atr) atr_pct = np.where(c &gt; 0, atr \/ c * 100.0, 0.0) entries, offsets = set(), {} for i in range(len(t)): oversold = rsi[i] &lt; params[&#8220;rsi_threshold&#8221;] uptrend = ema_f[i] &gt; ema_s[i] if oversold and uptrend and atr_pct[i] &gt; 0: ts = float(t[i]) entries.add(ts) offsets[ts] = ( clamp(FIX[&#8220;sl_atr_mult&#8221;] * atr_pct[i]), clamp(params[&#8220;tp_atr_mult&#8221;] * atr_pct[i]), ) run_data[&#8220;entries&#8221;] = entries run_data[&#8220;offsets&#8221;] = offsets if run_data.get(&#8220;plot&#8221;): await obs.plot_indicator(ctx, f&#8221;RSI({params[&#8216;rsi_period&#8217;]})&#8221;, t, rsi, entries) await obs.plot_indicator(ctx, f&#8221;EMA{FIX[&#8217;ema_fast&#8217;]}&#8221;, t, ema_f) await obs.plot_indicator(ctx, f&#8221;EMA{FIX[&#8217;ema_slow&#8217;]}&#8221;, t, ema_s) await obs.plot_indicator(ctx, &#8220;ATR %&#8221;, t, atr_pct) async def strategy(ctx): now = obs.current_live_time(ctx) if now not in run_data[&#8220;entries&#8221;]: return sl, tp = run_data[&#8220;offsets&#8221;][now] await obs.market( ctx, &#8220;buy&#8221;, amount=FIX[&#8220;position_size&#8221;], stop_loss_offset=f&#8221;-{sl:.2f}%&#8221;, take_profit_offset=f&#8221;{tp:.2f}%&#8221;, ) return initialize, strategy def metrics(res): br = res.report.get(&#8220;bot_report&#8221;, {}) first = lambda d: float(list(d.values())[0]) if isinstance(d, dict) and d else float(&#8220;nan&#8221;) return { &#8220;profitability&#8221;: first(br.get(&#8220;profitability&#8221;, {})), &#8220;market&#8221;: first(br.get(&#8220;market_average_profitability&#8221;, {})), &#8220;reference&#8221;: br.get(&#8220;reference_market&#8221;), &#8220;start_portfolio&#8221;: str(br.get(&#8220;starting_portfolio&#8221;)), &#8220;end_portfolio&#8221;: str(br.get(&#8220;end_portfolio&#8221;)), &#8220;candles&#8221;: res.candles_count, &#8220;duration_s&#8221;: round(res.duration or 0, 2), &#8220;errors&#8221;: res.report.get(&#8220;errors_count&#8221;), } async def load_data(window): &#8220;&#8221;&#8221;Try each exchange until one serves data (Binance blocks many datacenter IPs).&#8221;&#8221;&#8221; start, end = window last = None for ex in CFG[&#8220;exchanges&#8221;]: try: print(f&#8221; \u2193 fetching {CFG[&#8216;symbol&#8217;]} {CFG[&#8216;time_frame&#8217;]} from {ex} &#8221; f&#8221;[{time.strftime(&#8216;%Y-%m-%d&#8217;, time.gmtime(start))} \u2192 &#8221; f&#8221;{time.strftime(&#8216;%Y-%m-%d&#8217;, time.gmtime(end))}]&#8221;, flush=True) data = await obs.get_data( CFG[&#8220;symbol&#8221;], CFG[&#8220;time_frame&#8221;], exchange=ex, exchange_type=&#8221;spot&#8221;, start_timestamp=start, end_timestamp=end, social_services=[], ) print(f&#8221; \u2713 {ex} ok -&gt; {data.data_files}&#8221;, flush=True) return data, ex except Exception as e: last = e print(f&#8221; \u2717 {ex}: {type(e).__name__}: {e}&#8221;, flush=True) raise RuntimeError(f&#8221;no exchange served data; last error: {last}&#8221;) async def backtest(data, params, plot=False, storage=False): run_data = {&#8220;entries&#8221;: None, &#8220;offsets&#8221;: {}, &#8220;plot&#8221;: plot} init_f, strat_f = build_callbacks(params, run_data) res = await obs.run( data, params, strategy_func=strat_f, initialize_func=init_f, enable_logs=False, enable_storage=storage, ) return res, len(run_data[&#8220;entries&#8221;] or ()) async def main(): out = {&#8220;grid&#8221;: [], &#8220;best&#8221;: None, &#8220;oos&#8221;: None, &#8220;errors&#8221;: []} print(&#8220;n&#8221; + &#8220;=&#8221; * 78 + &#8220;n IN-SAMPLE GRID SEARCHn&#8221; + &#8220;=&#8221; * 78, flush=True) is_data, ex_used = await load_data(CFG[&#8220;in_sample&#8221;]) out[&#8220;exchange&#8221;] = ex_used keys = list(CFG[&#8220;grid&#8221;].keys()) combos = [dict(zip(keys, v)) for v in itertools.product(*CFG[&#8220;grid&#8221;].values())] print(f&#8221; {len(combos)} configurations to evaluaten&#8221;, flush=True) for i, params in enumerate(combos, 1): try: res, n_sig = await backtest(is_data, params) m = metrics(res) m.update(params); m[&#8220;signals&#8221;] = n_sig m[&#8220;edge&#8221;] = m[&#8220;profitability&#8221;] &#8211; m[&#8220;market&#8221;] out[&#8220;grid&#8221;].append(m) print(f&#8221; [{i:&gt;2}\/{len(combos)}] {params} &#8221; f&#8221;P&amp;L {m[&#8216;profitability&#8217;]:+.2f}% vs market {m[&#8216;market&#8217;]:+.2f}%<\/p>","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"pmpro_default_level":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"_pvb_checkbox_block_on_post":false,"footnotes":""},"categories":[52,5,7,1],"tags":[],"class_list":["post-110680","post","type-post","status-publish","format-standard","hentry","category-ai-club","category-committee","category-news","category-uncategorized","pmpro-has-access"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v25.3 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis - YouZum<\/title>\n<meta name=\"description\" content=\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/youzum.net\/fr\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\" \/>\n<meta property=\"og:locale\" content=\"fr_FR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis - YouZum\" \/>\n<meta property=\"og:description\" content=\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\" \/>\n<meta property=\"og:url\" content=\"https:\/\/youzum.net\/fr\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\" \/>\n<meta property=\"og:site_name\" content=\"YouZum\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/DroneAssociationTH\/\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-11T20:51:29+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\" \/>\n<meta name=\"author\" content=\"admin NU\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u00c9crit par\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin NU\" \/>\n\t<meta name=\"twitter:label2\" content=\"Dur\u00e9e de lecture estim\u00e9e\" \/>\n\t<meta name=\"twitter:data2\" content=\"16 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\"},\"author\":{\"name\":\"admin NU\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c\"},\"headline\":\"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis\",\"datePublished\":\"2026-08-11T20:51:29+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\"},\"wordCount\":815,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\"},\"image\":{\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\",\"articleSection\":[\"AI\",\"Committee\",\"News\",\"Uncategorized\"],\"inLanguage\":\"fr-FR\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\",\"url\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\",\"name\":\"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis - YouZum\",\"isPartOf\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\",\"datePublished\":\"2026-08-11T20:51:29+00:00\",\"description\":\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\",\"breadcrumb\":{\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#breadcrumb\"},\"inLanguage\":\"fr-FR\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage\",\"url\":\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\",\"contentUrl\":\"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/youzum.net\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/yousum.gpucore.co\/#website\",\"url\":\"https:\/\/yousum.gpucore.co\/\",\"name\":\"YouSum\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/yousum.gpucore.co\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"fr-FR\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\",\"name\":\"Drone Association Thailand\",\"url\":\"https:\/\/yousum.gpucore.co\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png\",\"contentUrl\":\"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png\",\"width\":300,\"height\":300,\"caption\":\"Drone Association Thailand\"},\"image\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/DroneAssociationTH\/\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c\",\"name\":\"admin NU\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fr-FR\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png\",\"contentUrl\":\"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png\",\"caption\":\"admin NU\"},\"url\":\"https:\/\/youzum.net\/fr\/members\/adminnu\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis - YouZum","description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/youzum.net\/fr\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/","og_locale":"fr_FR","og_type":"article","og_title":"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis - YouZum","og_description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","og_url":"https:\/\/youzum.net\/fr\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/","og_site_name":"YouZum","article_publisher":"https:\/\/www.facebook.com\/DroneAssociationTH\/","article_published_time":"2026-08-11T20:51:29+00:00","og_image":[{"url":"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png","type":"","width":"","height":""}],"author":"admin NU","twitter_card":"summary_large_image","twitter_misc":{"\u00c9crit par":"admin NU","Dur\u00e9e de lecture estim\u00e9e":"16 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#article","isPartOf":{"@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/"},"author":{"name":"admin NU","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c"},"headline":"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis","datePublished":"2026-08-11T20:51:29+00:00","mainEntityOfPage":{"@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/"},"wordCount":815,"commentCount":0,"publisher":{"@id":"https:\/\/yousum.gpucore.co\/#organization"},"image":{"@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage"},"thumbnailUrl":"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png","articleSection":["AI","Committee","News","Uncategorized"],"inLanguage":"fr-FR","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/","url":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/","name":"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis - YouZum","isPartOf":{"@id":"https:\/\/yousum.gpucore.co\/#website"},"primaryImageOfPage":{"@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage"},"image":{"@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage"},"thumbnailUrl":"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png","datePublished":"2026-08-11T20:51:29+00:00","description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","breadcrumb":{"@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#breadcrumb"},"inLanguage":"fr-FR","potentialAction":[{"@type":"ReadAction","target":["https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/"]}]},{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#primaryimage","url":"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png","contentUrl":"https:\/\/s.w.org\/images\/core\/emoji\/17.0.2\/72x72\/2705.png"},{"@type":"BreadcrumbList","@id":"https:\/\/youzum.net\/building-and-validating-a-quantitative-trading-strategy-with-octobot-walk-forward-backtesting-parameter-optimization-and-interactive-analysis\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/youzum.net\/"},{"@type":"ListItem","position":2,"name":"Building and Validating a Quantitative Trading Strategy with OctoBot, Walk-Forward Backtesting, Parameter Optimization, and Interactive Analysis"}]},{"@type":"WebSite","@id":"https:\/\/yousum.gpucore.co\/#website","url":"https:\/\/yousum.gpucore.co\/","name":"YouSum","description":"","publisher":{"@id":"https:\/\/yousum.gpucore.co\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/yousum.gpucore.co\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"fr-FR"},{"@type":"Organization","@id":"https:\/\/yousum.gpucore.co\/#organization","name":"Drone Association Thailand","url":"https:\/\/yousum.gpucore.co\/","logo":{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/","url":"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png","contentUrl":"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png","width":300,"height":300,"caption":"Drone Association Thailand"},"image":{"@id":"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/DroneAssociationTH\/"]},{"@type":"Person","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c","name":"admin NU","image":{"@type":"ImageObject","inLanguage":"fr-FR","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/image\/","url":"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png","contentUrl":"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png","caption":"admin NU"},"url":"https:\/\/youzum.net\/fr\/members\/adminnu\/"}]}},"rttpg_featured_image_url":null,"rttpg_author":{"display_name":"admin NU","author_link":"https:\/\/youzum.net\/fr\/members\/adminnu\/"},"rttpg_comment":0,"rttpg_category":"<a href=\"https:\/\/youzum.net\/fr\/category\/ai-club\/\" rel=\"category tag\">AI<\/a> <a href=\"https:\/\/youzum.net\/fr\/category\/committee\/\" rel=\"category tag\">Committee<\/a> <a href=\"https:\/\/youzum.net\/fr\/category\/news\/\" rel=\"category tag\">News<\/a> <a href=\"https:\/\/youzum.net\/fr\/category\/uncategorized\/\" rel=\"category tag\">Uncategorized<\/a>","rttpg_excerpt":"In this tutorial, we build a complete quantitative backtesting workflow with OctoBot and OctoBot-Script while keeping the environment isolated from Colab\u2019s preinstalled dependencies. We configure a rule-based trading strategy that combines RSI-based oversold signals, EMA trend confirmation, and ATR-driven adaptive stop-loss and take-profit levels, and we execute it through OctoBot\u2019s native market-order and backtesting APIs.\u2026","_links":{"self":[{"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/posts\/110680","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/comments?post=110680"}],"version-history":[{"count":0,"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/posts\/110680\/revisions"}],"wp:attachment":[{"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/media?parent=110680"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/categories?post=110680"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/youzum.net\/fr\/wp-json\/wp\/v2\/tags?post=110680"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}