v4.0 — multiplayer, 100+ stocks, watchlist, trailing stop, OCO, price alerts, and a rebuilt market engine.
Real-time multiplayer via WebSocket. Trade alongside other players on a shared market — prices move the same for everyone. Live leaderboard, synchronized price ticks, and race-to-the-top portfolio competition. Falls back to local simulation when the multiplayer server is unreachable.
The sidebar now only shows stocks you've pinned to your watchlist. Pin any stock from the main grid by clicking the ★ icon. Default watchlist includes the 7 original stocks, plus any stocks you currently own (shares > 0).
A stop-loss that automatically trails behind the highest price seen since activation. Choose a fixed dollar distance or a percentage trail. When price drops by that distance from the peak, it triggers an automatic sell.
Set a paired stop-loss + take-profit. When either trigger fires, the other is automatically cancelled. Visual connector on the chart shows the OCO pair.
Set a price level and get a toast notification when the price crosses it. No automatic trade — just an alert so you can decide what to do.
All visible scrollbars are now hidden across the entire UI. Scrollable containers still work via gesture / wheel / keyboard, but the scrollbar track is invisible for a cleaner look.
The market engine has been rebuilt to behave like a real financial market instead of a random walk. Prices now move through distinct regimes — sideways ranges, slow grinds, strong trends, accumulation, distribution, exhaustion, panic, and euphoria — each persisting for believable durations before transitioning. Trends emerge from interacting long-, medium-, and short-term biases; pullbacks and continuations occur naturally. Support and resistance levels form from rolling recent extremes, occasionally produce stop-hunt wicks, and flip polarity cleanly on real breakouts. Volatility clusters (calm stretches stay calm, chop stays choppy) and spikes around news. Returns are fat-tailed: large moves are rare but happen far more often than a coin-flip model would predict. Candlestick shapes — dojis, marubozus, hammers, engulfing patterns — emerge from the underlying forces rather than being inserted by hand.
News headlines now drive a richer reaction than a single up-or-down tick. Each event carries an importance (1–5), an impact duration (3–18 seconds), an optional sector tag, and per-stock jitter so the same headline hits different tickers differently. Important news can flip the medium-term trend; minor news barely moves price. Some headlines are mostly priced in (weak reaction), some trigger a "sell the news" reversal, and macro events like Fed decisions and inflation prints affect the whole board at once. The news feed is bundled directly into the server — no more dependency on an external file host.
Fixed NEXT_PUBLIC_WORKER_WS_URL env var reading. The previoustypeof process !== 'undefined' guard broke in Next.js becauseNEXT_PUBLIC_* vars are inlined at build time for client components. Also added a clear console warning when the fallback placeholder URL is used.
On logout, the multiplayer WebSocket connection was not being disconnected before the hard redirect to /login. This left a stale connection that could send the old JWT, causing "token never changes" appearance. Now multiplayer.reset()is called before clearing state — it disconnects, drops all listeners, clears cached leaderboard/feed, and rotates the per-tab session ID.
The worker now sends price updates scoped to what each client is actually viewing. A client on /stock/AAPL receives per-tick updates for AAPL only — not the full 150-symbol price map every second. Dashboard viewers get an aggregated snapshot every ~5 seconds instead of per-tick. Bandwidth and CPU usage drop dramatically on both sides.
The server used to send the full welcome payload (price map + leaderboard + feed) on every WebSocket reconnect — including transient drops within the same session. Now it sends a lightweight welcomeBack ack instead, since the client already has the data. Reconnects are faster and cheaper.
UI settings (color preset, hide-leaderboard, hide-market-feed, etc.) were not applied on page reload — the user had to toggle any setting to force them to take effect. The settings manager now runs applyAll() on every page mount via a root-level SettingsApplier component, so the body classes and CSS custom properties are always in sync with localStorage.
The CSS rule body.hide-player-joins .player-notification was applied to every market-feed row (which all carried the player-notificationclass), so toggling "Show Player Joins" off hid ALL trades, not just join/leave toasts. Removed the class from feed rows; actual join/leave toasts now go through the island toast system and respect the setting in JS.
The "Show Player Joins" setting had no visible effect because join/leave notifications were never actually rendered anywhere. The dashboard now shows island toasts when other players enter or leave the market, gated by the setting.
multiplayer.off() was being called with fresh arrow functions inside cleanups, which silently matched nothing and left the original listeners registered. Each dashboard / stock-view mount leaked ~7 listeners, accumulating across navigations. All listeners now use stable useCallback references so cleanup actually removes them.
Every price tick was calling localStorage.setItem with the entire stock-history map — with 150 stocks updating per tick, that was 150 full-history writes per second. Persistence is now debounced (2s coalesce window) with a quota-exceeded fallback that trims oldest entries.
The dashboard live-ticker interval was 1.5s but the server now pushes updates every 5s. Bumped to 5s to match — DOM updates more often than the data arrives were just burning CPU on no-op reflows.
The worker's connectedCount could go negative under reconnect race conditions. Now clamped at 0 with a guard that no-ops if the player was already removed.
The dashboard's portfolio panels used to flicker back to hardcoded values (e.g. $1,000.00) every time React re-rendered, overwriting the real numbers written by the live game state. Panel values are now bound togameDatadirectly, so re-renders can no longer fabricate values. This was the root cause of the "SUPER unstable portfolio values" bug.
Debounced cloud saves (1s coalesce window) used to be lost when the user closed the tab, hit the back button, or logged out inside that 1s window. A newflushPendingSave() is now called on pagehide,beforeunload, visibilitychange (hidden), beforesetView('dashboard'), before logout, and on StockClient cleanup. All unload-path saves use keepalive: true so they survive the tab being torn down.
Server-confirmed trades were previously persisted via a debounced save, which meant a trade could be silently dropped if the tab closed inside that 1s window. Trades now go through commitGameData() — an immediate, non-debounced save — so the database is always consistent with what the server just confirmed.
GameManager now awaits loadGameData()beforeinitializing the UI, eliminating the race where stale localStorage briefly overwrote the freshly-loaded DB values on every login. If the DB fetch fails, the client falls back to the localStorage cache and shows a non-blocking "Showing cached data" warning instead of silently lying.
Autosave interval shortened from 5 minutes to 60 seconds, but only fires when game data is actually dirty. updateDisplay() anddashboardLivenow skip DOM writes when the value hasn't changed, eliminating needless reflows and the resulting transition re-triggers.