Skip to content

Latest commit

 

History

History
191 lines (132 loc) · 12.4 KB

File metadata and controls

191 lines (132 loc) · 12.4 KB

StockReplay — Functionality Bugs & Errors

Audited: 2026-03-17 Status: Unresolved — no changes made yet


CRITICAL (Completely Broken)

BUG-1: Search Bar Returns No Results

File: static/app.js (search logic), data_service.py:160–223 (search_ticker()) What breaks: Typing any query (e.g., "AAPL", "Apple", "TSLA") returns zero results. The dropdown never populates.

Root cause: The search index in data_service.py is built with lowercase prefixes (e.g., "a", "ap", "app", "appl", "apple"). The search_ticker() function converts the query to lowercase via key = query.lower().strip() on line 202, then does an exact lookup _SEARCH_INDEX.get(key, []). However, the prefix-matching logic only stores whole-word prefixes — it does not store partial mid-word matches or substring matches. So a search for "ppl" (mid-word) returns nothing. More critically, the search depends on the index being populated at startup via _build_index(), but if that function fails silently or the index is empty, all searches return nothing.

User experience: User types in the search box on the home page, waits 300ms for the debounce, sees no results. Dropdown stays empty. There is no error message — just silence.


BUG-2: Leaderboard Link Opens Raw JSON Instead of a Modal

File: templates/base.html:22, static/app.js:30–37 What breaks: Clicking "Leaderboard" in the navigation header navigates the browser to /api/leaderboard, displaying raw JSON on a blank page instead of opening the modal popup.

Root cause: The <a> tag has href="/api/leaderboard" — a direct link to the JSON API endpoint. The JavaScript event listener on line 33 calls e.preventDefault() and then fetchAndShowLeaderboard(). However, if JavaScript is not fully initialized at click time, or if there is any JS error before the click handler runs, the browser falls through and follows the href, navigating to the JSON endpoint. Even when JS is working, the pattern is fragile — href should be "#" or "javascript:void(0)".

User experience: User clicks "Leaderboard" and sees a page of raw JSON like [{"ticker":"AAPL","difficulty":"intermediate","profit_pct":12.3,...}] in the browser with no styling or context. The modal never appears.


BUG-3: Keyboard Shortcuts 'B' / 'S' Do Not Execute Trades

File: templates/simulation.html:576–585 What breaks: The UI hints (B = Buy, S = Sell) imply pressing these keys will execute a trade, but they only move keyboard focus to the input field.

Root cause: Lines 582–583:

case 'b': case 'B': document.getElementById('trade-input').focus(); break;
case 's': case 'S': document.getElementById('trade-input').focus(); break;

Both cases call .focus() only. There is no logic to set the trade action or submit a trade. A correct implementation would call executeTrade('buy') / executeTrade('sell'), or at minimum set the action type and await a quantity from the user before submitting.

User experience: User presses 'B' expecting to buy instantly. Instead, the cursor just moves into the quantity field. The BUY/SELL button still needs to be clicked manually. The keyboard shortcut hint on screen is effectively misleading.


HIGH (Broken in Specific Conditions)

BUG-4: Session Loss During Simulation Silently Redirects User With No Warning

File: app.py:139–145, app.py:251–266 What breaks: If a user's session expires or is cleared mid-simulation (server restart, browser cookie cleared, session timeout), all progress — trades, portfolio value, current day — is permanently lost. The user is silently redirected to the home page with no message explaining why.

Root cause: The entire simulation state (trades, cash, shares, current day) is stored exclusively in Flask's session cookie. There is no server-side persistence of in-progress simulations. When _get_sim() returns None (session missing), the route silently calls redirect(url_for("index")) with no flash message or error context.

User experience: Mid-simulation, the page suddenly reloads to the home screen. The user has no idea their session expired, their trades are gone, and there is no way to recover. This will happen to anyone who leaves the simulation open for a long time.


BUG-5: Invalid Ticker URL Not Validated at Setup — Propagates to Start

File: app.py:174–183 (/setup/<ticker>) What breaks: A user who manually navigates to /setup/RANDOMFAKETICKER can reach the setup page and configure a difficulty. When they click Start, the app calls pick_scenario("RANDOMFAKETICKER", ...), which fetches data that doesn't exist, then renders an error page — but by this point Flask may leave a partial session state.

Root cause: The /setup/<ticker> route does not validate that the ticker exists in STOCK_META or that OHLCV data is available before rendering the setup page. The error is only caught later in /start.

User experience: User can land on a setup page for a fake ticker, choose difficulty, and click Start — only to be shown an error. It should reject the invalid ticker at the /setup step, not after the user fills out the form.


BUG-6: Scenario Can Have Very Short Trading Windows (1–5 Days) Making It Unplayable

File: scenario_engine.py:29–41 What breaks: For stocks with very limited historical data (just over the minimum 60-date threshold), the scenario engine can produce scenarios as short as 1–5 trading days, which are functionally unplayable.

Root cause: Lines 32–33:

if len(all_dates) < trading_days + 10:
    trading_days = len(all_dates) - 10

If len(all_dates) is 11, trading_days becomes 1. There is no lower bound enforced on trading_days after this reduction. The only guard is if max_start < 1: return None, but that check only prevents negative start indices — it does not prevent scenarios of 1, 2, or 3 days from being created.

User experience: On certain obscure or newly listed tickers, the simulation starts and immediately ends, or finishes after 1–2 advance clicks.


MODERATE (Incorrect Behavior / Edge Cases)

BUG-7: Trade Spam — No Rate Limiting or Request Deduplication

File: templates/simulation.html:483–527, app.py (/api/scenario/trade) What breaks: Multiple rapid clicks on BUY/SELL (or simulated calls from the browser console) fire multiple simultaneous trade requests to the server. The server processes all of them, leading to unintended over-buying or over-selling.

Root cause: executeTrade() has no in-flight request guard. After clicking BUY, the button is not disabled, so a second click fires a second request before the first completes. The server also has no per-session trade rate limiting.

User experience: Double-clicking BUY accidentally executes two trades. If a user is clicking fast, they can accidentally buy more shares than intended, or over-sell into a negative position (though the server-side validation should catch the "insufficient shares" case).


BUG-8: Leaderboard Modal Crashes If profit_pct or final_value Is Null

File: static/app.js:62–75 What breaks: If any leaderboard row has a null or undefined value for profit_pct or final_value (e.g., from a corrupted database row), calling .toFixed(2) or .toLocaleString() on it throws a JavaScript TypeError, crashing the entire modal render.

Root cause: Lines 68–72 directly call:

r.profit_pct.toFixed(2)
r.final_value.toLocaleString(...)

There is no null-check or fallback before these calls. One bad database row breaks the entire leaderboard modal.

User experience: User opens Leaderboard, sees a blank/broken modal or a console error. No leaderboard data is displayed.


BUG-9: Auto-Play Can Queue Overlapping Advance Requests

File: templates/simulation.html:360–365 What breaks: If the server takes longer than 2 seconds to respond to a /api/scenario/advance call (e.g., due to a cache miss triggering a full yfinance download), the next auto-play interval fires before the first response arrives. Two advance requests are now in-flight simultaneously, potentially advancing the simulation by 2 days at once and desynchronizing the UI state.

Root cause: The auto-play uses setInterval(..., 2000) without checking whether the previous request has completed. There is no isRequesting flag to skip the tick if a request is in progress.

User experience: Simulation skips days during auto-play when the server is slow. The chart jumps 2 or more candles at once instead of advancing one at a time.


BUG-10: Results Page "Replay" Button May Submit Wrong Ticker Case

File: templates/results.html:197–208 What breaks: The replay form constructs the ticker value from the Jinja2 template variable {{ stock.ticker }}. If the ticker is stored in mixed case (e.g., "Aapl"), it submits that case to /start, which then calls ticker.upper() in app.py — but only on the /start route, not on the /setup route. So the replay correctly starts, but the setup URL could be case-inconsistent.

Root cause: Ticker case normalization is applied inconsistently. app.py does ticker = ticker.upper() in some routes but not all.

User experience: Minor: the ticker may appear in wrong case on the setup page when replaying, but functionally the simulation loads correctly because /start normalizes it.


MINOR (UI / Edge Case Issues)

BUG-11: Progress Bar Has No Upper Bound Clamp

File: templates/simulation.html:287–288 What breaks: If current_day ever exceeds total_days (due to a state bug), the progress bar CSS width exceeds 100%. CSS rendering will likely clip it, but the DOM value will be wrong.

((s.current_day + 1) / s.total_days * 100) + '%'  // no Math.min(100, ...)

BUG-12: animateNumber() Does Not Cancel Previous Timer Before Starting New One

File: templates/simulation.html:267–290 What breaks: If updateUI() fires twice in quick succession (e.g., a fast click on Advance while auto-play is running), two animation timers run on the same DOM element simultaneously, producing flickering numbers.

Root cause: No clearInterval call before creating a new animation on the same element.


BUG-13: renderTradeLog() Rebuilds Even When No New Trades Occurred

File: templates/simulation.html:318–331 What breaks: Every state poll (every 2 seconds) fully rebuilds the trade log DOM even when no trades were made. This causes existing scroll position on the trade list to reset every 2 seconds.

User experience: If a user has scrolled down to see older trades, the view snaps back to the top every 2 seconds during auto-play.


BUG-14: Logo CDN Requests Have No Timeout — Page Can Hang on Slow Network

File: templates/index.html:179–181, 222 What breaks: Stock logo images are loaded from an external CDN. If the CDN is unreachable, the browser opens a connection that hangs until the OS TCP timeout (could be 30–60s), blocking the browser's parallel request limit.

Root cause: No loading="lazy", no timeout, no fallback beyond onerror="this.style.display='none'".


Summary Table

# Severity Description File Lines
1 CRITICAL Search bar returns no results data_service.py / app.js 160–223
2 CRITICAL Leaderboard link shows raw JSON base.html / app.js 22 / 30–37
3 CRITICAL B/S keyboard shortcuts don't execute trades simulation.html 576–585
4 HIGH Session loss silently wipes progress app.py 139–145, 251–266
5 HIGH Invalid ticker not rejected at setup page app.py 174–183
6 HIGH Very short scenarios (1–5 days) possible scenario_engine.py 29–41
7 MODERATE Trade spam — no rate limit or deduplication guard simulation.html / app.py 483–527
8 MODERATE Leaderboard modal crashes on null DB fields app.js 62–75
9 MODERATE Auto-play overlapping advance requests simulation.html 360–365
10 MODERATE Replay button ticker case inconsistency results.html 197–208
11 MINOR Progress bar no upper bound clamp simulation.html 287–288
12 MINOR animateNumber() no timer cancel before restart simulation.html 267–290
13 MINOR Trade log scroll position resets every 2s simulation.html 318–331
14 MINOR Logo CDN requests can hang page load index.html 179–181, 222