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 --jsoneach morning (WIB), parsepicks[], pick the stock wherebest_strathasexpectancy>0andwilson_low>0andregime_skip==null. For 70% winrate +5% hunt, filterscalp_*_5withwinrate>=60andpooled n>=30— if empty, report no trade (honest). See Agent Workflow below.
- 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_*_5TP 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)*avgLossand Wilson 95% CI, not raw winrate — so agent avoids 70% win with 0.5R losers that lose money. - Walk-forward honest:
--validate --period 2yproves OOS (train 252 step 60 warmup 100) before tuning. No tuning on 1y.
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.
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.
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 |
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<30orwilson_low==0→ unreliable, do not size on winrate alone.expectancy <0→ negative edge, skip even if winrate 35%.70% HUNTblock lists onlyscalp_*_5withwin>=60; if empty that day, agent must outputno 70% trade todaynot force.- Always next-open fill:
fill = open*(1+SLIP)withSLIP 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_*.
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.
Trend Template 8 (Minervini SEPA): pass ≥7 = Stage 2 leader.
- Price > SMA150 & SMA200
- SMA150 > SMA200
- SMA200 rising 20 bars (1 month)
- SMA50 > SMA150 & SMA200
- Price > SMA50
- Price ≥30% above 52-week low
- Price within 25% of 52-week high (×ばつ high)
- 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.
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) orscalp_drop_5(single-ticker 100% n=1) but requirepooled win≥60andWilson≥55%+n≥30before 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.
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).
trading-cli --period 2y --validate→ walk-forward OOS top3train 252 step 60 warmup 100reportsOOS win pooled+regime_oosper window and pooledwf_regime. Requires 2y (1y →need 2ynote). Gate: keep tuned threshold only ifOOS expectancy>baselineandn≥100pooled andWilson lower>0.tune.pygridtight 4/6/8 ×ばつ vol 0.6/0.7/0.8OOS max 57.1% (6/0.6) vs frozen 55.6% (+1.5%) — keep frozen.
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.
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
command not found→ln -sf .../trading-cli ~/.local/bin/trading-cli+~/.local/binin PATH401 Invalid Crumb→trading-cli --workers 4or--doctorthen wait 5 minpossibly delisted→ dead ticker, ignored- Slow first run → populating XDG cache; next ×ばつ faster
- No qualifiers → Stage3/4 tape; shows
NEAR MISSWAIT +70% HUNTempty = agent reports no trade (correct) - Pooled n 1-4 Wilson 0% → unreliable, need full 956 2y for n≥30
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.
MIT — see LICENSE. Copyright 2026 mariobgsp.