Stock Checker screens US equities and evaluates strategies with a shared signal engine, web dashboards, local MCP reports, a CLI, and WhatsApp notifications.
| Workspace | Purpose |
|---|---|
packages/core |
Signals, backtests, persistence, and CLI |
packages/automation |
TypeScript service tooling and WhatsApp SDK regressions |
apps/api |
Fastify API — port 5101 |
apps/web |
Next.js dashboard — port 5100 |
apps/mcp |
Local stdio MCP reports and screening |
packages/finance |
Python 3.14 finance tools, Ruff, and Pyrefly |
| Screener | Ticker detail |
|---|---|
![]() |
![]() |
Install mise, then run from the repository:
mise install # Node 26, Bun, Python 3.14, and uv
mise run install # dependencies and Git hooks
mise run dev # API + webOpen http://localhost:5100. See mise.toml or mise tasks for commands.
The local MCP works without web servers; browser dashboards require dev.
| Menu or page | Route | Features |
|---|---|---|
| SCREENER | / |
Signals, scores, and sector heatmap for the selected 20 symbols |
| MARKET SCREEN | /market-screen |
Saved Finviz jobs, progress, results, pause/resume, and paper performance |
| PORTFOLIO | /portfolio |
Portfolio tickers and current analysis |
| WATCHLIST | /watchlist |
Tracked tickers and current analysis |
| ALERTS | /alerts |
Browser alert rules and notification settings |
| Ticker detail | /:ticker |
Charts, signal, fundamentals, earnings, news, and events |
| Backtest | /:ticker/backtest |
Historical strategy evaluation |
Create Finviz jobs through the MCP flow. MARKET SCREEN reads saved jobs. Resume continues pending candidates; pause lets in-flight requests finish. Browser alerts run while the app or PWA is open; they stop when it closes.
mise run predict -- --ticker=TSLA,PLTR --sort=asc
mise run predict -- --ticker=OII --format=json
mise run backtest -- --cost-bps=20
mise run backtest -- --research --quickDefault predictions append monthly CSV rows under packages/core/public/;
--format=json exports JSON instead. --sort=asc|desc orders by ticker;
--sort-by=decision orders BUY, SELL, HOLD, then score. --fundamentals,
--news, --earnings, --dividends, and --options print extra data for
every --ticker symbol after the prediction table.
--portfolio-action=report analyzes the saved portfolio assets (or --ticker),
prints the report, and saves it under data/reports/. The portfolio and
watchlist live in data/portfolio.json and data/watchlist.json, shared by the
CLI, API, and web regardless of working directory.
The summary table shows a score mix for BUY/SELL rows only (hidden for blocked
HOLDs) and the measured in-sample base rate with its SPY benchmark; scores are
not win probabilities.
Use mise run predict -- --help for the full list.
For Slack BUY/SELL summaries, set SLACK_WEBHOOK_URL or pass --slack-webhook.
CLI, web, MCP, screening, and backtests share the leader-pullback strategy and
the complete configuration in data/config/optimized_weights.json (v3).
Without a compatible file, all use DEFAULT_QUALITY_PIPELINE_CONFIG; legacy
partial weight files are ignored. No command rewrites that file: the former
optimize/learn loop and the weekly optimization workflow were removed
because they overwrote the validated configuration with a winner fitted on a
single ticker. backtest measures the active configuration; --research
replays the retired exploratory variants without saving anything.
See trading principles and the
measured win rates.
Reopen Codex or Claude Code in this repository after installation.
stock_checker is registered in .codex/config.toml and
.mcp.json, using mise run mcp over stdio. Keys and recipient settings
come from the environment or private MCP configuration; reconnect after changes.
The separate Serena and Aside entries require their own installed CLIs.
| Tools | Use |
|---|---|
analyze_stock |
Signal reasons, historical rates, ATR references, PER/PSR peer comparisons, and analyst targets |
show_stock_dashboard, open_stock_dashboard |
Inline MCP Apps dashboard or existing web page in the browser |
screen_stocks |
Final SC decisions for a selected list of up to 50 tickers |
prepare_finviz_screen, create_market_screen |
Prepare collection filters and save a durable candidate job |
get_market_screen, control_market_screen |
Read results or pause/resume a saved job |
get_market_screen_performance, refresh_market_screen_performance |
Read cached paper outcomes or explicitly fetch later prices |
list_market_screens, get_daily_report |
List saved jobs (origin, status, counts, US session date) or open the latest 09:00 daily run with near misses on zero-match days; read-only |
get_market_screen and get_daily_report accept format: "compact" for one
line per ticker with the first blocking reason. Non-US symbols (exchange
suffixes such as .KS, futures, FX) are analyzed with a warning: decisions and
the measured base rate were validated on US equities only.
Example analyze_stock input:
{"ticker":"OII","lookbackDays":730}Example screen_stocks input:
{"tickers":["OII","NVDA","TSLA"],"decision":"BUY","limit":20}An empty screen input uses the selected 20 symbols. Decisions are BUY (default),
SELL, HOLD, or ALL; a high score alone does not qualify BUY. Screen lookback
defaults to 730 days; ticker reports default to 2920. Both accept 730–3650 days;
use the same window when comparing them. Selected scans retain completed results
after their 45-second budget expires.
Inline dashboards require an MCP Apps-capable client; browser dashboards require
the web server and do not start it automatically.
The market flow has two steps:
- Call
prepare_finviz_screen, then collect its ticker rows through an authorized browser MCP such as Aside or a supplied export. Preparation does not fetch pages. - Call
create_market_screenwith those tickers and their Finviz provenance. SC evaluates the saved candidates with its existing signal engine.
BUY preparation defaults to stocks below SMA50. Market-cap, average-volume,
and price cuts are optional; belowSma50: false broadens collection. Finviz
covers US exchanges, and its intraday filters can differ from SC's completed bars.
A Finviz match is a candidate, not a final SC BUY.
Preserve the source URL, ordered filters, filtered total, capture time, and
complete/partial status. Complete collection requires the unique count to
equal the declared total; missing or blocked pages remain partial.
Public browser collection needs no Finviz key.
Reports show the collected candidate count and evaluated count separately. A candidate limit is not an analysis failure. Unavailable results retain their cause, including missing history, invalid prices or average daily price ranges, or infeasible risk references; risk rejections do not pause the job as provider failures.
Jobs accept up to 15,000 input rows and live in ignored data/market-scans/.
Creation starts work by default; autoStart: false saves a paused manifest.
Read with get_market_screen; use control_market_screen with action: "pause"
or "resume". Keep the runner open and explicitly resume after interruption;
completed symbols are retained. Collection coverage and analysis completion
are reported separately.
Matched BUY paper outcomes use the next session open to the fifth session close (including the entry session), with 10 bps round-trip cost. Win rates use completed wins/sample counts; pending and unavailable records remain separate, and zero completed samples means no rate. Refresh performance explicitly to fetch later prices; reads use saved data. These are simulated observations, not brokerage fills. Scores are not win probabilities. No validated out-of-sample win rate is published for the current implementation.
Link a WhatsApp Web account through the local gateway; no Meta Cloud API key,
template, or message-fee setup is needed. Set WHATSAPP_TO to the recipient's full
E.164 number, including + and country code, in ignored mise.local.toml:
[env]
WHATSAPP_TO = "YOUR_E164_NUMBER"On macOS, install the user service with initial pairing enabled:
mise run whatsapp:service:install -- --linkOpen http://127.0.0.1:5102/ and scan the QR from the phone's
Settings → Linked devices → Link a device. Then switch to normal mode:
mise run whatsapp:service:install
mise run whatsapp:service:statusUse whatsapp:service:restart for an expired QR; start, stop, and uninstall
control the service. The computer must be logged in, awake, and online. The service
loads mise settings; temporary shell exports do not persist into it.
For foreground use, run whatsapp:link and leave it running, or stop it and run
whatsapp:gateway. Linked sessions remain in ignored data/whatsapp/.
CLI BUY/SELL batches and market-screen completion summaries send after saving
results. MCP screen_stocks sends only with notifyWhatsApp: true (default false).
Every message opens with the completed US session date. Up to three detailed
BUY tickers carry a 매매 계획 block (next-session open entry and fifth-session
close exit as US dates with Australia/Sydney times from the shared NYSE calendar,
a position-size hint, and the in-sample validated expectation), the original
decision and reasons, per-ticker BUY history (collapsed to counts and dates below
ten samples), analyst targets, and ATR stop/target levels labelled as references
outside the validated rule. Zero-match mornings send a short heartbeat instead.
Set STOCK_CHECKER_MAX_POSITIONS (default 5) and optionally
STOCK_CHECKER_CAPITAL to shape the size hint. Failures preserve results.
Restart API/MCP after changing the recipient; relinking is unnecessary.
Unconfigured recipients and GitHub-hosted jobs send nothing.
Before each send the client probes the gateway's /status; while the gateway is
unreachable or the session is not connected, nothing has been sent, so it waits
and re-probes at about 0/60/180/600 s before giving up. Once a request is posted,
ambiguous outcomes (timeouts, 502/504) are never retried, so a message cannot be
duplicated. When the session is not connected or a send is not accepted, a macOS
Notification Center alert is raised (set STOCK_CHECKER_DESKTOP_ALERTS=0 to
disable). The gateway only sends to the WHATSAPP_TO number (403 otherwise) and
refuses more than WHATSAPP_DAILY_SEND_CAP sends per local date (default 50,
429 daily-send-cap). GET /status reports the session state, how long it has
held, today's send count, and the last send result.
Account risk. The gateway links a personal WhatsApp account through an unofficial WhatsApp Web client (Baileys). WhatsApp's terms do not permit automated clients; accounts using them can be rate limited or banned without notice, and the only recovery is a manual re-link or a new number. Use a dedicated number if possible, keep the recipient pinned and the daily cap low, and never expose the gateway beyond loopback. The official WhatsApp Business Cloud API is the supported alternative when a template-based, fee-bearing integration is acceptable.
For daily reports, run mise run daily-report:service:install on macOS.
The service starts once each day at 09:00 Australia/Sydney, including DST;
it can start until 09:14 if the computer wakes late, and a late catch-up run is
allowed afterwards until 90 minutes before the next NYSE open (still once per
Sydney date, labelled (지연)). Keep the computer logged in, awake, and online.
By default the report scans the configured validated universe (the ticker list
the leader-pullback edge was measured on; no browser needed) with the shared
engine for up to 20 minutes, which the --dry-run estimate keeps within budget.
Each run records the latest completed NYSE session; a later Sydney date whose
session was already delivered (Sunday, Monday, post-holiday) is skipped with
no-new-session instead of resending the same result, and the session date is
shown in the report title. Set DAILY_REPORT_SOURCE=finviz before installing to
opt into the Finviz discovery lane instead: stocks below SMA50 ordered by
descending volume, up to 200 candidates in 60 seconds, labelled as an
unvalidated universe in the saved job; a Finviz or Aside failure falls back to
the configured universe. Saved jobs record collection coverage (capped by the
candidate limit versus a partial failure) separately from analysis status and
appear in MARKET SCREEN. Check with mise run daily-report:service:status or
mise run daily-report -- --status. --dry-run checks readiness without
scanning or sending. Set DAILY_REPORT_MAX_CANDIDATES, DAILY_REPORT_MAX_PAGES,
or ASIDE_BIN privately for the Finviz lane. mise run market-scans:prune -- --days=30 archives older scans while keeping BUY matches and paper ledgers.
Each morning report ends with a '보유 계획' section built from a file-backed ledger in
ignored data/positions/ (one JSON row per ticker:signalSession). Rows are created
automatically from BUY recommendations the daily report actually delivered (saved
market-scan jobs whose notification.json result is accepted), deduplicated by
ticker and signal session so weekend re-sends never create a second position. The
plan follows the validated rule only: entry at the next NYSE open after the signal
session and exit at the fifth session close, with Sydney local times from the shared
NYSE calendar (holidays and early closes included). The section lists entries due at
the next open, '오늘 밤 청산' reminders on the morning before the exit session, open
positions with their day count, '청산 확인 필요' rows after the exit close (with the
saved paper P/L when the performance ledger has one, otherwise n/a), recent closes,
a per-BUY weight, and the in-sample expectation once (n=155, 57.4% wins, +0.63% mean,
-13.6% worst). A SELL decision on a held ticker is shown as a reference only; it is
not part of the validated rule. Digest failures never block the report.
BUYs outside the measured universe stay in the ledger but out of the plan unless
POSITIONS_INCLUDE_UNVALIDATED=1. POSITIONS_CAPITAL (optional) and
POSITIONS_MAX_POSITIONS (default 5) set the per-BUY weight hint.
mise run positions -- list # every ledger row with its stage
mise run positions -- digest # print today's '보유 계획' without sending
mise run positions -- sync # ingest newly delivered BUYs
mise run positions -- record-fill --ticker=AAPL --session=2026-10-12 --price=123.45 --size=10
mise run positions -- close --ticker=AAPL --session=2026-10-12 --price=125.10--session is the signal session (the BUY's 종가일). Fills and closes appear in the
next morning's section; a close recorded without a fill shows no realised return.
Yahoo data needs no API key. Optional environment or private client settings:
| Variable | Purpose |
|---|---|
TIINGO_API_KEY |
Daily-OHLCV fallback when Yahoo fails |
FMP_API_KEY |
Fallback for recent individual analyst target updates |
WHATSAPP_AUTH_DIR, WHATSAPP_GATEWAY_URL, WHATSAPP_GATEWAY_TOKEN |
Override the local session directory, loopback gateway, or token |
WHATSAPP_DAILY_SEND_CAP, STOCK_CHECKER_DESKTOP_ALERTS |
Gateway sends per local date (default 50); 0 disables macOS failure alerts |
STOCK_CHECKER_MAX_POSITIONS, STOCK_CHECKER_CAPITAL |
Position-size hint in BUY messages (default 1/5 of capital, 5 concurrent) |
STOCK_CHECKER_DASHBOARD_URL |
MCP browser links; default http://localhost:5100 |
API_HOST, CORS_ORIGIN |
API bind address (default loopback 127.0.0.1) and the single browser origin allowed to call it (default local dashboard origins; never a wildcard) |
The API has no authentication. Writes (portfolio, watchlist, market-screen
controls) require the dashboard origin from a browser, and Origin-less requests
are accepted from loopback clients only. The screener accepts at most 50 symbols
per request, runs four analyses at a time, and lists failed symbols under
unavailable with a redacted reason. GET /api/market-screens?job=<id> pins a
saved job first for /market-screen?job=<id> deep links.
See the API environment example for the configuration.
mise run lint # Biome + Ruff lint/format checks
mise run typecheck # TypeScript no-emit + Python Pyrefly
mise run test # offline workspace, finance, and automation regressions
mise run deps:outdated
mise run deps:updateAdd :core, :api, :web, or :mcp to test / typecheck for scoped checks.
mise imports Bun workspace scripts using its experimental Node task inference;
mise run //apps/web:typecheck runs the package script directly. Automation
uses .mts and tsx; typecheck:automation is included in the same quality gate.
Python uses packages/finance/uv.lock; finance:run executes scripts, and
finance:deps:outdated / finance:deps:update maintain the lock.
Quality includes application and Python checks;
daily data commits CSV predictions on US
weekdays, tagging each row with StrategyId and ConfigSha256.
Main pushes run Release Please: synchronize
versions and bun.lock, validate the release PR, then merge and create the release.
Use Conventional Commits: feat bumps minor, fix and configured maintenance
types bump patch, and breaking changes bump major. Failed checks leave the PR open.
Nightly chore(data) CSV commits touch only packages/core/public/, which is
excluded from release calculation, so they never cut a release on their own.

