[qb]
Projects

StockSim

MontyPython-HTML·Python·lead contributor

live demo

README

main

StockSim

Replay real market history, trade it, and still have to pay rent.

Hackrice 2026 · Monty Python & HTML

Landing page · API docs · Every setting


What it is

Pick a basket of stocks and replay a real stretch of their history one trading day at a time. You trade against the close while the signals those names actually printed sit on the chart beside you: SMA crossovers, RSI, MACD, volume spikes. The main chart follows one symbol at a time. Next to it you get the whole basket rebased to 100, so correlated names look correlated, and your account equity day by day.

Three things make it more than a chart viewer:

  • The coach teaches on your own chart. When one of your holdings prints a pattern (golden cross, death cross, the MACD crossovers, an RSI extreme, a volume spike), a lesson card explains the pattern your chart is drawing right then, quoting that bar's numbers. Meet a pattern for the first time and it is explained on sight; meet it again and it comes back only after enough sessions have passed to be worth re-reading. The Pattern school panel tracks the syllabus session by session.
  • The market keeps going. Real data runs out eventually. From there the session rolls into a simulated future: a generated price series plus AI-invented news that moves it. Every event carries a scope. A company story moves one symbol, a sector story moves every name you hold in that sector, a macro story moves the whole basket.
  • The bills are real. Starting cash, a paycheck and eight recurring bills come from a Nessie bank account, and as the clock passes each bill's day the money leaves the same balance you trade from. Cash never goes below zero: a bill you cannot cover makes the bank sell shares.

The training levels are five short graded runs. Each one switches off every panel it is not teaching and scores each trade against a single rule, so you can learn the game before you play it.

Prices, sessions, trades, dividends and events all live in TigerData (TimescaleDB).

Quick start

sh
uv sync # installs the project and fetches Python 3.14 if needed cp .env.example .env # then fill in the TigerData connection (see Configuration) # 1. create the tables and seed the ticker catalog (Nasdaq-100 + a few extras) uv run python -m scripts.ingestion.sync_universe --init-schema # 2. load price history for the curated starter set uv run python -m scripts.ingestion.ingest_prices --starter --start 2022-01-01 --end 2024-12-31 # 3. run it ./run.sh # http://127.0.0.1:5000 (Windows: run.bat)

uv sync is the only install step. The stylesheet is committed, so the app looks right straight after a clone and you do not need Node until you change a template.

Prerequisites

You need Why
uv Runs the app, the CLIs and the checks. It reads .python-version and fetches Python 3.14 itself.
A PostgreSQL / TimescaleDB database Everything persistent lives there. TigerData is what this was built against.
Node (optional) Only to rebuild the stylesheet after editing templates.

Configuration

Everything is an environment variable, read through src/config.py. .env.example is the annotated full list; this is the short version of what actually matters.

Required

Variable Notes
PGHOST PGPORT PGUSER PGPASSWORD PGDATABASE The database. PGSSLMODE=require for a hosted instance.
TIMESCALE_SERVICE_URL An alternative to the PG* set if your provider hands you one URL.

Optional, but you probably want it

Variable Default Notes
GEMINI_API_KEY (unset) Turns on the AI coach, AI-invented market events and Gemini-taught lessons. GEMINI_API_KEY2 is read as a second key.
GEMINI_MODEL gemini-3.6-flash Which model the coach and event generator use.
NESSIE_USE_MOCK true true serves the bank from src/temp/nessie_mock_data.json, bills included, no network.
NESSIE_API_KEY, NESSIE_DEFAULT_CUSTOMER_ID Needed once NESSIE_USE_MOCK=false, after seed_nessie has created a sandbox customer.

Tuning. .env.example documents each one properly. The groups are the simulated future (SIMULATION_*), the events that bend it (SHOCK_*), the pattern curriculum (PATTERN_*), gameplay pacing (RANDOM_EVENT_PROBABILITY, PREDICTION_INTERVAL_DAYS, MIN_DAYS_BETWEEN_EVENTS, MAX_WATCHLIST) and the connection pool (DB_POOL_*).

Running without an API key

You do not need a Gemini key. This is a supported way to run it, not a broken one:

  • predictions are skipped and the coach panel says why, disabling its button instead of failing silently (GET /api/ai/status reports the reason);
  • market events fall back to a locally generated headline, so the simulated market still moves during a demo;
  • pattern lessons come from the built-in syllabus in src/scripts/game/patterns.py.

The AI runs through an MCP server the Flask app spawns as a subprocess (python -m mcp_server.server, stdio transport), so a model call never blocks a request.

Loading market data

ingest_prices is the only way prices get in. --start and --end are required, and anything in the catalog without history shows up greyed out on the landing page.

sh
# the curated 50-name tech starter set uv run python -m scripts.ingestion.ingest_prices --starter --start 2022-01-01 --end 2024-12-31 # a handful of names uv run python -m scripts.ingestion.ingest_prices --ticker AMD,AVGO,PLTR --start 2022-01-01 --end 2024-12-31 # every ticker in a universe (comma-separate for several: nasdaq100,us_tech_extra) uv run python -m scripts.ingestion.ingest_prices --universe nasdaq100 --start 2020-01-01 --end 2025-12-31

Pass --start far enough back to get real depth: the landing page offers each symbol's own full stored range, so "all history" on a 1980 listing reaches back to 1980. A whole-universe ingest is slow on purpose: yfinance rate-limits, so the loader paces itself.

Three more one-shot CLIs, none of them needed for the quick start:

Command What it does
uv run python -m scripts.ingestion.sync_universe --init-schema Applies schema.sql and seeds/refreshes the ticker catalog. --universe limits it to one.
uv run python -m scripts.ingestion.seed_nessie Creates a Nessie sandbox customer, a funding account and eight recurring bills. --balance, --random, --seed, --list.
uv run python -m scripts.ingestion.backfill_dividends Fills the dividend history the ex-date schedule and reinvestment read. --ticker, --limit, --dry-run.

The checks

Four suites in src/scripts/checks/, 136 checks between them. They are not unit tests. They drive the real engine against the real database the way the page does, read back what the ledger, the cash balance and the JSON the browser receives actually say, then delete the sessions they created.

Suite Checks Covers Needs the app up?
smoke_finance 89 Dividends, bills, paychecks, the return split yes
smoke_ahead 16 The day-ahead chart preview matching the tick that follows it yes
smoke_chart 19 Chart x-axis width, so a growing series does not re-space itself yes
smoke_database 12 Pool saturation, timeouts, dead connections no
sh
./run.sh & # three of the four talk to http://127.0.0.1:5000 cd src uv run python -m scripts.checks.smoke_database uv run python -m scripts.checks.smoke_finance uv run python -m scripts.checks.smoke_ahead uv run python -m scripts.checks.smoke_chart

smoke_ahead and smoke_chart take --keep to leave their rows behind to poke at. Each suite prints n/n checks passed and exits non-zero on any failure.

Project layout

src/
  app.py                    Flask app, the two page routes, the JSON error handler
  config.py                 every setting, read from the environment
  routes/game_routes.py     all /api/* handlers (thin: they parse, then call engine)
  mcp_server/               the MCP server and its Gemini tools
  scripts/
    game/                   engine (sessions, trades, the clock), indicators,
                            price_source / simulation (real vs generated prices),
                            events, expenses, dividends, patterns, levels, news,
                            performance, price_cache
    database/               schema.sql, and every read and write
    api/                    nessie, finance, and the MCP client
    ingestion/              the CLIs above, plus the ticker universes
    checks/                 the four smoke suites
  templates/                index.html (setup) and game.html (the game screen)
  static/
    input/input.css         Tailwind source        -> prod/output.css (committed build)
    js/game.js              the frontend: play/pause/speed, trades, panels
    js/chart-setup.js       TradingView Lightweight Charts: price, RSI, MACD,
                            the rebased basket and the equity curve
public/                     the standalone landing page: static HTML/CSS/JS, not served by Flask

The two page routes are / (pick a basket or a training level) and /game/<session_id>.

Front-end styles

Templates use Tailwind v4. src/static/prod/output.css is a built, committed file, so the app looks right straight after a clone. If you edit a template or a JS file, though, a new class will not exist until you rebuild:

sh
cd src npm install # once npm run build:css # or: npm run watch:css

Troubleshooting

The clock stops and the page says to retry. Every request thread shares one small connection pool, and DB_POOL_MAX (12) is how many ticks can be in flight at once. A full pool returns 503 with Retry-After rather than hanging. GET /api/health reports pool usage without a debugger.

A code change did not take effect. Both launchers pass --no-reload, so the server does not pick up edits on its own. Restart it.

Nessie looks down. The base URL is https://api.nessieisreal.com; the API refuses connections on port 80, which is what makes it look like an outage.

Bills or salaries are missing for a session. Finances come from a Nessie customer. Either keep NESSIE_USE_MOCK=true, or run seed_nessie and point NESSIE_DEFAULT_CUSTOMER_ID at the customer it prints.

A symbol is greyed out on the landing page. It is in the catalog but has no price history, so run ingest_prices for it.

Docs

  • docs/API.md: every route, the full session state object, how the simulated future and its shocks work, bills and the bank account, the Daily Ledger, teacher mode, the training levels, and a file-by-file map.
  • .env.example: every setting, annotated.