Technical Stock Screener & Backtester (US / TW)
Pricing
from $5.00 / 1,000 stock results
Technical Stock Screener & Backtester (US / TW)
Rule-table stock screener with multi-timeframe and sequential conditions, plus a backtester with a holding-matched null baseline.
Pricing
from $5.00 / 1,000 stock results
Rating
0.0
(0)
Developer
宗諭 楊
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
10 days ago
Last modified
Categories
Share
Describe technical conditions as a JSON rule, list the symbols that match now, and backtest the same rule. Rules can mix timeframes (daily / weekly / monthly), require ordered conditions ("a golden cross after KD dipped below 20"), combine AND / OR groups, and keep entry and exit conditions separate. US stocks come from Yahoo Finance; Taiwan listed and OTC stocks come from the official TWSE and TPEx endpoints.
Checked against independent implementations
| Indicator | Reference | Max absolute difference |
|---|---|---|
| RSI(14), Wilder smoothing | ta.momentum.RSIIndicator | 0.000000 |
| MACD DIF / DEA | ta.trend.MACD | 0.000000 |
| MA(20) | pandas rolling mean | 0.000000 |
| 52-week position (252) | pandas rolling, computed separately | 0.000000 |
| Volume ratio (20) | pandas, computed separately | 0.000000 |
| RSV(9) | ta.momentum.StochasticOscillator | 0.000000 |
| Taiwan-convention KD (n=9, α=1/3, seed 50) | pandas rolling + ewm(alpha=1/3, adjust=False) on AAPL, MSFT, 2330.TW (5 years) | 5.7e-14 |
The Taiwan KD is the convention used by Taiwanese brokers:
RSV = (C − LLV(L,9)) / (HHV(H,9) − LLV(L,9)) × 100K = (2/3)·K_prev + (1/3)·RSVD = (2/3)·D_prev + (1/3)·K seed K0 = D0 = 50
It is not the English-language Stochastic(14,3,3), which smooths with an SMA; the two differ by up to 19.36 points on the same data and must not be mixed. What the comparison above does not settle is whether every Taiwanese broker uses exactly α=1/3; that needs a reading from a charting terminal.
Rule format
{"name": "Buy: weekly KD golden cross after K<20 / Sell: weekly MACD death cross, or weekly KD death cross after K>80","entry": {"all":[{"tf":"W","ind":"kd","params":{"n":9},"event":"cross_above","a":"K","b":"D","within":1,"prior":{"ind":"kd","event":"below","a":"K","value":20},"prior_within":8}]},"exit": {"any":[{"tf":"W","ind":"macd","event":"cross_below","a":"DIF","b":"DEA","within":1},{"tf":"W","ind":"kd","params":{"n":9},"event":"cross_below","a":"K","b":"D","within":1,"prior":{"ind":"kd","event":"above","a":"K","value":80},"prior_within":8}]}}
| Field | Meaning |
|---|---|
tf | D daily, W weekly, M monthly (resampled from daily bars, ISO weeks) |
ind | kd, rsi, macd, ma, volume (volume ratio), range52 (52-week position), breakout (% from the N-day high), squeeze (moving-average squeeze), gap, price, fundamental; new: prior_range, ma_cluster, turnover, high_n, kd_runs, macd_live (see New conditions below) |
event | cross_above, cross_below, cross_above_or_approaching, above, below, gte, lte (new: >= / <=), rising, falling, approaching_level |
b / value | b is another key of the same series (for example KD's "D") or another series spec; value is a literal number. A threshold is "event":"below","value":30, never "b":30 |
within | The event must happen within the last N bars. 1 means the last bar only |
lag | New. Evaluate the whole condition lag bars earlier ("lag":1 = "as of the previous bar"). Default 0 |
px | New. "adj" (default: the price series you passed in, adjusted for dividends in the long-history tool) or "raw" (unadjusted prices, for absolute-price conditions) |
params.min_bars | New. Warm-up: the indicator is treated as missing (condition false) for the first min_bars-1 bars |
prior + prior_within | Ordered condition: prior must have held within N bars before the main event (counted on its own timeframe) |
approaching_level | Needs level, from_above and max_distance. max_distance is not optional: "the gap is shrinking" alone would count RSI falling from 70 to 58 as "approaching 30" |
A malformed rule is refused before any data is fetched, with a message naming the condition and the missing key; a refused run writes no dataset rows and is not charged per row.
New conditions (2026-10-13)
All of them are strict: when there is not enough history the value is missing and the condition
is false; nothing is computed on a partial window (the older range52 / breakout / squeeze indicators keep their original behaviour, so existing rules give identical results).
ind | Parameters | Keys | Typical use |
|---|---|---|---|
prior_range | n | HH LL highest high / lowest low of the previous n bars excluding today, HC LC highest / lowest close, AMP = (HH−LL)/LL×100 | Breakout of the previous N-day high (price high above HH), close above the previous N-day highest close (close above HC), N-day box amplitude below 8% (AMP below 8) |
ma_cluster | periods (default 5,10,20,60) | CL = (max MA − min MA)/min MA ×100, MAX MIN | "Moving averages tangled": CL lte 5. Add "lag":1 to ask about the previous bar |
turnover | n | TO = N-bar average traded value (TWD, includes today), TOP = same over the previous N bars | Liquidity: TO gte 50000000. Needs traded value, which only the official TWSE/TPEx sources carry |
high_n | n (252 ≈ 52 weeks), use high/close | H52 L52, DTH52 = % below the N-bar high, FRL52 = % above the N-bar low | Near the 52-week high: DTH52 lte 15. Strict: fewer than n bars → false |
kd_runs | n (9), hi (80), lo (20), mid (50) | K D, run lengths K_HI_RUN D_HI_RUN K_LO_RUN K_MID_UP K_MID_DN | KD "blunting" (鈍化): K_HI_RUN gte 3 means K>80 for 3 bars in a row; mid-axis: K_MID_UP gte 5 |
macd_live | period W/M, fast slow sig | DIF DEA OSC aligned to daily bars | Weekly or monthly MACD including the period in progress, as a daily-timeframe indicator. Since all tf:"W"/"M" conditions are now evaluated as of each day (see Weekly and monthly conditions below), it gives the same values as tf:"W" + macd |
Rules that use only daily conditions give exactly the same entries and trades as before (checked old engine against new on 61 randomly generated daily rules × 24 price series, and on real Taiwan data).
Weekly and monthly conditions are evaluated as of each day
A weekly or monthly condition sees the week (or month) in progress up to that day, the same weekly candle a charting terminal shows mid-week, and the same one a screen run on that day uses. So a backtest signal on a Wednesday is exactly what a screen run that Wednesday would have reported, and no day after it is used. On the last trading day of a week or month the value is the completed bar, as before.
Until 2026-10-14 the backtest instead used the last completed week or month on mid-period days, so weekly and monthly signals could fire only on the last day of a period while a daily screen could fire mid-week: backtest and screen answered different questions. Daily-only rules are not affected; for weekly and monthly rules, results on the last day of each period are unchanged and mid-period days now follow the screen.
Output
matchesNow: whether the last bar satisfiesentry- Backtest mode adds
trades,meanReturnPct,medianReturnPct,winRatePct,holdMedianBarsandexitReasons SUMMARY.backtestaggregatesexitReasonsacross symbols for you:
| Field | Meaning |
|---|---|
totalTrades | Trades across all symbols |
exitReasons | {max_hold: n, exit_rule: n, stop_loss: n} |
maxHoldSharePct | Share of trades that ended at the cap. If this is high, your exit rule is not doing the exiting |
exitRuleSharePct | Share of trades closed by the exit condition |
symbolsWithNoTrades | Symbols that never entered |
maxHoldBars | The cap used in this run |
notes calls out three cases by name: zero trades, a max_hold share of 50% or more, and all
exits coming from the rule.
maxHold counts daily bars, even for weekly rules
Even when a rule says tf: "W", the cap counts daily bars: 60 bars is about 12 weeks.
| maxHold | Closed by the exit rule | Ended at the cap |
|---|---|---|
| 60 (default) | 50% | 50% |
| 400 | 100% | 0% |
(Measured 2026-10-10: 10 US stocks, 24 months, weekly KD + MACD rule, 16 trades.)
Trades that end at the cap report "hold N bars after entry", not the result of your exit rule. For weekly rules use maxHold ≥ 250.
Fundamental conditions
Rules can also use fundamental fields:
{"ind":"fundamental","a":"trailingPE","event":"below","value":22}
Available fields (20): trailingPE forwardPE priceToBook dividendYield trailingEps
forwardEps marketCap beta profitMargins returnOnEquity debtToEquity
revenueGrowth earningsGrowth sharesShort shortRatio shortPercentOfFloat
pegRatio enterpriseToEbitda currentRatio payoutRatio
Backtest mode always refuses them, with no switch
The source has only today's values, no history. Judging a bar from three years ago with
today's P/E uses future information to decide past trades: backtest returns come out
systematically too high while every field looks filled and normal. So a rule with
fundamentals is refused in mode: backtest, and there is no parameter to bypass it. Remove the
fundamental conditions to backtest, or run screen to use them.
The source mixes units
Raw values measured on 2026-10-11 for AAPL, JPM, XOM, KO, T and NVDA:
| Field | Raw range | Unit |
|---|---|---|
dividendYield | AAPL 0.32, T 5.00 | percent |
debtToEquity | 15.92 … 129.1 | percent |
profitMargins / returnOnEquity / payoutRatio / revenueGrowth / earningsGrowth / shortPercentOfFloat | 0.0083 … 1.49 | fraction |
marketCap / sharesShort | absolute | absolute |
| others | multiples | multiples |
Only 2 of the 20 fields are percentages; the other ratios are fractions. Writing
dividendYield > 0.03 to mean 3% would match everything (0.32 > 0.03). This Actor therefore
evaluates every condition on fractions and outputs both the raw value and _asFraction.
Coverage is uneven by nature
AAPL and XOM have 20 of 20 fields, but JPM has 17 of 20: banks have no currentRatio,
debtToEquity or enterpriseToEbitda. A condition on a missing field evaluates to false
(no error), SUMMARY.fundamentals.coverageByKey gives the share of symbols that have each
field, and symbolsMissingByKey lists up to 10 symbols missing it, so "this symbol has no such
field" stays distinct from "this symbol does not match".
Limits
| Limit | Detail |
|---|---|
| Survivorship bias | (Actor run) Only symbols listed today are included; the local long-history layer above keeps delisted names. Names delisted or acquired during the period are missing, so returns are biased upward, more so for longer holds |
| Unfinished intraday bar | If the median of "last bar volume ÷ previous 20-bar average" across symbols is below 0.5, the last bar is dropped and notes says so. Measured intraday the median ratio was 0.107 (normally about 0.9); keeping that bar would break every volume condition |
| Weekly and monthly volume | Resampling sums daily volume over the period (checked: [100,200,300] + [400,500,600,700] → [600, 2200]). Only when the daily data carries no volume is a volume condition refused |
| 200 symbols per run | Each run fetches its own data; refetching a whole market is not feasible (6,136 US stocks over 5 years took 254 seconds) |
| Symbols are required for US | market: "us" needs a symbol list. market: "tw" with an empty list scans every listed and OTC stock |
| Wrong symbols | If none of the symbols returns data while a control symbol (SPY or 2330) does, the run is refused with that explanation instead of returning an empty success; if the control fails too, the run fails because the source is down |
| Taiwan OTC is slower | TWSE has a per-symbol endpoint (used automatically for 40 symbols or fewer: requests = symbols × months). TPEx has none, so OTC data is fetched day by day for the whole market (requests = trading days, regardless of symbol count) |
| No look-ahead | Evaluating day t reads only data up to day t, including weekly and monthly conditions (the period in progress counts only up to day t). Tested by evaluating every rule both on the full history and on history cut at day t: the answers must match. Backtest entries fill at the next bar's open; exit signals fill at the open of the following bar |
This is a screening and research tool, not investment advice.