Skip to content

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:

  1. Pattern-based noise (noise.go, noise_pattern.go): a configurable pattern walk that adds synthetic deltas to micro_bump based on time-of-day and activity gates. Controlled by NOISE_ENABLED env var + NOISE_SAMPLE_EVERY_N config. The flusher samples noise via flushNoisyPriceTicks() and writes results to price_ticks with source='synthetic'.

  2. Mean-reverting walk (woven into ticker.go:282–291): writes live_match_until to Redis to gate live-match noise sampling. postmatch.go:296–302 and postmatch.go:654–662 clean up noise_pattern Redis 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:

  1. Eliminates dead code that reviewers must read and reason about.
  2. Makes it impossible to accidentally enable noise/bots via an env var misconfiguration in a deployment manifest.
  3. Reduces the compilation unit and binary size.
  4. 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 via detectStatDeltas), and user trades (AMM bonding curve via position_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_price silently fall through to server_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 in deploy-staging.yml diff.
  • The reviewer for both deletion branches must confirm that internal/replay/, GameplayBotPanel admin routes, and permImpactDecayEveryN/decayPermanentImpact are 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: 0030 in the same PR.

Verification

Post-deploy staging log check:

grep -E "noise engine started|bot engine started" <api-server-logs>

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):

SELECT u.role, COUNT(*) FROM leaderboard l
JOIN users u ON u.id = l.user_id
GROUP BY u.role;

No row with role='bot' should appear.