Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

trading-cli — IHSG Minervini Scanner + Swing Agent

Pure CLI. No web, no API keys. Scans ~900 IDX/IHSG tickers daily, prints what to enter tomorrow morning for swing trading (1-20 days) with realistic IDX costs. Every output is machine-readable for agents.

Agent TL;DR: Run trading-cli --period 1y --json each morning (WIB), parse picks[], pick the stock where best_strat has expectancy>0 and wilson_low>0 and regime_skip==null. For 70% winrate +5% hunt, filter scalp_*_5 with winrate>=60 and pooled n>=30 — if empty, report no trade (honest). See Agent Workflow below.

Goals

  • Swing entry daily: Rank Stage-2 leaders by Minervini Trend Template (8) + VCP contraction — so agent enters only healthy uptrends, not chop.
  • 70% winrate hunt with 5% profit / 5% loss in 1-2 days: Scalp sleeve (scalp_*_5 TP 5% SL 5% Time 2) — agent trades only when pooled winrate ≥60-70% and Wilson≥55% with ≥30 trades; otherwise sits out (no forced trade).
  • Expectancy over winrate: Leaderboard ranked by expectancy p*avgWin + (1-p)*avgLoss and Wilson 95% CI, not raw winrate — so agent avoids 70% win with 0.5R losers that lose money.
  • Walk-forward honest: --validate --period 2y proves OOS (train 252 step 60 warmup 100) before tuning. No tuning on 1y.

Honesty — No Proven 70% Win-Rate Sleeve on Daily OHLCV Alone (2026年08月29日)

Current status: There is no proven 70% win-rate sleeve with 5% TP on daily yfinance (OHLCV) alone. Every higher-R and divergence candidate tested has failed both gates: pooled Wilson 95% low ≥55% and 2y walk-forward OOS ≥50% across ≥50 pooled trades.

Tested and rejected (see trading_cli/backtest.py:323 ponytail notes, commits 827ee57, 6dfc88a, 2697a2b):

Setup 2y pooled n=200 2y WF OOS 20 LQ45 Verdict
hr_orb (Opening-Range Breakout, R1.67) 32.9% n=33 Wilson 0.0 exp +0.69 12.5% (2/16) Too strict, too rare
hr_pdl (Prior-Day Low Reversal, R1.67) 50.8% n=111 Wilson 4.6 exp +1.09 12.5% (2/16) Best in-sample, collapses OOS
hr_rev2d (2-Day Reversal, R1.67) 24.2% n=44 Wilson 0.0 exp -0.40 12.5% (2/16) Negative expectancy
div_bull (RSI bullish divergence + vol>1.2x, R1.67) 0.0% n=4 Wilson 0.0 exp -0.76 0% (0/0) — too rare to measure Signal too rare
vcp_cmf (VCP + CMF rising 3d, R1.67) 20.0% n=6 Wilson 0.0 exp +0.45 25.0% (3/12) CMF slope not lifting

Baseline scalp_ema_5 OOS 33–60% pooled; no existing 18 strats clear Wilson≥55%. The 70% hunt on daily price alone caps at ~50% pooled — real ceiling.

Agent rule until 70% is proven: Treat 70% sleeve as empty. Report "no 70% trade today" when leaderboard has no scalp_*_5/hr_*/div_bull/vcp_cmf with pooled_trades≥50 and wilson_low≥55 and OOS ≥50%. Rank by expectancy (★ BEST) not winrate. Stage-2 swing (e.g. vcp_breakout 25% win +5% exp pooled) is the honest edge today.

Scanner still ships: div_bull always-on field on every scan result (human table ✓ column + HOW TO ENTER ✓ BULL DIV). Scanner-only, no STRATS entry — rare signal stays visible even though measured strat was too rare.

What unlocks 70% (research 6.6p, research-idx-70pct-2026年08月29日.md): Actor filter — broker concentration (1–3 buyers, BK/KZ/RX) or foreign net-buy streak (3d, LQ45). Requires idx.co.id scrape (fragile) — behind ml_filter.gate_pooled_n(need=100) walk-forward gate. Not built until price-only ceiling is declared.

Install

cd ~/Projects/trading-cli
~/Projects/trading-suite/backend/.venv/bin/pip install -e .
ln -sf ~/Projects/trading-suite/backend/.venv/bin/trading-cli ~/.local/bin/trading-cli
trading-cli --help

Requires Python 3.11+, yfinance>=0.2.40, pandas>=2.0, numpy>=1.24.

Quick start

trading-cli # full 900, ~90s cold / ~20s cached
trading-cli --top 15 # show 15 picks
trading-cli --scan-only # no backtests, ~3s cached
trading-cli --ticker BBCA # deep dive one ticker
trading-cli --limit 50 --scan-only # debug 50
trading-cli --workers 4 # lower if 401/429
trading-cli --period 2y --validate # walk-forward OOS for top3
trading-cli --doctor # probe yfinance + cache health
trading-cli --clear-cache # wipe XDG cache
trading-cli --json > picks.json # agent: parse picks[] + leaderboard
python -m trading_cli --top 10 # same
Flag Effect
--top N picks shown (1-100, default 10)
--workers N yfinance concurrency 1-16 (default 8, use 4 on 401)
--period {1mo,3mo,6mo,1y,2y} history window (strict choices)
--scan-only fastest, no backtests
--ticker CODE one ticker (e.g. BBCA, no .JK)
--limit N first N of universe (debug)
--universe-file CSV CSV/TXT tickers (one per line or comma, strips .JK, # comments)
--vcp-tight 6 --vcp-vol 0.7 VCP thresholds (tight% and vol dry-up ratio)
--validate walk-forward OOS top3 (requires --period 2y, train 252 step 60 warmup 100)
--doctor / --clear-cache probe / wipe cache
--json machine JSON (picks + near_miss + leaderboard + wf)
--force bypass cache
--version show version

Agent Workflow (every morning WIB)

Goal for agent: Recommend 0-3 entries for swing 1-20d with 5% TP /5% SL if 70% hunt viable, else best expectancy swing.

# 1. Scan
trading-cli --period 1y --json > /tmp/picks.json
cat /tmp/picks.json | jq '.leaderboard'
cat /tmp/picks.json | jq '.picks[] | {ticker, stage, tmpl: .template_score, vcp: .vcp_score, rs: .rs_pct, best: .best_strat, win: .best_winrate, exp: .best_expectancy, regime: .regime_skip, entry: .entry}'
# 2. Health (fail <5%?)
trading-cli --doctor
# expect: probe 6/6 ok, cache ~25MB WAL, yfinance 1.5.2
# 3. Decision tree (agent must follow)
# a) Qualifiers: picks[] where pass==true or tmpl>=6 && vcp>=30 && regime_skip==null
# b) Rank by leaderboard exp, not winrate. ★ BEST is expectancy leader.
# c) 70% hunt: among picks, look at backtests scalp_*_5 with winrate>=70 && pooled n>=30 && wilson_low>=55
# - If found -> recommend that scalp (5% TP/SL 2d, next-open entry)
# - Else -> recommend best_strat by expectancy (even if 30-50% win) or report "no 70% trade today" (honest)
# d) Risk: entry.buy_zone pivot*1.005-1.02, stop = max(20d low, close-2ATR, close*0.92) capped 8%, R = close-stop, T1 +1R T2 +2R
# e) Never enter if regime_skip=="IHSG Stage4" unless scalp mean-rev and validate OOS passed — demoted -20 combo already

JSON shape for agent:

{
 "scanned": 956, "fail": 3, "rs_n": 953,
 "leaderboard": {
 "vcp_breakout": {"avg_winrate": 14.8, "best": 100, "count": 6, "avg_expectancy": 1.70, "pooled_trades": 6, "wilson_low": 0.0},
 "scalp_ema_5": {"avg_winrate": 47.9, "best": 75, "count": 110, "avg_expectancy": -0.17, "pooled_trades": 110, "wilson_low": 13.8}
 },
 "picks": [{
 "ticker": "ABDA", "close": 3810, "template_score": 8, "stage": "Stage 2 - Uptrend", "vcp_score": 100, "rs_pct": 92,
 "pass": true, "regime_skip": null, "combo": 128, "checks": {...},
 "entry": {"pivot": 4100, "buy_zone": [4120,4182], "stop": 3756, "risk_pct": 1.4, "targets": {"T1":3864,"T2":3919}},
 "backtests": {
 "vcp_breakout": {"trades": [{"regime":"Trending","pct":6.1}], "open": null, "stats": {"total":1,"wins":1,"winrate":100,"avg_win":6.1,"avg_loss":0,"pf":null,"expectancy":6.1,"wilson_low":21,"wilson_high":100}, "regime_stats": {"Trending":{"winrate":100,"total":1,"expectancy":6.1,"wilson_low":21},"Choppy":{"winrate":0,"total":0},"Bear/HighVol":{"winrate":0,"total":0}}},
 "scalp_ema_5": {"stats": {"total":5,"winrate":40,"expectancy":-0.5,"wilson_low":12}}
 },
 "best_strat": "vcp_breakout", "best_winrate": 100, "best_expectancy": 6.1
 }],
 "regime_pooled": {"Trending":{"winrate":52,"total":12,"expectancy":0.6},"Choppy":{"winrate":37,"total":48},"Bear/HighVol":{"winrate":42,"total":52}},
 "wf_regime": {"Trending":{"winrate":42,"total":12},"Choppy":{"winrate":37,"total":40}}
 "near_miss": [{... "WAIT" ...}]
}

Agent rules:

  • pooled n<30 or wilson_low==0unreliable, do not size on winrate alone.
  • expectancy <0negative edge, skip even if winrate 35%.
  • 70% HUNT block lists only scalp_*_5 with win>=60; if empty that day, agent must output no 70% trade today not force.
  • Always next-open fill: fill = open*(1+SLIP) with SLIP 0.10% + FEE_BUY 0.15% FEE_SELL 0.25%.
  • Time-stop: scalps 2 bars, swings 20 bars — so 1-2 day trades are scalp_*.

Universe

trading_cli/universe.py — 900+ IDX codes without .JK (appended for yfinance). Mirrors trading-suite/backend/config/tickers.py last synced via scripts/sync_universe.sh from IDX. Includes ^JKSE (IHSG) for RS vs market. Use --universe-file to inject fresh list without code change.

Method — Trend Template + VCP

Trend Template 8 (Minervini SEPA): pass ≥7 = Stage 2 leader.

  1. Price > SMA150 & SMA200
  2. SMA150 > SMA200
  3. SMA200 rising 20 bars (1 month)
  4. SMA50 > SMA150 & SMA200
  5. Price > SMA50
  6. Price ≥30% above 52-week low
  7. Price within 25% of 52-week high (×ばつ high)
  8. RS ≥70 vs IHSG (126d return percentile, fails → normalized 7→8)

Stage: Stage2 Uptrend (price>SMA150/200 + 150>200 + rising) / Stage1 Basing (~8% of SMA200) / Stage4 Decline (price<SMA200 + SMA50<150) / else Stage3 Topping.

VCP 0-100 (tight base): last 40 bars High-Low/Close% average r1 15d, r2 10d, r3 5d. tightening = r1>r2>r3 && r3<r1*0.7 (+40), tight = r3<tight% (+20, default 6% via --vcp-tight), r3<3% (+10), dry = 5d vol /20d vol < vol_ratio (+30, default 0.7 via --vcp-vol) else <0.9 (+10), near 20d high <5% (+10). ≥60 = tradable tight base.

Regime gates (soft -20 combo demote, not hard veto — winrate maintained): IHSG Stage4 → demote, adtv(20) <1B IDR (illiquid, goreng) → demote, ATR14/close >5% → demote, CMF(20)<0 && adtv>5B institutional distribution → demote (Chaikin Money Flow CMF = sum(MFV,20)/sum(Vol,20) where MFM=((C-L)-(H-C))/(H-L)∈[-1,1], MFV=MFM*Vol; CMF>0 accumulation >0.05 strong). Gated only if ADTV>5B (LQ45/IDX80) to avoid small-cap fake A/D. Visible as regime_skip and ⚠ regime filter demoted. Soft demote preserves n (110) vs hard veto → n=40 Wilson 0% drag; thresholds fixed (percentile not tuned) until walk-forward proves shift.

Methods — All Strategies (purpose, entry, exit, stop, hold, when)

Core swing (next-open entry, 20d time-stop, 8% Cardinal Sin + 10% trail):

Setup Purpose (agent when) Entry Exit Stop/Hold
vcp_breakout Breakout in Stage2 tight base — best expectancy leader (30% win +5% exp pooled ★). Goal: catch 20-30% winners, 7-8% losers. Close >20d high + Vol&g×ばつ + tight <6% <10d low 8% / trail 10% / Time 20
pullback_50 Pullback to 50MA in uptrend — add to leader on dip. Uptrend (50>150>200) + within 2% of 50 + reclaim >50 Close70 8%
ema21_bounce EMA21 bounce in uptrend — faster pullback, higher freq but lower expectancy (35% win -0.19% exp). Uptrend + cross up EMA21 Close<EMA21 8%

Mean-reversion family (high winrate scalps, Stage1/2 dips):

Setup Purpose Entry Exit Stop
rsi_oversold Snap-back after washout First bar RSI14 ≤30 (prev>30) Stall 3 bars no new 5d high + <SMA5 or 2 red closes, or RSI>55 red 8%
stoch_green_vol Stoch oversold + green + vol K10<20 & D<20 + green candle + Vol>20MA Red + (K cross down or K>50) or <5d low 8%
rsi2_scalp Isolated RSI2<10 + SMA200 RSI2<10 & Close>SMA200 3 bars or RSI2>60 8%

70% hunt — 5% TP/SL 2-day scalps (1-2 day trades, agent's 70% sleeve):

All scalp_*_5 use SL 5% TP 5% 1:1 Time 2 (R 0.6 for _3 variants TP 3% SL 5%). Next-open fill, fees+slip included. Goal: 70% win → +2% exp/trade gross (breakeven 50% at 1:1). Current pooled max ~48% win (scalp_ema_5 n=110) — so agent must gate on win≥70 otherwise report no trade.

Setup Entry (70% hunt) Ideal regime
scalp_rsi2_5 RSI2<10 + SMA200 + green Deep oversold bounce, Stage2
scalp_stoch_5 K15<15 D<15 + green + Vol&g×ばつ Oversold + volume spike
scalp_gap_5 Gap down >2% + hammer (wick>60%) Gap-down reversal
scalp_vcp_5 Tight <4% + Vol×ばつ breakout Micro VCP
scalp_rsi14_5 RSI14<25 + SMA200 + green (looser) Shallow dip
scalp_ema_5 Cross up EMA10 in uptrend (best pooled 47.9% n=110) Pullback to 10EMA
scalp_vol_5 Vol×ばつ spike + green + >SMA50 Volume leader
scalp_drop_5 3d drop >10% + RSI2<15 + green Washout bounce
scalp_hammer_5 Hammer wick>60% body<30% + RSI14<30 Hammer reversal
scalp_rsi2_3 / ema_3 / drop_3 Same as above but TP 3% SL 5% R 0.6 → lifts win ~48% → 55-60% with same edge (use when 70% not hit at 5%) Higher win, lower R

Choose:

  • Swing hold 1-20d → vcp_breakout / pullback_50 (best expectancy +5% pooled, not winrate).
  • 1-2d 70% scalp → scalp_ema_5 (highest n) or scalp_drop_5 (single-ticker 100% n=1) but require pooled win≥60 and Wilson≥55% + n≥30 before sizing — else no trade.

Fees: 0.15% buy +0.25% sell +0.10% slip each side ≈0.5% round-trip drag. 60+ bars needed for swing, scalps need ≥30 pooled trades for Wilson CI.

Entry plan (Minervini): Pivot =20d high, buy zone pivot*1.005-1.02, stop max(20d low, close-2ATR, close*0.92) <close (tightest, max 8%), risk close-stop, T1 +1R T2 +2R T3 +3R, trail after +1R.

Regime Contribution — Backtest & Forward (per-trade, AI must read)

Every completed trade is tagged at entry_date with market regime:

  • Trending = IHSG Stage2 + Normal ATR (CMF>0) — breakout edge lives here
  • Choppy = else (IHSG Stage1/3 Normal)
  • Bear/HighVol = IHSG Stage4 or HighVol (ATR>5%)

Source: regime.py tag_ihsg_stage() (rolling Trend Template on IHSG) + tag_atr_regime() + cmf() gated. backtest.run(..., market_history=ihsg_hist) adds trade.regime and stats.regime_stats[Trending|Choppy|Bear] with winrate/avg_win/avg_loss/expectancy/wilson_low (Wilson 95% only if total≥10 else 0 flagged). cli --json emits top-level regime_pooled (pooled across qualifiers: winrate/exp/n/wins) and per-ticker backtests[STRAT].regime_stats shown as Regime back: Trending 100% n=1 in HOW TO ENTER.

Live limit 50: Choppy 44.4% exp +0.07% n=144 / Bear 42.9% -0.25% n=212 (Trending 0 today, IHSG Stage4). Pooled REGIME CONTRIBUTION block proves filter not dragging winrate (soft -20 kept n, hard veto would →0). Validate weekly: trading-cli --period 2y --validate --json adds per-window regime_oos and pooled wf_regime (Trending 42% n=12 etc) — only promote CMF hard veto if OOS Trending exp > Choppy exp +0.2 with n≥100.

Institutional accumulation: CMF(20)>0.05 strong accumulation, <0 distribution. Combines A/D Line ad_line = cum(MFM*Vol) slope + CMF level vs OBV binary close-vs-close (30-40% disagree days). No intraday VWAP needed (daily yfinance only); OBV cheap but whipsaw, VWAP needs 5m bars (cache ×ばつ), foreign net buy highest alpha but scraper fragile (idx.co.id broker summary) — deferred behind CMF walk-forward gate ml_filter.gate_pooled_n(need=100).

Validation

  • trading-cli --period 2y --validate → walk-forward OOS top3 train 252 step 60 warmup 100 reports OOS win pooled + regime_oos per window and pooled wf_regime. Requires 2y (1y → need 2y note). Gate: keep tuned threshold only if OOS expectancy>baseline and n≥100 pooled and Wilson lower>0.
  • tune.py grid tight 4/6/8 ×ばつ vol 0.6/0.7/0.8 OOS max 57.1% (6/0.6) vs frozen 55.6% (+1.5%) — keep frozen.

Data & Cache

yfinance .JK auto-append, ^JKSE untouched. XDG cache ~/.cache/trading-cli/cache.db (auto-copies old data/cache.db once, fallback). WAL, 12h TTL, throttled 0.18s + stale-while-revalidate via 4-thread pool. 401 Invalid Crumb / 429 → exp backoff 1s→2s jitter, 3 tries. trading-cli --doctor probes ^JKSE+5 live.

Project Structure

trading-cli/
├── pyproject.toml
├── trading_cli/
│ ├── __main__.py
│ ├── cli.py # arg parse + leaderboard (expectancy) + 70% hunt + near_miss split
│ ├── scan.py # fetch IHSG + universe + TMPL/VCP + regime gates (ADTV/ATR/IHSG)
│ ├── minervini.py # TMPL 8 + VCP(tight/vol params) + Stage
│ ├── backtest.py # 18 strats (3 swing +1 rsi2 + 9 scalp_5 +3 scalp_3) next-open, time 2/20, 5%/8% stops
│ ├── validate.py # walk_forward
│ ├── tune.py # grid VCP OOS
│ ├── ml_filter.py # gate pooled n≥300 before ML
│ ├── entry.py # pivot/stop/targets
│ ├── data.py # yfinance + throttle + XDG stale pool
│ ├── cache.py # XDG WAL KV
│ ├── indicators.py # SMA/EMA/RSI/ATR/stochastic/to_frame/adtv
│ └── universe.py # 900+ IDX tickers + load_universe_file()
├── regime.py # IHSG Stage + ATR + CMF regime tagging per trade
├── tests/test_minervini.py / test_backtest.py
├── scripts/sync_universe.sh
└── data/cache.db # legacy, migrated to XDG

Troubleshooting

  • command not foundln -sf .../trading-cli ~/.local/bin/trading-cli + ~/.local/bin in PATH
  • 401 Invalid Crumbtrading-cli --workers 4 or --doctor then wait 5 min
  • possibly delisted → dead ticker, ignored
  • Slow first run → populating XDG cache; next ×ばつ faster
  • No qualifiers → Stage3/4 tape; shows NEAR MISS WAIT + 70% HUNT empty = agent reports no trade (correct)
  • Pooled n 1-4 Wilson 0% → unreliable, need full 956 2y for n≥30

For the Swing Agent — Minimal Prompt

You are a swing-trade recommender (IDX). Every morning run trading-cli --period 1y --json.
- Qualifiers: tmpl>=6 && vcp>=30 && pass && regime_skip==null
- Rank qualifiers by leaderboard avg_expectancy (★ BEST), not winrate.
- 70% sleeve: check leaderboard scalp_*_5/_3 where avg_winrate>=70 && pooled_trades>=30 && wilson_low>=55.
 - If any -> recommend that scalp (TP 5% SL 5% 2d or TP 3% SL 5% for _3) for tomorrow open.
 - Else -> recommend best expectancy swing (vcp_breakout) or report "no 70% trade today" and suggest nearest near_miss WAIT.
- Risk: buy_zone pivot*1.005-1.02, stop = entry.stop, risk_pct, targets T1/T2. Position = 1-2% risk per trade, 5% stop → size = risk / (entry-stop).
- Validate weekly: trading-cli --period 2y --validate --json ; OOS expectancy must stay >0.

License

MIT — see LICENSE. Copyright 2026 mariobgsp.

About

IDX/IHSG Minervini CLI — Stage 2 VCP scanner + 18 swing/scalp backtests (70% 5% 2d) + regime & CMF institutional contribution + walk-forward OOS for daily swing agent

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /