adr: 0030 title: "Noise engine + trading bot full removal (prices move only from events, stats, trades)" status: accepted implementation-status: pending date: 2026-06-08 implemented-date: null implemented-in: - ftl-backend fix/noise-removal - ftl-backend fix/bot-removal deciders: [@amalkrsihna] affects-specs: [amm-pricing, event-ingestion] affects-code: - ftl-backend/internal/sportmonks/noise.go - ftl-backend/internal/sportmonks/noise_pattern.go - ftl-backend/internal/sportmonks/scoring.go - ftl-backend/internal/sportmonks/ticker.go - ftl-backend/internal/sportmonks/postmatch.go - ftl-backend/internal/sportmonks/scheduler.go - ftl-backend/internal/sportmonks/events_feed.go - ftl-backend/internal/bot/ - ftl-backend/internal/flusher/flusher.go - ftl-backend/cmd/api-server/main.go - ftl-backend/cmd/flusher/main.go - ftl-backend/cmd/ws-server/main.go - ftl-backend/config.go - ftl-backend/deploy-staging.yml - ftl-backend/internal/leaderboard/service.go - ftl-backend/scripts/loadtest/trader_bots.js - ftl-backend/scripts/loadtest/smart_event_traders.js - ftl-backend/scripts/loadtest/ws_minimal.js supersedes: [0018, 0021] superseded-by: null
ADR-0030: Noise engine + trading bot full removal¶
Context¶
Noise engine (ADR-0018, ADR-0021)¶
The noise engine generates synthetic price movements to simulate market activity during periods without football events (between polls, between matches, overnight). It uses two subsystems:
-
Pattern-based noise (
noise.go,noise_pattern.go): a configurable pattern walk that adds synthetic deltas tomicro_bumpbased on time-of-day and activity gates. Controlled byNOISE_ENABLEDenv var +NOISE_SAMPLE_EVERY_Nconfig. The flusher samples noise viaflushNoisyPriceTicks()and writes results toprice_tickswithsource='synthetic'. -
Mean-reverting walk (woven into
ticker.go:282–291): writeslive_match_untilto Redis to gate live-match noise sampling.postmatch.go:296–302andpostmatch.go:654–662clean upnoise_patternRedis hash keys at FT.
In Lua, last_noisy_price is a field in the price computation pipeline. If absent, the
chain falls through to server_price — safe to remove.
The noise engine was built to make the market feel "alive" between real price-moving
events. As of 2026-06-08, NOISE_ENABLED is not set on the production Container Apps.
On staging, it has been confirmed off since the capacity audit. The engine has no active
users.
Trading bot engine (internal/bot/)¶
Three files implement synthetic trading bots: bot.go (core bot logic), bot_wallet.go
(bot wallet management), bot_schedule.go (bot scheduling). Bots are synthetic users
that execute random trades to simulate liquidity. BOT_ENABLED is not set on production
Container Apps (confirmed live 2026-06-08).
The leaderboard queries (leaderboard/service.go:176–179, GetROIFriends,
GetDistrictStandings) do not filter by role != 'bot'. When bots are present in the
users table (seeded in staging), they appear on the leaderboard and inflate metrics.
Any bot users remaining in the staging database after removal will corrupt leaderboard
data.
Sportmonks WebSocket source¶
The WS subscriber (subscriber.go) has a branch for source = "synthetic" in the
handler switch. The corresponding SourceSynthetic constant in the WS package serves
only the noise engine.
Lua last_noisy_price references¶
Two Lua scripts reference last_noisy_price as a Redis hash field. If the field is
absent (as it will be after removal), the Lua chain falls through to server_price.
This is safe — confirmed by tracing the Lua fallback logic.
Why full deletion rather than env-gate¶
The noise and bot engines are not behind env gates in the source — they are wired into
startup unconditionally (main.go imports + scheduler Start() calls). NOISE_ENABLED and
BOT_ENABLED are runtime guards that prevent specific goroutines from executing, but the
code paths, structs, and goroutines are always compiled and partially initialized. This
creates maintenance risk: a future change in an unrelated area could accidentally trigger
a code path that references noise or bot structs. Full deletion:
- Eliminates dead code that reviewers must read and reason about.
- Makes it impossible to accidentally enable noise/bots via an env var misconfiguration in a deployment manifest.
- Reduces the compilation unit and binary size.
- Makes the codebase's stated invariant — "prices move only from events, stats, and trades" — self-enforcing at compile time.
The decision was made 2026-06-08 after confirming that neither system has been active in production or has any planned activation path for WC26.
Decision¶
Delete the noise engine and trading bot engine entirely. The deletion is organized into two independent branches with separate review cycles (merge order matters — see Consequences).
fix/noise-removal — what is deleted¶
| File/section | Action |
|---|---|
internal/sportmonks/noise.go |
Delete entirely |
internal/sportmonks/noise_pattern.go |
Delete entirely |
internal/sportmonks/noise_test.go, noise_pattern_test.go, noise_integration_test.go |
Delete entirely (3 test files) |
internal/flusher/flusher.go |
Delete: flushNoisyPriceTicks(), SetNoiseSampleEveryN(), related fields, noise test helper |
cmd/flusher/main.go:47–55 |
Delete: noise config block (SetNoiseSampleEveryN call) |
config.go |
Delete: 10 noise fields + env parsing (NOISE_ENABLED, NOISE_SAMPLE_EVERY_N, NOISE_MEAN, NOISE_STD, NOISE_MIN_INTERVAL, NOISE_MAX_INTERVAL, NOISE_MATCH_WINDOW, NOISE_ACTIVITY_GATE, NOISE_PATTERN_STRENGTH, NOISE_LIVE_WINDOW) |
cmd/api-server/main.go:306–317 |
Delete: SetNoiseConfig block |
internal/sportmonks/scheduler.go |
Delete: noiseConfig field, SetNoiseConfig() method, noise start-block in Start() |
internal/sportmonks/ticker.go:282–291 |
Delete: live_match_until Redis write (noise gate) |
internal/sportmonks/postmatch.go:296–302 |
Delete: noise_pattern HDel at FT (early) |
internal/sportmonks/postmatch.go:654–662 |
Delete: noise_pattern HDel at FT (late) |
internal/ws/ |
Delete: SourceSynthetic constant + "synthetic" case in subscriber switch |
deploy-staging.yml |
Delete: NOISE_ENABLED env var block |
| Lua scripts | Delete: last_noisy_price field refs (chain falls to server_price — safe) |
scripts/loadtest/ws_minimal.js |
Delete: dead load-test script (noise-only test) |
Protected from deletion (must be absent from diff):
- internal/replay/ — replay engine is NOT a noise component; it is a legitimate
testing tool for replaying historical match fixture data
- GameplayBotPanel admin routes — "gameplay bot" in the admin panel refers to the
replay engine's test interface, not the trading bot engine; it is KEPT
- permImpactDecayEveryN / decayPermanentImpact in flusher — this is ADR-0017
trade-footprint decay; it is NOT a noise component and is KEPT
- TRADE_SLIPPAGE_TOLERANCE_PCT — real trade path; value review deferred post-launch
fix/bot-removal — what is deleted¶
| File/section | Action |
|---|---|
internal/bot/ (entire directory) |
Delete: bot.go, bot_wallet.go, bot_schedule.go |
config.go |
Delete: BotEnabled, BotWallet fields + env parsing (BOT_ENABLED, BOT_WALLET_BALANCE) |
cmd/api-server/main.go:28 |
Delete: bot package import |
cmd/api-server/main.go:277–282, 288, 337–343 |
Delete: bot wiring (init, Start, Stop calls) |
internal/sportmonks/scheduler.go |
Delete: bot import, bot field, bot param in constructor, bot Start/Stop calls |
scripts/loadtest/trader_bots.js |
Delete: dead bot simulation script |
scripts/loadtest/smart_event_traders.js |
Delete: dead bot simulation script |
Add (fix/bot-removal):
internal/leaderboard/service.go:176–179 — add AND u.role != 'bot' filter to the
main leaderboard query, GetROIFriends, and GetDistrictStandings. Prevents any
residual bot users from appearing on the leaderboard in any environment.
Staging database cleanup (after fix/bot-removal merges to integration + deploys):
-- Check FK cascades first (positions, wallets, trades referencing bot user_ids)
SELECT COUNT(*) FROM users WHERE role = 'bot';
-- If FK cascades are safe:
DELETE FROM users WHERE role = 'bot';
# Redis cleanup (bot wallet + leaderboard ZSETs)
redis-cli --scan --pattern 'bot:*' | xargs redis-cli DEL
redis-cli ZREM 'leaderboard:roi:global' <bot_user_ids...>
This is an ops step, not a migration — it runs after the deploy, not via the CA Job migration runner.
Merge order (critical)¶
Deletion branches must merge to integration/wc26-player-market-platform before
feat/propagation-engine (which modifies ticker.go) and feat/admin-event-config
(which modifies deployment YAML). This minimizes merge conflicts: propagation-engine
changes apply cleanly onto a ticker.go baseline that has already had noise sections
removed; admin-event-config changes apply onto a deploy YAML that has already had
NOISE_ENABLED removed.
Sequence: fix/noise-removal → integration → fix/bot-removal → integration →
feat/propagation-engine → integration → (remaining feature branches).
Alternatives considered¶
Env-gate only (keep code, ensure NOISE_ENABLED=false, BOT_ENABLED=0 on all envs). Rejected. The engines remain compiled and partially initialized. Future contributors must read and reason about dead code paths. A missed env var in a new deployment manifest silently re-enables synthetic price movement. The env-gate approach has already failed once: NOISE_ENABLED was absent from production Container Apps as of 2026-06-08 [live measurement] — meaning the engine initialization code ran on every startup without the flag being explicitly false (relying on Go zero-value behavior). Full deletion is safer.
Extract to a separate repo or build tag. Rejected. The engines are tightly coupled to the scheduler and ticker — extracting them requires the same deletions from those files. A build tag achieves the same as env-gate (dead code visible to contributors). The engines have no planned reactivation path; keeping them available via build tag adds maintenance burden with no benefit.
Keep noise for market-hours simulation (between matches). Rejected. The product decision is that prices should not move without a football reason. Synthetic movement between matches misleads users about market integrity and creates customer support risk ("why did my player's price change at 3am?"). Clean behavior between events is a feature.
Consequences¶
- Price movements are limited to three sources: football events (discrete +
propagation, via
processEvents), football statistics (stat-delta viadetectStatDeltas), and user trades (AMM bonding curve viaposition_open.lua). This invariant is compile-time enforced post-deletion — no code path can produce synthetic movement. - No users will appear on the leaderboard with
role='bot'after the bot removal deploy and staging DB cleanup. - Binary size and startup time decrease (noise goroutines were started unconditionally
even when
NOISE_ENABLED=false). - Lua scripts referencing
last_noisy_pricesilently fall through toserver_price(confirmed safe). No Lua behavior change. - 10 noise config fields deleted from
config.go. Any external tooling or documentation that references these env vars is invalid after this deploy. Deployment manifests must not contain the deleted env vars — confirm YAML cleanup indeploy-staging.ymldiff. - The reviewer for both deletion branches must confirm that
internal/replay/,GameplayBotPaneladmin routes, andpermImpactDecayEveryN/decayPermanentImpactare absent from the diffs. These are the three protected components; their accidental deletion would break the test toolkit and the trade-footprint decay model. - ADR-0018 (activity-gated pattern noise) and ADR-0021 (mean-reverting noise walk) are
superseded by this ADR. Both are set
status: superseded,superseded-by: 0030in the same PR.
Verification¶
Post-deploy staging log check:
Both strings must be absent. If either appears, the deletion was incomplete or an import was missed.
Post-deploy staging price check: observe price_ticks table for 60 seconds with no
active replay running. No new rows with source='synthetic' should appear.
Leaderboard check (staging):
No row with role='bot' should appear.