pmtrade · system map

Upgrade

Where the system is now, what every live knob does, and what's next. Architecture & roadmap render anywhere; current settings are local-only (read from the bot machine's config.json + ladder_schedule.json, inert on the public site).

Architecture

Pipeline

board (api.php, future_only) → supervisor (poll 300s: gamma-liquidity filter, rank gate, concurrency cap) → spawns child per match (state/child_cfg) → child loop: parallel fetch (CLOB price/bid + Sofascore) → weighted pre-game ladder → guards → BUY/close → durable ledger (state/positions) + CSV/JSONL tape

Guard & exit ladder

Entry guardsbefore any BUY
  1. Max-buy-odds cap (12×)
  2. Falling-knife guard (3rd+ rung into an accelerating fade)
  3. Dead-market / min-fill-price block
  4. Per-match cooldown (odds must move)
  5. Per-sport bet cutoff (0.72× avg game)
  6. Anchor lock + weighted tier ladder
Positionwhile holding
  1. Phantom-buy settle guard (delayed fill booked UNSETTLED; VOID after 20min)
  2. Blended P&L on CONFIRMED shares only
  3. Bid-side TP trigger + slippage buffer
  4. Close-retry escalation (marketable-limit reprice; stuck alert)
Exit — in ordereach cycle
  1. Decayed take-profit (bar lowers as the game runs out)
  2. Game-state stop-loss (lost a set / behind late, losing position)
  3. Salvage (recoverable < ~8% of stake — near-dead, recover cents)
  4. Hard stop (3h after start — stop monitoring)

Node, enrollment & one-wallet lock

Local-only data model & cadence

Rank pipeline & accounting truth model

Current settings (live, local-only)

Not the bot machine.

Live settings are local-only by design — they're read from config.json + ladder_schedule.json in the pmtrade folder on the machine running the bot, and never leave it. On the public server this section stays inert.

To view: on the bot machine serve the board locally — php -S 127.0.0.1:8899 -t <board folder> — and open http://127.0.0.1:8899/upgrade/.

Known Issues & Roadmap (26 items · upgrade/todo.json)

P1
Rerun agree-bucket loser decomposition on new-engine data
2026-07-23 finding (state/retro/entry-sweep.md Part 5, n=35): 66% of loss $ came from market-doubted picks (pregame <50c); best cut 'no bets <0.55' = net +$34.54 BUT all win $ sit in the doubt buckets (comebacks) AND the strong-agree bucket touched TP50 100% yet netted -$11.93 under the OLD exit engine. After ~2 weeks of 1-min data + fixed exits (bid-TP/buffer/decay/stop-loss) + result stamps: rerun the decomposition; if agree flips positive and doubt still bleeds, ship the pregame price floor (or half-stake <0.40) with real evidence.
strategybacktestretune
P1
Lock down VPS pipeline triggers
AUDIT B1 (P1): _wc_trigger.php / _enrich_trigger.php / _race_trigger.php spawn background Python with NO auth — anyone with the URL can trigger them on the public VPS (DoS/resource-abuse). The _w56x/_rescore triggers use weak hardcoded ?k= tokens; _w563_probe.php dumps hostname + crontab. Delete the orphaned one-shots; gate anything that must stay behind the superadmin secret + POST (never a GET token).
auditsecurityvps
P1
Superadmin first-run TOFU hardening
AUDIT B2 (P1): superadmin/index.php accepts a set-password form from anyone when no password is set yet — on the public VPS, whoever reaches it first claims superadmin. Provision /srv/access/superadmin.hash at deploy time and fail-closed (deny the set form) unless it's an authenticated first-run.
auditsecurityenrollment
P1
End-to-end run_loop integration test
AUDIT P1: the money-critical cycle is covered only by pure-helper units + inspect.getsource string assertions. Add a hermetic run_loop test (fake ctx + injected fetch/ctf_shares/market_buy/sell) that drives a full BUY->settle->close cycle and asserts the executed exit decision ORDER (TP-decayed -> stop-loss -> salvage -> hard-stop), not a source substring.
audittesttrading
P1
Phantom-BUY live verification of the v2 limit-sell path
The P3 close-retry escalation places a marketable-limit sell (py_clob_client_v2 create_order/OrderArgsV2) on repeated close failures; it falls back to market_sell on any API error so it is never worse than today, but the limit path itself is NEEDS-LIVE-VERIFICATION on the sig-3 deposit wallet. Confirm on the next real stuck close.
tradingverify
P1
Retune parameters after ~1 min recorded cadence (~2 weeks of data)
The full parameter sweep (optimize-2026-07-23.md) ran on a small loss-heavy 37-match sample, so only salvage_stake_frac 0.32->0.40 was applied. With finer (~28-60s) cadence now recording, re-run the sweep in ~2 weeks on a larger, more balanced sample and re-decide max_buy_odds / tp_decay / stop_loss knobs on robust plateaus.
strategybacktest
P1
Accounting share-tracking audit
The data-api REDEEM sometimes reports usdcSize/size=0 (Rockies eternal-open, now fixed) and occasionally a partial redeem (Broom: residual held after a usdcSize>0 redeem). Do a full pass reconciling every market's on-chain CTF share flow vs the data-api aggregation so held/realized is exact for all edge shapes, not just the two patched ones.
accountingaudit
P1
Canonicalize 1v1 vs mvm gap math across live board, API, and backtest
KNOWN ISSUE — audited 2026-07-25. The live board's giant ‘Gap Total’ is NOT a raw team total: 1v1 compares the two individual player scores, while mvm first averages each player's three opponent scores, applies the game-date boost and hinge weight, then compares normalized team scores (sum of rounded player scores / sum of role weights). The format difference is implicit from row shape; computeMatchScores ignores match.format. Three definitions now conflict: live/API use the weighted denominator (normally 4.5 for a 3-player team with one 2.5x hinge), backtest and PROJECT_OVERVIEW divide by player count (3), and the label implies a raw team-total difference. Fixture 698834 proves the impact: live mvm gap 67.11 versus backtest 100.67. Separate display rounding can also show +32 : -36 beside a giant 67, and api.php hard-codes Hybrid 2.1x / hinge 2.5x even when browser controls change. Acceptance: add explicit 1v1 and mvm branches with roster/cardinality validation; choose and name the canonical normalized gap; expose raw team-total gap separately if wanted; use identical math and controls in live, API, backtest, exports, sorting, colors, and docs; round once so the headline reconciles with the displayed side scores; add fixtures covering positive/negative scores, hinge weights, malformed rosters, and slider changes. RESOLVED 2026-07-25 (deployed): canonical gap is Σadj÷Σsumw everywhere — backtest ÷count bug fixed, explicit match.format branch added, headline rounds once so it equals the displayed side-score difference. 1v1/1vmany/mvm gaps byte-identical before/after (0 diff); parity api.php↔index.html↔extract_gap.js = 0 mismatches on 58 fixtures.
known-issuescoringgapparity
P1
Deploy per-sport surfacing (fix tennis monopoly)
2026-07-25 diagnosis: traded data is 96% tennis but the board SUPPLIES 62% non-tennis (28 MLB matches in a 6-day window). Root cause: red/orange coloring ranks `gap` in one POOLED cross-sport pile, and team sports' roster-averaged gap is compressed so they never clear the tennis-dominated cutoff (MLB max gap 48.4 < orange cutoff 51.0 => zero MLB could EVER surface). Fix on branch `fix/multisport-surfacing` (bot-side per-sport top-15% ranking, unioned with existing red/orange, off by default via per_sport_top_frac=0, 495 tests pass): today's board 10 candidates/0 MLB -> 18/5 MLB. Deploy + verify non-tennis actually arms & records. A complementary per-sport coloring for the human dashboard is proposed, not built. RESOLVED 2026-07-25 (deployed): board colors each sport∩format bucket against ITSELF (top ceil 5/10/15%), no pooled cut, no fallback, no display floor — every sport (N>=1) surfaces its own top, so tennis no longer monopolizes red. Gaps are within-sport-only (disclaimer added); cross-sport magnitude normalization intentionally NOT done (5-agent analysis showed it is unvalidated/arbitrary). Home page grouped by color, sorted by raw gap within each color. Tiny-sport red culling deferred to the bot-side betting gate (>=10 games to rank).
strategymultisportdata
P2
Sign-off: superadmin server-side wallet P&L display
AUDIT B4 (P2, gray area): superadmin/index.php renders each node's wallet address + realized P&L from server-side machines.json. It's computed from the node's PUBLIC on-chain wallet (not the private ledger), so arguably public — but it's the ONLY wallet-linked figure surfaced on the VPS. Conscious sign-off, or move it client-side/loopback like accounting.
auditlocal-only
P2
Move committed wallet identity out of config.json
AUDIT P2: config.json commits expected_address + funder proxy (public addresses, but they tie the tracked repo to one wallet and de-anonymise the operator if shared). For the enrollment model, move them to a gitignored per-node file (node.json) or template with placeholders + per-node override.
auditsecurityenrollment
P2
Sofascore single-source degradation plan
AUDIT P2: Sofascore is the only score feed. The bot fails safe (no score -> no stop-loss, cutoff via board game_time) but every game-state feature degrades to price-only if Sofascore blocks/changes shape. Add a fallback feed or an explicit degradation/alert path.
auditspofdata
P2
Complete seed rotation + delete stale key backup
AUDIT P2/P3: the 12-word seed briefly appeared in a 2026-07-19 transcript (user-deferred while balance is low). secret.env.bak-preswap is a stale pre-swap key backup on local disk (gitignored, not committed). Rotate to a fresh seed as the balance grows, then delete the backup. Escalate the nudge as funds increase.
auditsecuritykeys
P2
Node management / streamlining + Mac/Linux installer + signed releases
Enrollment + one-wallet-lock + git-cron reconcile exist for Windows. Streamline multi-node ops (status board, remote health), ship a Mac/Linux installer alongside install.ps1, and sign the dist releases so a fresh operator can trust the binary.
opspackaging
P2
Fresh-machine install test
End-to-end test of install.ps1 + enrollment + board task on a clean Windows box (no dev tooling): venv, secret.env, scheduled task, local board, git-cron reconcile — verify a non-developer can go from zero to a running enrolled node.
opstest
P2
Bio DB: MLB StatsAPI + f1-constructor fix (robustness, NOT a breadth lever)
Review done 2026-07-25 (scratchpad/bio-db-review.md). Both the sport-debug and bio reviews AGREE: a bigger bio DB does NOT add sport breadth. DOB coverage is ~100% by construction (a valid DOB is the DB entry condition; only 13/5000 live rows miss one), and the non-tennis lever is the surfacing fix, not bios. Real drops are a data wall (obscure ITF tennis = 0 Wikidata hits; boxing surname-only markets; unfixable free). Bio work IS worth doing as an ACCURACY/ROBUSTNESS upgrade: P1 = switch MLB bios to MLB StatsAPI (probed live: 1,324 players, 100% DOB+city+country, one keyless GET, +215 over the current ESPN source, ZERO name-match risk via existing person IDs) + reorder lookup.py SOURCES so sport-authoritative APIs come first + make authoritative sources OVERWRITE lower-confidence bios (de-risks MLB from ESPN datacenter-IP blocks, fixes hemisphere accuracy). P2 = fix the f1 'constructor markets' classification BUG (Mercedes/Ferrari teams are being treated as players). ~3/4 day, near-zero risk. Later: NHL/NBA official APIs, golf/soccer tail. All writes via the GAS Sheet (direct database.tsv writes get clobbered).
dataenrichmentscoringrobustness
P2
Paper-trade strong-fav reversion regime
2026-07-25 backtest (backtest/pmlab/reversion_backtest.py): a strong AGREED favorite (~1.2x / price ~0.83) that spikes to ~1.5x reverts. Best config: buy dip <=0.70, stop <=0.50, TP = recover ~70% of the dip depth. Train +2.07%/trade (77% hit) -> test +0.69% (70% hit) => the ONLY +EV regime found, positive on both splits. BUT the edge lives entirely in the spread: frictionless +4.29% -> 2c-haircut -2.20%; the dynamic rank/gamma/theta TP overfits (ship a flat rule); the literal +35-70%-from-entry line is unreachable from a 0.68 entry (proof the flat +50% blended close can't fire here). The bot can't even enter this 1.2-1.5x band today (entries only >=3x). Paper-trade to collect REAL fills (+ multisport breadth), then re-backtest before building a shallow-odds entry regime. Do NOT size up.
strategybacktestscalp
P2
Tune hinge weight + game-date boost per sport (data-driven)
The gap scoring weights are directional PRIORS, not calibrated values: PM_GAP_KEYW=2.5 (hinge/key-role player weighted 2.5x vs 1.0x others) and PM_GAP_BOOST=2.1 (game-date score weighted 2.1x vs the pvp average), in api.php pm_gap_computeScores. User 2026-07-25 confirmed the DIRECTIONS are right (hinge > other players; game-date > pvp) but the exact multipliers are arbitrary until a large labeled dataset exists. TASK: once enough matches with (a) resolution outcomes and (b) the recorded odds-bounce / price-move-vs-own-baseline the algo actually predicts (being wired in via the TP tournament + basket harness + per-bet odds-bounce logging) accumulate, regress the score components (pvp avg, game-date, hinge vs non-hinge contribution) against realized performance and let the data set the ratios PER SPORT. Until then keep 2.5/2.1 as reasonable priors. NOTE the risk is CONTAINED because gap comparison is within-sport only (every match in a sport runs the identical weighting, so relative ordering holds even if absolute magnitude is uncalibrated) -- cross-sport comparison stays disallowed. Prereq: outcome + odds-bounce recording running and accumulating; needs weeks-to-months of multi-sport data.
scoringtuningdatabacktest
P3
Delete stale board scratch/backup files
AUDIT B5 (P3): orphaned + safe to delete — the _w563..w569 / _w563_probe / _trigger2 / _rescore / _wc / _enrich / _race one-shots, and backups index-good.html, index-good2.html, index.live-2026-07-17.html, retro/_archived_index.html, data.csv (1.5MB). KEEP halfbaked.php + data.js (live despite scratchy names).
auditcleanupboard
P3
Defaults single-source (fallbacks read DEFAULT_SCHEDULE)
AUDIT P3: config drift (salvage_stake_frac fallback 0.40 vs config/schedule 0.08) was fixed by aligning literals, but the root cause is duplicated default literals in pmtrade.py vs staking.DEFAULT_SCHEDULE. Have run_loop read defaults FROM DEFAULT_SCHEDULE so a tuned default can never leave a stale code fallback. Also replace the ~7 inspect.getsource string-assertion tests with behavioural ones (subsumed by the run_loop integration test).
auditmaintainability
P3
LNA (Local Network Access) browser test for the local board
The board is served locally (php -S) and read over 127.0.0.1. Verify behavior under browsers' Local Network Access / private-network-access restrictions so the accounting/admin/upgrade pages keep working as browsers tighten localhost access.
boardtest
P3
Realtime SKEW-from-options proxy for cohorts
Add a realtime skew proxy derived from the options chain to the cohorts view (bfm), so cohort divergence carries an options-implied tail signal, not just spot.
bfmcohortsoptions
P3
SPX / NAS reference overlay on per-ETF pages
Per-ETF pages should carry the SPX/NAS baseline on its own independent reference axis (force-fill, 1px) so each ETF is read relative to the index, per the universal chart spec.
bfmcharts
P3
Hero-chart dateline sync
Sync the hero chart's dateline/crosshair with the other time-charts on the page (shared X-axis + linked dateline) so hovering one moves all.
bfmcharts
P3
Supervisor gamma-event refetch cache (minor)
The supervisor refetches each board match's gamma event every 300s poll (single process, low load). A short TTL cache keyed by match_id would trim redundant gamma reads. Not a cross-child duplicate, so low priority.
perfsupervisor
DONE
Same-sport Sofascore shared cache
Children of the same sport pulled the identical live feed every cycle. Now a shared per-sport cache file (state/cache/sofa_<slug>.json, TTL 20s, atomic write) — first stale-reader fetches, others read the file. ~Nx less Sofascore load.
perfcache
pmtrade · local-only · settings resolve schedule → config → default · roadmap in upgrade/todo.json