# Strategy Workbench

A complete local research dashboard: TradingView Lightweight Charts 5.2.1,
VectorBT PRO, pandas/NumPy, Python's HTTP server, and plain JavaScript.
No React, Node server, cloud service, broker connection, or model API is required.

## Run

Use a Python environment with a licensed VectorBT PRO installation. Tested with
Python 3.11.8, VectorBT PRO 2026.10.5, pandas 2.3.3, and NumPy 2.4.4.
Follow the official VectorBT PRO installation instructions for the private package.
The requirements file pins the public numerical packages used for verification.

```bash
python app.py
```

Open http://127.0.0.1:8765 after the terminal prints `Ready`.
First-use compilation happens before the server starts. It can take substantially
longer on another machine. Keep `dashboard.html.txt`, `data_sources.py` and `vendor/` next to `app.py`.
The chart library is bundled, so there is no CDN request. Synthetic or CSV
research runs offline. Downloading historical data requires provider access.
The `.html.txt` source extension lets this example be distributed by the static
publication without indexing it as a second article. The Python app serves it as HTML.

## Try the workflow

1. Drag lookback, Bollinger width or Keltner width, or type exact values beside them.
   Bands, compression shading, momentum, fills, portfolio metrics and trades update.
2. Open Costs and capital, then raise the fee and slippage inputs. These are basis points per side, not percent.
3. Change the evaluation window. Each run starts flat, using earlier loaded bars
   only for indicator history.
4. Inspect a closed trade to zoom the price chart around its entry and exit.
5. Open Parameter map and run 165 lookback/Keltner combinations. Bollinger width
   stays fixed at the displayed value. Click a cell to apply its settings. Changing
   Bollinger width, data, costs or allocation invalidates the map.
6. Export all closed trades to CSV. The table shows only the latest 100.

The compact toolbar keeps the dataset, window and squeeze controls visible. Costs and
allocation open on demand. The chart header links to VectorBT PRO as the engine
used to build the app.

The synthetic demo data is not a real ticker and is not a performance claim.
Both demo datasets contain 10,000 hourly bars. The app labels them explicitly.

## Use your own data

```bash
python app.py --csv /path/to/prices.csv --name "BTCUSD / 1h" --port 8765
```

CSV columns: `timestamp,Open,High,Low,Close`. `Volume` is optional and validated
if supplied. Include between 250 and 50,000 sorted, unique, completed bars.
Prices must be positive, finite, and consistent with the high/low range.
The parser treats naive timestamps as UTC and converts aware timestamps to UTC.
It rejects bad rows instead of sorting, deduplicating, filling, or repairing them.
No data is uploaded to an external service.

Use conventional OHLC rather than synthetic chart types. Use consistent session,
corporate-action and account-currency conventions. The CSV loader does not adjust prices,
convert currencies, account for dividends separately, charge funding or borrowing,
model volume/queues, or certify provider data. Gaps are retained as supplied.

## Strategy and accounting

The default lookback is 20, Bollinger width 2 and Keltner width 1.5. All squeeze components
use the shared lookback. The optional RSI filter uses its own fixed 14-bar window. VBT's BBANDS supplies a simple close average and population
standard deviation (ddof=0). Keltner uses VBT's close EMA and Wilder ATR, both with
adjust=False. True range is missing on the first bar because it needs a previous
close. Compression requires both Bollinger boundaries strictly inside Keltner.

Momentum uses VBT's rolling high/low extrema and OLS. Detrended price is close
minus the average of the rolling high/low midpoint and close SMA. Momentum is the
rolling fitted line evaluated at the current bar, first available after
2 * lookback - 1 bars. Histogram colors distinguish direction and acceleration.
Dark zero-line dots indicate compression, ochre release, gray other valid bars.
This specified variant does not claim exact TradingView or LazyBear script parity.

Enter long on a compression release with positive momentum, requiring valid
current and prior momentum. Exit when momentum crosses from positive to zero or
negative. Signals use completed closes and fill at the next available row's open.
Missing market-closed rows should already be absent. A final-row signal remains unfilled. No shorting,
leverage, stops, pyramiding, or partial-fill model is included.

Starting cash is 10,000 account-currency units. Entry size is the chosen fraction
of available cash, including fees. Slippage worsens buy and sell prices. Open
positions remain marked at the final close. Return and drawdown include them.
Win rate and profit factor use closed trades. An undefined profit factor is `n/a`.

The passive benchmark buys at the first evaluation open using the same allocation,
fee and slippage settings, then holds without forced final liquidation.

The price chart retains every bar and actual fill marker. Account-value lines are
sampled for display at up to 1,400 regular observations plus the worst-drawdown row.
All metrics and trades use full-resolution engine results.

## Historical data

Open Historical market data. Choose Yahoo Finance, BTC-USD, 1d, start 2018-01-01,
and exclusive end 2026-01-01. Download prices once, then reuse the saved rows for
all parameter changes. Select another provider to see its symbol format and access
requirements. Supported adapters are YFData, BinanceData, CCXTData (Binance, Kraken,
Coinbase) and AlpacaData (IEX or entitled SIP stock bars).

Install only the optional provider packages you use:

```bash
pip install yfinance
pip install python-binance
pip install ccxt
pip install alpaca-py
```

Yahoo uses adjusted OHLC and regular sessions. Alpaca uses split and dividend
adjustment. Binance and CCXT use exchange spot OHLCV. These are different data
sources. Provider history, symbol availability, region and entitlement limits apply.
Yahoo intraday history is limited, so the worked multi-year example uses daily bars.
The app accepts at most 50,000 possible bars per request and validates returned OHLCV.
Bars whose opening timestamp plus interval exceeds the end boundary are excluded.
This conservative check may also exclude a completed exchange-time daily stock bar.
The source shows returned dates and excluded counts. Gaps are not filled.

Keys are used only for the selected download and cleared from the form afterward.
The server retains plain prices and public provenance, not VBT Data objects or keys.
Four remote datasets stay in memory until restart. Failed downloads preserve the
previous selection. No data is streamed and no broker orders are submitted.

## RSI extension and fixed-split comparison

Enable RSI(14) entry filter to require Wilder RSI strictly above its threshold on
the completed signal bar. Exits and next-open fills remain unchanged. RSI blocked
circles are rejected candidate entry signals, not simulated fills. The parameter map
holds the filter and threshold fixed. The run summary states the applied rule.

Set Evaluation split to 2024-01-01 and click Compare original vs RSI filter. Both
rules run before and from the split with identical costs and squeeze parameters.
Both sides start flat. Earlier bars warm indicators. Positions do not cross the
boundary, and open positions at each period's end remain marked to market. The
split requires at least 100 loaded bars on each side. Repeatedly selecting settings
against the later period invalidates any claim that it remains untouched.

Reproduce the article's fixed historical comparison:

```bash
python historical_study.py --save-csv /path/to/private/btc-daily.csv
python historical_study.py --csv /path/to/private/btc-daily.csv
```

The script prints settings, data fingerprint, four results and the comparison
method. Its threshold was fixed before inspecting the recorded comparison. The
RSI filter improved the earlier result and worsened the later one. The recorded
results are in historical-study.json. This is a retrospective demonstration with
few trades, not a prospectively validated strategy. Raw provider prices are not
redistributed in the example archive.

## Training-region selection and later checks

The article's stronger setup is lookback 15, Bollinger width 2, Keltner width 3.25,
RSI off, fee 5 bps, slippage 2 bps and allocation 95%. It was selected only on
2018-2023 BTC-USD daily prices, using the same 165 lookback/Keltner combinations
as the dashboard. The source snapshot and selection objective were fixed before
its later-period evaluation. Original/RSI results for 2024-2025 had already been
inspected, so that period is a retrospective comparison. The 2026 prices were
first downloaded after freezing the new choice.

An eligible center has positive training return, at least ten closed trades and a
complete 3x3 neighborhood. Rank centers by the median training Calmar of their nine
cells. Calmar here is geometric annualized return divided by maximum drawdown,
using 365.25 calendar days per year. Zero-drawdown paths get score zero. Require a
positive neighborhood median. Ties favor higher center Calmar, lower lookback,
then lower Keltner width. No test-period results enter the ranking.

The selected center had 25 closed training trades, +591.02% return and 29.66%
maximum drawdown. On 2024-2025 it returned +83.70%, with 15.92% drawdown and six
closed trades. On January-October 2026 it returned +1.46%, with 8.31% drawdown and
two closed trades. The matched buy-and-hold returns were +101.49% and -10.99%.
The earlier selection did not beat the earlier passive return. Few later trades
limit conclusions about repeatability.

All nine frozen neighbors were positive in 2024-2025. Six were positive in 2026,
with returns ranging from -3.21% to +3.13%. The chosen setup also remained positive
at fee 10 bps and slippage 5 bps: +82.03% and +1.15%. These are the additional cost
checks actually run, not a modeled spread/impact or capacity study.

Each period starts flat with 10,000 cash and uses earlier history for indicators.
No position crosses a period boundary. Open positions remain marked to market.
All 26 baseline, RSI, selected, neighbor and higher-cost evaluations, plus the full
165-cell training grid, are in parameter-study.json. No later winner replaces the
frozen choice, and no prospective or statistical-significance claim is made.

Reproduce the study in two explicit phases. First obtain the original daily
snapshot with the existing script, then select and freeze settings before fetching
later data:

```bash
python historical_study.py --save-csv /path/to/private/btc-through-2025.csv
python parameter_study.py select --csv /path/to/private/btc-through-2025.csv --output selection.json
python parameter_study.py evaluate --selection selection.json --save-csv /path/to/private/btc-through-october-2026.csv --output study.json
```

To repeat without another download, pass the extended CSV with `--csv` to the
evaluate command. Use a new output path because the script refuses to overwrite an
existing research record. Provider revisions to training rows cause evaluation to
stop rather than silently choose new parameters. Save the price CSV privately.

For a browser check of 2024-2025, load BTC-USD 1d with start 2018-01-01 and exclusive
end 2026-01-01, enter the selected inputs, set split 2024-01-01 and choose From split
date. For the later check, load exclusive end 2026-11-01, set split 2026-01-01, and
choose From split date again, and keep exactly the same strategy and cost inputs.
Each new download resets Test window to All loaded bars. The app retains downloaded datasets
in its Dataset selector, so you can switch between the two snapshots.

## Responsiveness

Indicator features are reused by dataset/lookback in a bounded 32-entry cache.
Uncached lookbacks compute their features first. Every parameter update runs a fresh
VBT PRO simulation. A Lightweight Charts primitive draws the bands and compression
shading using visible coordinates, above a synchronized momentum pane. The frontend
reuses chart instances and market data. A 40 ms coalescing window starts the first
request. During a drag, one request runs at a time and the next uses the newest
inputs. Completed parameter previews update the charts while the pointer is held.
The run summary labels the settings actually displayed. Responses for a different
dataset, evaluation window, or cost configuration are discarded. Releasing the
slider leaves the final requested settings applied. A parameter map is a separate batch calculation, not a cache of
predetermined outcomes used to pretend that input changes were backtested. The map
uses the same simulation in chunks of at most 48 configurations to bound memory.
It holds Bollinger width fixed rather than selecting a hidden best value per cell.

Displayed simulation time includes portfolio construction and account-value
calculation. Response time includes the request and JSON transfer. Update time also
includes updating chart data and the trade table, but does not measure canvas paint.
These are different measurements. First-load data transfer and engine compilation
are not warm interaction times. Exact latency depends on hardware, backend,
browser, bar count, trade count, and concurrent work.

### Measured workload

On an Apple M3 Mac, 20 warmed requests over the 10,000-bar synthetic series gave
these medians after the provider and RSI features were added:

| Measurement | Milliseconds |
| --- | ---: |
| Portfolio simulation and account value | 11.2 |
| Server computation and response preparation | 55.5 |
| HTTP request through decoded JSON | 78.9 |
| One 165-configuration parameter map, server computation | 213.1 |

The request measurements exclude input scheduling, browser rendering, initial data
delivery, network provider downloads and startup. The map measurement is one run.
The environment was Python 3.11.8, VectorBT PRO 2026.10.5, pandas 2.3.3, NumPy 2.4.4,
and a development build of the Rust extension. No matched TradingView comparison
or native release-build benchmark was performed. Raw samples are in timings.json.

## Verify

```bash
python -m unittest -v test_app.py test_parameter_study.py
```

With the app already running on its default port, measure 20 fresh HTTP requests:

```bash
python benchmark.py
```

Tests reconcile cash, positions, order timing, fees and slippage against independent
arithmetic. They also check warmup, causal prefixes, input rejection, cost sensitivity,
and parity between parameter-map cells and individual simulations.

The complete suite has 20 checks. Live public downloads were exercised through Yahoo
Finance, BinanceData and CCXTData on Binance. Alpaca mapping and credential handling
were tested without live account keys.

Exploring the map exposes the same data repeatedly. A good cell is not evidence of
future profitability. Reserve an untouched period outside this exploration.

## Files and licensing

`app.py` is the server and research engine integration. `data_sources.py` contains
the provider adapters. `historical_study.py` reproduces the fixed comparison and
`historical-study.json` records that comparison. `parameter_study.py` and
`parameter-study.json` contain the frozen training-region search and later checks.
`test_parameter_study.py` verifies selection boundaries, ranking and metric parity.
`dashboard.html.txt` is the complete interface, styles and browser code.
`test_app.py` contains the verification suite and `benchmark.py` measures latency.
The publication's example source
may be copied and adapted under the MIT license in `LICENSE`.
VectorBT PRO remains a separately licensed dependency and is not redistributed.

The bundled official Lightweight Charts file is Apache-2.0. Preserve its source
banner, `vendor/LICENSE`, `vendor/NOTICE`, and visible TradingView attribution
when adapting the app. Its version is 5.2.1, downloaded from the official npm
registry. No other chart library is used.
