Skip to content

Repository files navigation

Half-Wick — Intraday Candle Range Wick Fill Strategy

Abstract

The strategy trades an instrument on an intraday chart (here, SPY on a 30-minute chart). When a green candle closes with an upper wick around 50% of its range and almost no lower wick — or the mirror, a red candle with a lower wick around 50% of its range and almost no upper wick — it enters in the direction of the dominant wick, targeting a retest of its extreme and stopping out at the candle's opposite extreme.

At most one trade is held at a time, at least 3 hours of session remains after the last entry, and any open position is closed at the day's close price regardless of whether target or stop has been reached. Developed and evaluated on roughly five years of 30-minute and 1-minute SPY bars from Interactive Brokers, following the 9-step Development Process and the Plutus Reproducibility Standard.

Full pipeline result: the original, un-tuned hypothesis (dominant wick 40–60%, opposite wick ≤20%) is weak in-sample (Sharpe 0.19, 51.4% win rate, +1.12% total return, mean R -0.02 ± 0.05). The optimization finds a higher-scoring candidate (dominant wick 45–55%, opposite wick ≤10%) that scores well in-sample (Sharpe 0.98, 57.4% win rate, +3.11% total return, mean R +0.12 ± 0.09) and falls back out-of-sample (Sharpe 0.36, 51.7% win rate, +0.63% total return, mean R +0.01 ± 0.13).

1. Forming Algorithm Hypothesis

Observation and Hypothesis: Price frequently revisits levels it has already touched — call this overlap, a byproduct of market noise. This strategy targets candles (or parts of candles) most likely to be overlapped again: wicks rather than solid candle bodies. The wick needs to be long enough for a reasonable risk-reward ratio, but not so long that the candle starts to resemble a classic reversal pattern (inverted hammer or hanging man), which would raise the odds of getting stopped out — roughly 50% of the candle's range looks like the right target. The wick also has to sit on the same side as the candle's body: upper wick with a green body, lower wick with a red body. Read together, the wick signals indecision while the body's color signals a push in one direction. The hypothesis is that the push wins, and the dominant wick's extreme gets revisited (overlapped) by a later candle before the opposite wick's extreme gets revisited. The opposite wick has to stay short, since a long one (plus an already-long dominant wick) would undercut the candle's "greenness" or "redness," leaving the next move closer to a coin flip. Finally, this is tested on intraday candles rather than daily ones because intraday price moves continuously, without the gaps a daily chart has between sessions.

I start with a dominant wick that is between 40% and 60% of the range of the candle and an opposite wick that is at most 20% of the range of the candle. These values will be optimized on the in-sample data.

2. Data Preparation

  • Source: Interactive Brokers (reqHistoricalData via ib_async), SPY on SMART routing.
  • Bars: 30-minute (signal generation) and 1-minute (intrabar fill-order resolution — determining whether a trade's stop or target was touched first).
  • Period: roughly five years, 2021-08-09 → 2026-08-06 (regular trading hours only). In-sample is the first three years (2021-08-09 → 2024-08-06), out-of-sample the most recent two (2024-08-07 → 2026-08-06).

Obtaining the data

# Requires IB Gateway or TWS running, logged in, and API access enabled
uv run --with ib_async python scripts/fetch_ibkr_data.py

Takes 3.5–4+ hours. The data is saved to data/spy_30min.csv and data/spy_1min.csv. scripts/fetch_ibkr_data.py uses fixed start/end dates to ensure reproducibility.

3. Forming Set of Rules

For a candle with open $O$, high $H$, low $L$, close $C$, and range $R = H - L$:

Long setup (green candle, $C > O$):

$$0.40 \le \frac{H - C}{R} \le 0.60 \qquad \text{and} \qquad \frac{O - L}{R} \le 0.20$$

Entry at $C$, target $H$, stop $L$.

Long setup: a green candle with a dominant upper wick, target at the high, stop at the low

Short setup (red candle, $C < O$) — the exact mirror:

$$0.40 \le \frac{C - L}{R} \le 0.60 \qquad \text{and} \qquad \frac{H - O}{R} \le 0.20$$

Entry at $C$, target $L$, stop $H$.

Short setup: a red candle with a dominant lower wick, target at the low, stop at the high

  • Entry: wick-ratio pattern above, on a closed 30-minute candle; enter at that candle's close.
  • Exit: target = the signal candle's extreme in the trade direction; stop = its opposite extreme. Whichever is touched first, resolved using 1-minute bars walked forward from entry.
  • Timing: an entry must leave at least three hours of session ahead of it, so the trade has room to reach target or stop (for a regular 4pm ET close, 1pm — the close of the 12:30–1:00 bar — is the last possible entry; for an early 1pm ET close, 10am — the close of the 9:30–10:00 bar — is the last possible entry). Any open position is force-closed at the day's close price, so no positions are held overnight.
  • Concurrency: at most one open position at a time. A second (or third) trade the same day is fine once the prior one has exited, as long as it still leaves the three-hour entry runway.
  • Sizing: 100% of current equity, compounded (every trade uses the whole account, win or lose). Shares per trade: shares = floor(equity / entry_price). Initial capital is $1,000,000 (INITIAL_CAPITAL in steps/_shared.py) — see Capital Sensitivity below for how results vary with capital.
  • Costs: commission $0.005/share, $1.00 minimum, charged on both the entry and exit fill. A fixed $0.01/share slippage (small, since SPY is liquid) applies only to stop fills — not to target fills (a limit order) or end-of-day forced closes.

Evaluation Metrics

Sharpe ratio (daily-aggregated, annualized over 252 days), Sortino ratio, win rate, total return, CAGR, maximum drawdown, and mean R (expectancy per unit of risk, stop-out is -1R before costs). Risk-free rate of 0% is used in the Sharpe and Sortino ratios to isolate the strategy's own performance. The distribution of exits by reason (target / stop / forced end-of-day) is also tracked.

Implementation & Reproducibility

The pipeline is packaged as half_wick. The pattern detector is half_wick.intraday.patterns. The backtest engine, half_wick.intraday.backtest, is the shared mechanism behind the in_sample, optimization, and out_of_sample steps.

Environment setup

uv sync     # create the env from the committed lockfile

Dependencies are pinned by uv.lock; .python-version pins the interpreter to 3.11. This project declares no secrets — the IBKR data pull runs once by hand, outside the reproducible pipeline (see Data Preparation above), so there's no .env.

Reproducibility

This repo ships a .plutus/manifest.yaml declaring the environment, data requirements, steps, and expected metrics. Reproduce results in an isolated Docker container with plutus-verify v0.5.1:

uv venv .plutus-venv && source .plutus-venv/bin/activate
uv pip install "plutus-verify[runner] @ https://github.com/algotrade-plutus/plutus-verify/releases/download/v0.5.1/plutus_verify-0.5.1-py3-none-any.whl"

plutus check .

Exit code 0 = reproduced, 1 = partial, 2 = failed.

4. In-sample Backtesting

Baseline run of the strategy as originally specified (dominant-wick range 40–60%, opposite ≤20%) before optimization.

uv run python -m half_wick.steps.in_sample

In-sample result (2021-08-09 → 2024-08-06, before optimization)

Metric Value
Sharpe Ratio 0.190
Sortino Ratio 0.278
Win Rate 51.4%
Total Return +1.12%
CAGR +0.37%
Max Drawdown −3.11%
Mean R −0.019 ± 0.049
Trades 418
Target Exits 215
Stop Exits 203
EOD Exits 0

5. Optimization

The search covers three parameters — the dominant-wick range's lower bound, upper bound, and the opposite-wick cap — searched via Optuna's BruteForceSampler on the Sharpe ratio. Range bounds run from 40% to 60% in 5% steps, with each range at least 10% wide (40–50, 40–55, 40–60, 45–55, 45–60, 50–60), crossed with opposite-wick caps of 0, 5, 10, 15 and 20% — 30 combinations in all. Sizing and the mechanical rules (one trade at a time, EOD close, at least 3 hours of session after last entry) stay fixed, not searched. Any combination producing fewer than 100 in-sample trades is penalized regardless of its Sharpe, to keep the strategy usable — a rule that fires a handful of times a year can't be paper-traded.

uv run python -m half_wick.steps.optimization

Optimization result (in-sample, 2021-08-09 → 2024-08-06)

Metric Value
Combinations Evaluated 30 (11 penalized due to firing less than 100 times across the 3-year in-sample window)
Best Dominant-wick Range 45%–55%
Best Opposite-wick Cap ≤10%
Sharpe Ratio 0.98
Sortino Ratio 1.57
Win Rate 57.4%
Total Return +3.11%
CAGR +1.03%
Max Drawdown −1.89%
Mean R +0.120 ± 0.094
Trades 115
Target Exits 66
Stop Exits 49
EOD Exits 0

Winning parameters are also written to parameter/optimized_params.json.

In-sample equity curve and drawdown: original hypothesis vs. optimized parameters, 2021-08-09 to 2024-08-06

This chart and Section 6's can be redrawn by uv run python scripts/make_figures.py after the pipeline.

6. Out-of-sample Backtesting

uv run python -m half_wick.steps.out_of_sample

Out-of-sample result (2024-08-07 → 2026-08-06, using optimized parameters)

Metric Value
Sharpe Ratio 0.36
Sortino Ratio 0.57
Win Rate 51.7%
Total Return +0.63%
CAGR +0.31%
Max Drawdown −1.32%
Mean R +0.008 ± 0.133
Trades 58
Target Exits 30
Stop Exits 28
EOD Exits 0

Out-of-sample equity curve and drawdown: optimized parameters, 2024-08-07 to 2026-08-06

Summary

Run Sharpe Total Return Mean R Period Ending Balance Period Ending Balance Using 3.5%/yr Benchmark
In-sample (original hypothesis) 0.19 +1.12% −0.019 ± 0.049 $1,011,196 $1,108,264 (+10.83%)
Optimization (best in-sample) 0.98 +3.11% +0.120 ± 0.094 $1,031,085 $1,108,264 (+10.83%)
Out-of-sample (optimized) 0.36 +0.63% +0.008 ± 0.133 $1,006,268 $1,070,786 (+7.08%)

Ending balances are computed over the row's own actual period (3 years for the two in-sample rows, 2 years for out-of-sample), with the benchmark compounding at 3.5%/yr over that same span.

Capital Sensitivity

This section is a standalone side analysis — it is not part of the Plutus pipeline. The table below can be regenerated with uv run python scripts/capital_sensitivity.py after running the pipeline.

Disregarding commission and slippage, the strategy would perform identically at any capital level — capital would be irrelevant. This backtest charges commission at $0.005/share with a $1.00 minimum per fill, approximating IBKR Pro's Fixed commission schedule. (Slippage of $0.01/share is also simulated, but only on stop exits, and its effect on capital is much smaller than the $1 minimum commission's.) Buying 200 shares of SPY pays the same $1 minimum commission as buying 2 shares — below 200 shares, the $1 minimum is the binding cost regardless of trade size — so commission eats a larger fraction of a smaller account's returns. A small account therefore needs a stronger edge just to break even.

The table below shows this strategy's metrics with the optimized parameters at different starting capital levels, over the full 5-year period (in-sample and out-of-sample combined). Total return crosses zero somewhere around $8,700. Metrics stabilize by roughly $500,000.

Initial capital Sharpe Sortino CAGR Total return over 5 years Max DD
$5,000,000 0.7592 1.2000 0.74% 3.76% -1.89%
$2,000,000 0.7592 1.2000 0.74% 3.76% -1.89%
$1,000,000 0.7593 1.2001 0.74% 3.76% -1.89%
$500,000 0.7593 1.2001 0.74% 3.75% -1.89%
$100,000 0.7537 1.1899 0.74% 3.72% -1.89%
$25,000 0.5513 0.8394 0.53% 2.67% -1.96%
$8,700 (total-return crossover zone) 0.0057 0.0079 0.00% 0.00% -2.16%
$5,000 -0.6491 -0.8292 -0.61% -3.00% -4.18%

Reference

[1] ALGOTRADE, The 9-Step Development Process. Online: https://www.algotrade.vn/knowledge/9-step-process

[2] ALGOTRADE, The Plutus Reproducibility Standard. Online: https://github.com/algotrade-plutus/plutus-guideline

[3] ALGOTRADE, 43. Return Rate. Online: https://hub.algotrade.vn/knowledge-hub/evaluation-of-algorithmic-performance/

[4] ALGOTRADE, Sortino Ratio. Online: https://hub.algotrade.vn/knowledge-hub/sortino-ratio/

[5] ALGOTRADE, 44. Maximum Drawdown in Algorithmic Trading. Online: https://hub.algotrade.vn/knowledge-hub/maximum-drawdown-in-algorithmic-trading/

About

Intraday trading strategy, backtested on 5 years of SPY 30-minute and 1-minute bars, with parameter search and out-of-sample test, following the Plutus Reproducibility Standard.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages