Skip to content

Latest commit

 

History

History
184 lines (117 loc) · 9.68 KB

File metadata and controls

184 lines (117 loc) · 9.68 KB

StockReplay — Critical Performance Issues

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


CRITICAL

1. fetch_daily_ohlcv() called multiple times per user action

File: app.py — Lines 175, 252, 324 Impact: 3× full stock history downloads per action (trade, state poll, results view)

Every major route independently calls fetch_daily_ohlcv() without sharing the result within the same request lifecycle. On a cache miss this hits yfinance and downloads period="max" — potentially years of daily OHLCV data — just to serve one API response. Even on a cache hit, the file is read and deserialized from disk three separate times per user interaction.

Root cause: No request-scoped caching or memoization of the fetched dataset. Affected routes: /api/scenario/state, /api/scenario/trade, /results


2. Full chart rebuilt from scratch every 2 seconds during auto-play

File: templates/simulation.html — Lines 297–301 Impact: Plotly redraws the entire candlestick chart on every auto-play tick

updateChart() is called every 2 seconds. It passes the complete candles dataset to Plotly.react(), which diffs and re-renders the whole trace. As the simulation progresses and the candle array grows, this gets progressively more expensive. The correct approach is to use Plotly.extendTraces() to append only the new candle.

Root cause: No incremental chart update — full dataset re-rendered each tick.


3. _avg_cost() recalculated on every state request

File: app.py — Lines 202, 416–431 Impact: O(trades) computation runs every 2 seconds during auto-play

_avg_cost() iterates through the entire trades list to compute the average cost basis on every /api/scenario/state poll. The trades list only changes when a new trade is made, but the function is called unconditionally on every state request including read-only polls.

Root cause: No caching of derived values; recalculated even when input hasn't changed.


4. get_current_price() downloads entire stock history for a single price

File: data_service.py — Lines 226–233 Impact: Fetches and deserializes full OHLCV history just to return the latest close

get_current_price() calls fetch_daily_ohlcv() internally, then does sorted(data.keys())[-1] to find the latest date. This is called on the index page, setup page, briefing page, and simulation page — meaning every page load downloads/deserializes the full history to get one number.

Root cause: No lightweight "latest price" path; reuses the expensive full-history fetch. Affected pages: index (line 75), setup (line 102), briefing (line 151), simulation (line 161)


HIGH

5. animateNumber() spawns a new interval timer on every state update

File: templates/simulation.html — Lines 267, 279, 290 Impact: Stacking interval timers cause continuous DOM reflows every 2 seconds

animateNumber() creates a setInterval (12 steps × 20ms = 240ms) for every number it animates. It is called for portfolio value, P&L, and unrealized P&L on every state fetch. If a prior animation hasn't finished before the next tick fires, multiple timers run simultaneously on the same DOM elements, causing layout thrashing and visible jank.

Root cause: No cancellation of previous animation before starting a new one; no requestAnimationFrame.


6. renderTradeLog() rebuilds entire DOM on every state update

File: templates/simulation.html — Lines 318–331 Impact: Full DOM replacement of trade list every 2 seconds

renderTradeLog() calls [...trades].reverse().map(...) to generate HTML for every trade, then sets innerHTML on the container. Every state poll — including polls where no new trade occurred — triggers a full teardown and rebuild of the trade log DOM.

Root cause: No diffing or incremental append; replaces entire list unconditionally.


7. No database index on leaderboard table

File: app.py — Lines 42–56 Impact: Full table scan on every leaderboard query

The leaderboard table is created with no indexes. SELECT * FROM leaderboard ORDER BY profit_pct DESC LIMIT 10 performs a full table scan and sort on every results page load and every /api/leaderboard call. Missing index: CREATE INDEX idx_leaderboard_profit ON leaderboard(profit_pct DESC).

Root cause: Table DDL has no indexes defined.


8. search_ticker() performs O(n) linear scan on every search

File: data_service.py — Lines 160–178 Impact: Iterates the entire STOCK_META dictionary on every search keystroke

The search function loops through every entry in STOCK_META doing string matching with no prebuilt index or prefix tree. As the metadata grows, this scales linearly. This is called on every debounced keystroke from the search box.

Root cause: No inverted index, trie, or sorted lookup structure for ticker/name search.


9. Session marked as modified on every state change

File: app.py — Lines 235–236, 305–306 Impact: Full session re-serialization and storage write every 2 seconds

session.modified = True is set on every state change, even on read-only state polls. This forces Flask to re-serialize the entire session object and write it to storage every 2 seconds during auto-play.

Root cause: Overly broad session modification flag; should only be set when session data actually changes.


MEDIUM

10. Logo images from Clearbit CDN block initial page render

File: templates/index.html — Lines 179–181, 222 Impact: Slow CDN responses delay page load; no lazy loading

Multiple <img> tags load company logos from an external CDN synchronously. If the CDN is slow or a request times out, it stalls the browser's rendering pipeline. There is no loading="lazy" attribute or placeholder strategy.

Root cause: No lazy loading; external resource fetched eagerly on page load.


11. Buy/sell point lookup is O(n×m) on results page

File: templates/results.html — Lines 153–155, 170–172 Impact: indexOf() called on the full candles array for every buy/sell point

When rendering the results chart, the code calls candles.x.indexOf(date) for every trade marker. With a large candles array and many trades, this is O(n×m). A pre-built {date: index} map would reduce this to O(1) per lookup.

Root cause: No pre-built index; linear search repeated per trade.


12. _sorted_dates() called multiple times on the same data

File: scenario_engine.py — Lines 46, 87 Impact: Redundant sort operations on the same dataset

_sorted_dates() sorts the full data keys in both pick_scenario() and _generate_briefing(). The sort is O(n log n) and the same data is sorted twice unnecessarily. The result should be computed once and passed through.

Root cause: No shared sorted-dates variable between functions that operate on the same dataset.


13. P&L animation on results page accumulates interval timers

File: templates/results.html — Lines 226–241 Impact: One interval per .pnl-big element, 30 steps × 40ms = 1200ms per element

Same pattern as issue #5 but on the results page. A setInterval is created for each P&L element with no cleanup. If the page is navigated away from or re-rendered before the interval completes, timers leak.

Root cause: No clearInterval before creating new timers; no cleanup on unmount.


14. Leaderboard modal fully destroyed and recreated on every open

File: static/app.js — Lines 48–105 Impact: Full DOM removal and HTML string rebuild on every modal open

document.getElementById('lb-modal').remove() is called first, then the entire modal HTML is regenerated and injected. There is no reuse of the existing modal element. The modal should be hidden/shown and only its data updated.

Root cause: No modal state management; recreate instead of update pattern.


15. Card hover uses unbounded mousemove 3D transform with no throttling

File: static/app.js — Lines 18–28 Impact: perspective(800px) rotateY() rotateX() recalculated on every mousemove event

3D CSS transforms are recalculated synchronously on every mousemove event with no requestAnimationFrame throttle. At 60+ events/second during mouse movement, this is unnecessary GPU/layout work.

Root cause: No requestAnimationFrame wrapping; no event throttle.


Summary Table

# Severity Issue File Lines
1 CRITICAL fetch_daily_ohlcv() called 3× per action app.py 175, 252, 324
2 CRITICAL Full Plotly chart rebuild every 2s simulation.html 297–301
3 CRITICAL _avg_cost() recalculated every state poll app.py 202, 416–431
4 CRITICAL get_current_price() downloads full history data_service.py 226–233
5 HIGH animateNumber() stacks interval timers simulation.html 267–290
6 HIGH renderTradeLog() full DOM rebuild every tick simulation.html 318–331
7 HIGH No DB index on leaderboard app.py 42–56
8 HIGH search_ticker() O(n) linear scan data_service.py 160–178
9 HIGH Session marked modified on every poll app.py 235–236, 305–306
10 MEDIUM CDN logos block page render index.html 179–181, 222
11 MEDIUM Buy/sell point lookup O(n×m) results.html 153–155, 170–172
12 MEDIUM _sorted_dates() called redundantly scenario_engine.py 46, 87
13 MEDIUM P&L animation timer leak on results page results.html 226–241
14 MEDIUM Leaderboard modal fully recreated on open app.js 48–105
15 MEDIUM Unbounded mousemove 3D transform app.js 18–28