StockTracker — Complete System Guide
An NSE Nifty-100 swing & intraday screener with virtual paper trading, live KITE quotes, and 15 trading strategies running every 5 minutes during market hours.
1. Overview
StockTracker scans the Nifty 100 universe every 5 minutes during market hours (09:00–16:00 IST) against 15 hand-tuned trading strategies spanning bull/bear/sideways regimes, both swing and intraday. Qualifying signals are auto-entered into a virtual ₹80,000 paper portfolio. Live market prices come from Zerodha KITE (when configured) or Yahoo Finance as fallback. No real orders are ever placed.
NSE equity, top 100 by market cap
6 Bull-Swing · 2 Bull-Intraday · 4 Bear · 3 Sideways
Virtual · 1 share per signal · Resettable
__call blocker — no place_order code exists anywhere in the codebase.
KITE is used only for live market quotes and historical OHLCV ingestion.
2. Architecture
Three independent layers communicating via MySQL + Redis:
┌──────────────────────┐
│ Frontend (static) │
│ login · dashboard │
│ screener · paper │
│ strategies · zero │
└──────────┬───────────┘
│ /api/*
▼
┌────────────────────────────────────────────────────────────┐
│ PHP API (api/index.php) │
│ ───────────────────────────────────────────────────────── │
│ AuthController · BrokerConfigController │
│ ScreenerController · StrategiesController │
│ PaperTradingController · MarketController │
│ WatchlistController · StockController │
│ │
│ lib/KiteClient.php (READ-ONLY, whitelist enforced) │
└─────────────┬────────────────────────────┬─────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────┐
│ MySQL │ │ Redis │
│ │ │ (cache) │
│ ohlcv_daily │ └─────────────┘
│ ohlcv_hourly │ ▲
│ stocks │ │
│ users │ ┌──────┴──────┐
│ user_broker │ │ │
│ virtual_* │ ┌──────┴──────┐ ┌────┴────────┐
│ strategy_* │ │ Python │ │ Python │
│ screening_* │ │ Ingestion │ │ Strategies │
│ ingestion_log│ │ Worker │ │ Engine │
└──────────────┘ └──────┬──────┘ └─────┬───────┘
│ │
▼ │
┌──────────────┐ │
│ KITE API │◄─────┘
│ │ (live LTP +
│ Yahoo API │ historical)
│ (fallback) │
└──────────────┘
Tech stack
| Layer | Technology | Files |
|---|---|---|
| Frontend | Vanilla JS + Tailwind CDN | *.html · assets/js/*.js |
| API | PHP 8 (no framework) | api/index.php · api/controllers/* |
| Ingestion | Python 3.10+ async (aiohttp) | python/ingestion/ |
| Strategy engine | Python (pandas) | python/screening/ |
| Paper trader | Python | python/paper/ |
| Cache | Redis (optional) | — |
| Storage | MySQL 8 (or MariaDB) | database/*.sql |
| Data feed | KITE Connect (live) · Yahoo (fallback) | kite_historical_client.py · yahoo_client.py · kite_prices.py |
3. Data Flow
Following a single trade from market to paper portfolio:
① 10:15 IST ingest cron fires
┌──────────────────────────────────────┐
│ python main.py ingest │
│ → KiteHistoricalClient (preferred) │
│ → YahooFinanceClient (fallback) │
└──────────────────────────────────────┘
│
▼ upsert
┌──────────────────────────────────────┐
│ ohlcv_daily (1d candles, 2y back) │
│ ohlcv_hourly (1h candles, 60d back) │
└──────────────────────────────────────┘
② Every 5 min strategies cron fires
09:00-16:00 ┌──────────────────────────────────────┐
│ python main.py strategies │
│ → load OHLCV from MySQL │
│ → compute indicators (pandas) │
│ → run 15 strategy fns concurrently │
│ → produce StrategySignal[] │
│ → persist signals │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ strategy_signals (live table, 24h TTL)│
└──────────────────────────────────────┘
│
▼ _paper_trade(signals)
┌──────────────────────────────────────┐
│ PaperTrader.sweep_stale_intradays() │ ← safety net
│ PaperTrader.auto_enter(signals) │ ← BUY/SELL paper
│ PaperTrader.update_unrealized() │
│ PaperTrader.check_swing_exits() │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ virtual_trades (the paper ledger) │
│ virtual_portfolio (cash + tallies) │
└──────────────────────────────────────┘
③ 15:25 IST paper-close cron fires
┌──────────────────────────────────────┐
│ python main.py paper-close │
│ → close all intraday positions │
│ → exit price priority: │
│ KITE LTP │
│ → today's hourly close │
│ → today's daily close │
│ → current_price │
│ → entry_price (last resort) │
└──────────────────────────────────────┘
④ User opens /paper-trading.html
┌──────────────────────────────────────┐
│ PaperTradingController (PHP) │
│ → self-heal stuck-open rows │
│ → sweepStaleIntradays() │
│ → return trades + portfolio + perf │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ Browser renders: │
│ Open Positions · Closed Today │
│ Today's Trades · Performance · etc │
│ LivePrices.fetch() overlays KITE LTP │
└──────────────────────────────────────┘
4. Cron Jobs
4.1 Cron table
| Schedule (IST) | Command | Purpose |
|---|---|---|
15 10 * * 1-5Mon–Fri 10:15 |
$PY $APP ingest |
Morning OHLCV catch-up. Refreshes ohlcv_daily with yesterday's EOD bar (and intraday bars). |
45 15 * * 1-5Mon–Fri 15:45 |
$PY $APP ingest |
EOD OHLCV ingest, 15 min after market close. Today's 15:30 close lands in ohlcv_daily. |
*/5 * * * 1-5Every 5 min, gated 09–16 IST |
$PY $APP screen |
Lightweight technical screener (RSI/EMA/Vol). Populates screening_results for the Screener page. |
*/5 * * * 1-5Every 5 min, gated 09–16 IST |
$PY $APP strategies |
Runs the 15 strategy functions, persists signals, and triggers auto_enter for paper trades. This is the only job that opens new positions. |
25 15 * * 1-5Mon–Fri 15:25 |
$PY $APP paper-close |
Force-close all open intraday positions. Aligned with trade-window close so nothing newer than 15:25 IST can slip past. |
* * * * *Every minute |
$PY scripts/watchdog.py |
Process health check. No trading logic; only alerts/recovery. |
4.2 What each job does
Pulls OHLCV bars from KITE first (if any user's access_token is fresh today)
or falls back to Yahoo Finance. Upserts into ohlcv_daily and ohlcv_hourly.
Why two times? 10:15 catches up after yesterday's missed EOD; 15:45 lands today's close. The strategy scanner runs on this data — fresh OHLCV = accurate signals.
Pure technical filter — computes RSI, EMAs, volume multiples, ATR%, MACD on every Nifty-100 stock,
assigns a 0–100 signal_strength, and writes results to screening_results.
Powers the /screener.html table. Does not open paper trades.
Heart of the system. Runs all 15 strategy functions on each stock; produces StrategySignal
dataclasses with entry, SL, T1/T2/T3, R:R, confidence. Persists to strategy_signals.
Then calls _paper_trade(signals) which:
sweep_stale_intradays()— closes yesterday's intradays that the cron missedauto_enter(signals)— validates trigger + slippage cap + opens paper positionsupdate_unrealized(prices)— refreshes P&L on open tradescheck_swing_exits()— closes swing trades that hit SL/target
Force-closes every open intraday position. Exit price uses the priority chain: live KITE LTP → today's hourly close → today's daily close → stored current_price → latest daily close → entry_price. Logs the actual source used per ticker for audit.
Timed at 15:25 IST to match the trade-window close (no new intraday can be entered after this point, so we capture everything within one sweep).
Health check — verifies the ingest/strategy crons fired on schedule, MySQL is reachable, Redis is alive.
No trading logic. Logs anomalies to watchdog.log.
5. Strategies (15)
All strategies share a common dataclass output (StrategySignal): direction, entry, SL,
T1/T2/T3, risk, R:R, signal_strength (0–100), confidence (HIGH/MEDIUM), and a human-readable reason string.
Each function is a pure transform of pre-computed indicators (_ExtIndicators) and produces
a signal only when every hard condition passes.
5.1 Bull Swing (6 strategies)
| Key | Name | Hold | Edge |
|---|---|---|---|
bb_squeeze | BB Squeeze Breakout | 3–7d | Volatility compression → expansion with volume confirmation |
ema_pullback | EMA Pullback | 5–10d | Buy the dip to EMA20/50 in confirmed uptrend (~60% win rate) |
supertrend_flip | Supertrend Flip | 4–8d | Indicator flip triggers algo entries; momentum continuation |
momentum_52w | 52-Week Breakout | 7–15d | Zero overhead resistance; pure price discovery |
mean_reversion | Oversold Bounce | 3–6d | RSI capitulation in macro uptrends; counter-trend (smaller size) |
vol_accumulation | Volume Accumulation | 7–15d | Smart-money footprint before base breakout |
5.2 Bull Intraday (2)
| Key | Name | R:R T3 | Edge |
|---|---|---|---|
bull_orb_breakout | ORB Long Breakout | 3.5:1 | 9:15–9:30 high broken on 2×+ volume; momentum + short-squeeze |
bull_gap_momentum | Gap & Momentum | 3.8:1 | Gap-up above EMA20 that holds; MACD-confirmed continuation |
5.3 Bear (4: 2 Intraday + 2 Swing)
| Key | Name | Type | Edge |
|---|---|---|---|
bear_vwap_rejection | VWAP Rejection Short | Intraday | Failed bounce at EMA20/VWAP in downtrend; tight R:R |
bear_orb_breakdown | ORB Breakdown Short | Intraday | Full bear stack + 3+ down days; institutional selling momentum |
bear_ema_deathcross | EMA Death Cross Short | Swing 5–12d | Death-cross + RSI 40–62 sweet spot (max downside left) |
bear_flag_breakdown | Bear Flag Breakdown | Swing 5–10d | Weak bounce on contracting volume; flagpole measured-move target |
5.4 Sideways (3: 2 Intraday + 1 Swing)
| Key | Name | Type | Edge |
|---|---|---|---|
sideways_nrd_breakout | NRD Coil Breakout | Intraday | Range < 60% ATR; ATR-based targets = 4–6:1 R:R |
sideways_inside_day | Inside Day Breakout | Intraday | Inside-day in uptrend + ST/MACD; algo buy-stops trigger |
sideways_bb_support | BB Range Support | Swing 3–7d | EMA20≈EMA50 + price at BB lower + RSI oversold + volume spike |
virtual_trades.strategy_key,
strategy_signals.strategy_key, and the JS STRATEGY_NAMES/STRATEGY_COLORS maps.
Changing a key requires a DB migration.
6. Trading Rules
Hard rules enforced by PaperTrader.auto_enter():
- Trade window: No new paper trades outside 09:20–15:25 IST, weekdays only. 5-min buffer at both ends of the 09:15–15:30 NSE session.
- Open-position block: If a ticker already has an OPEN paper position (any strategy), no new entry for that ticker — even from a different strategy.
- Cooldown: If
(ticker, strategy_key)was traded and closed within the last 5 days, skip — wait for cooldown to expire. Different strategy on the same ticker is OK after cooldown. - Confidence: Only HIGH or MEDIUM signals — never LOW.
- Cash check: Skip if
current_balance < live_entry_price. - Trigger validation: For LONG, execute only when
live ≥ strategy_entry. For SHORT, execute only whenlive ≤ strategy_entry. - Slippage cap:
0.5%for intraday strategies,0.75%for swing strategies — skip "chasing" entries where live has already moved too far past the trigger.
Strategy vs Actual prices
Each paper trade stores both:
| Column | Meaning |
|---|---|
strategy_entry_price | The strategy's planned trigger level |
entry_price | The actual live execution price (used for P&L) |
strategy_exit_price | The strategy's planned exit level when an SL/target hit (NULL for MANUAL/INTRADAY_AUTO) |
exit_price | The actual live exit price (used for P&L) |
target1/2/3, stop_loss | Strategy's planned levels (unchanged) |
Position sizing & portfolio
- 1 share per signal. No leverage, no portfolio sizing math.
- Cash deducted on entry, refunded + P&L credited on close.
- Portfolio is a single row in
virtual_portfolio(id=1) — shared across all users in this build. - Reset via
POST /api/paper/resetwith{"confirm":"RESET_CONFIRMED"}body.
7. Pages & UI
All protected pages use a 4-item collapsible sidebar (Dashboard · Screener · Strategies · Paper Trading)
plus a Broker settings button. The sidebar collapse-state persists in localStorage['sb_col'].
auth.js blocks unauthenticated access via a global /api/auth/check probe.
7.1 login.html — Sign in
- Username + password authentication (bcrypt, 5-attempt lockout for 15 min)
- Data Source selector (Yahoo / Zerodha KITE) baked into the form
- When KITE is selected, expands a collapsible block with API Key / Secret / Access Token inputs
- Animated ticker tape at top, shake-on-error, grid background
- Redirects to
/on success
7.2 index.html — Dashboard
- 6 metric cards: Total Signals, Strong, Moderate, Avg Strength, Portfolio Value, Today's P&L
- Top Signals table (2/3 width) — top 10 highest-strength signals from the screener
- Open Positions list (1/3 width) — quick glance at current paper holdings
- Strategy Performance table — per-strategy win-rate, average win/loss, total P&L
7.3 screener.html — Technical Screener
- Full Nifty-100 table with Price · Change% · RSI 14 · Volume× · ATR% · Rules badges · Signal Strength
- Search by ticker / company name; Sector filter; Min Signal Strength slider
- Rule badges (T/M/V/MC/VR/ST) show which technical conditions passed per row
- Click any row → modal with TradingView-style chart (EMA 20/50/200 overlay) via Lightweight Charts
7.4 strategies.html — Strategy Explorer
- Strategy cards grid — one card per strategy with description, edge, hold period, risk level
- Click a card → filter the signals table below to just that strategy's current signals
- Direction badges (LONG / SHORT), Intraday/Swing chips, signal counts per strategy
- Signals table: per-row entry, SL, T1/T2/T3, R:R, confidence, time since signal
- Click signal row → same chart modal as Screener page
- "Screener" link in toolbar to jump to
/screener.html
7.5 paper-trading.html — Paper Trading Simulator
5 tabs across the top:
| Tab | Content |
|---|---|
| Overview | Per-strategy cards: trades count, win rate, avg P&L, recent trades. |
| Open Positions | Live-overlaid current price (KITE LTP if connected), unrealized P&L. Expandable rows show planned vs actual entry, SL/targets with distance %, sector, signal strength, full entry time. Close button per row. |
| Closed Today | 4 summary tiles (count, P&L, wins, losses) + table of trades that exited today. Expandable rows show full entry/exit times, planned vs actual exit price, exit reason. |
| Today's Trades | All trades touching today (opened or closed). Same expandable detail panel. |
| Performance | Strategy-level performance table — total/open/wins/losses, win-rate, avg %, total P&L. |
| History | Paginated list of all historical closed trades. |
- Top toolbar always shows a "PAPER ONLY · No real orders" green badge — visible reassurance that KITE is read-only.
- Portfolio summary bar: Available · Invested · Realized P&L · Unrealized (live-updated) · Win Rate · Today's P&L. The Unrealized tile flashes amber on every live-price refresh.
- Reset button wipes all trades and restores ₹80,000 cash (confirmation modal).
7.6 settings.html — Trading Parameters
Edit any of the 10 tunable trading parameters from the UI instead of source code. Changes apply on the next strategies-cron run (within 5 min). Grouped into four categories:
- Entry Rules — slippage caps (intraday + swing), minimum confidence (LOW/MEDIUM/HIGH), minimum signal strength
- Trade Window — open and close times (IST), enforced against weekday market hours
- Risk Management — cooldown days for same ticker+strategy, stale-swing expiry days
- Sizing — quantity per signal (shares), default initial balance for Reset
Settings are global (single row per key). Backed by the
app_settings table. Python reads via settings_loader.py with hard-coded fallbacks.
7.7 zero.html — KITE Token Exchange
- Configured as the Redirect URL in your Kite Connect app
- Reads
?request_token=XXX&status=successfrom the URL after Zerodha login - POSTs to
/api/broker/kite-token— backend computes the SHA-256 checksum, exchanges for anaccess_token, saves it touser_broker_config - Shows success / error / retry states; auto-redirects to
/after 3s on success - Strips the
request_tokenfrom the URL bar viahistory.replaceState()— won't leak via bookmarks
7.8 Broker Settings modal (sidebar → Broker)
- Data Source toggle (Yahoo / KITE) with explanatory text
- Read-only KITE callout: "no real orders are ever placed"
- Pre-filled credential inputs with masked values (
ab••••yz) — proves a value is saved without leaking the secret - Focus → select-all so paste replaces cleanly; blur with empty restores the masked display
- "Test Connection" button — hits
/user/profileon KITE, prints "Connected as <your-name>" - "Login with Zerodha →" button — redirects to
kite.zerodha.com/connect/login?api_key=...which bounces back to/zero.html - Token age display: "Token last updated 3h ago" / "likely expired" if > 24h
8. Broker / Data Source
Two sources, with KITE strongly preferred when configured:
Yahoo Finance (default)
- Free, no credentials needed
- Provides 2-year daily and 60-day hourly OHLCV
- EOD only — no real-time intraday ticks
- Used as the fallback when KITE is unavailable
Zerodha KITE (preferred)
- Requires Kite Connect subscription (₹2,000/mo + GST)
- Real-time intraday quotes via
/quote/ltp - Historical OHLCV via
/instruments/historical/<token>/<interval> - Access token rotates daily (~6 AM IST) — refresh via "Login with Zerodha"
Daily KITE flow
1. Sidebar → Broker → "Login with Zerodha →"
│
▼
2. Browser redirects to:
https://kite.zerodha.com/connect/login?api_key=YOURKEY&v=3
│
▼
3. User signs in (Zerodha ID + password + TOTP)
│
▼
4. Zerodha redirects to:
https://trade.recruval.in/zero.html?request_token=XXX&status=success
│
▼
5. zero.html JS captures request_token, POSTs to /api/broker/kite-token
│
▼
6. BrokerConfigController computes SHA256(api_key+request_token+api_secret)
POSTs to https://api.kite.trade/session/token, gets access_token
│
▼
7. access_token saved to user_broker_config.kite_access_token
kite_token_updated_at = NOW()
│
▼
8. /api/market/quote, /quote/ltp, /instruments/historical
now all served from KITE for the rest of the day
Where each data source is used
| Consumer | Primary | Fallback |
|---|---|---|
| Historical OHLCV ingest (Python) | KITE | Yahoo |
| Live quotes for paper P&L (PHP/JS) | KITE | Latest DB close |
| Intraday auto-close exit price | KITE | hourly → daily → entry |
| Swing exit price (target/SL hit) | KITE | Today's daily close → target level |
| Manual close exit price | KITE | hourly → daily |
9. Paper Trading Engine
All paper-trade lifecycle methods live in python/paper/trader.py:
| Method | When called | What it does |
|---|---|---|
auto_enter(signals) | Every strategies-cron run | Applies all 7 rules; opens new paper positions at live execution price |
update_unrealized(prices) | Every strategies-cron run | Updates current_price and unrealized_pnl on every OPEN row |
check_swing_exits() | Every strategies-cron run | For each open swing trade, checks today's H/L vs SL/T1/T2/T3; closes if hit. Exit price = KITE LTP → today's close → level |
auto_exit_intraday() | 15:25 IST cron (paper-close) | Closes ALL open intraday positions. Exit price via 6-level priority chain (KITE LTP best, entry_price worst) |
sweep_stale_intradays() | Every strategies-cron + every PHP API call | Safety net — closes any intraday whose entry_time < CURDATE() (cron missed yesterday) |
Exit reasons (enum)
TARGET1,TARGET2,TARGET3— price hit one of the strategy's targetsSTOP_LOSS— price breached the strategy's stopINTRADAY_AUTO— closed by the 15:25 cron (or by the stale-intraday sweep)MANUAL— user clicked the Close button in the UIEXPIRED— swing trade open > 15 days with no target/SL hit; force-closed at latest close
10. Database Schema
| Table | Purpose | Notable columns |
|---|---|---|
stocks | Nifty 100 universe (static) | ticker, company_name, sector, exchange |
ohlcv_daily | 2 years of daily candles | ticker, date, open, high, low, close, volume |
ohlcv_hourly | 60 days of hourly candles | ticker, datetime, OHLCV |
users | Auth (bcrypt-hashed passwords) | username, password_hash, is_active, locked_until, last_login |
user_broker_config | Per-user data source + KITE creds | data_source, kite_api_key, kite_api_secret, kite_access_token, kite_token_updated_at |
screening_results | Latest technical-screener output | ticker, signal_strength, RSI, ATR%, vol_mult, … |
strategy_signals | Live signal table (24h retention) | strategy_key, direction, entry, SL, targets, R:R, signal_strength, confidence, reason |
virtual_portfolio | Single-row cash + tallies | current_balance, total_invested, total_pnl, trades_total/won/lost |
virtual_trades | The paper-trade ledger | direction, is_intraday, entry_price, strategy_entry_price, exit_price, strategy_exit_price, exit_reason, pnl, status |
watchlists | Per-user watchlist | user_id, ticker |
ingestion_log | Per-run audit of OHLCV pulls | ticker, source ('kite'/'yahoo'), status, records_upserted |
app_settings | Tunable trading parameters (Settings page) | setting_key, setting_value, value_type, label, description, category, min_value, max_value |
Migrations
Run in order on a fresh install:
mysql -u app -p stocktracker < database/schema.sql mysql -u app -p stocktracker < database/nifty100_stocks.sql mysql -u app -p stocktracker < database/add_users.sql mysql -u app -p stocktracker < database/add_direction_column.sql mysql -u app -p stocktracker < database/add_strategy_signals.sql mysql -u app -p stocktracker < database/add_paper_trading.sql mysql -u app -p stocktracker < database/add_broker_config.sql mysql -u app -p stocktracker < database/add_strategy_prices.sql mysql -u app -p stocktracker < database/add_app_settings.sql # Optional / on-demand mysql -u app -p stocktracker < database/repair_paper_trades.sql
11. API Endpoints
All routes registered in api/index.php. Public routes
(/api/auth/*) skip the auth guard; everything else requires $_SESSION['user_id'].
| Method | Path | Purpose |
|---|---|---|
| POST | /api/auth/login | Username + password (optionally + data_source + KITE creds) |
| POST | /api/auth/logout | Clear session |
| GET | /api/auth/check | Session probe — returns user + broker config view |
| GET | /api/screener/results | Latest technical screener output |
| GET | /api/screener/stats | Aggregate counts (strong / moderate / total / avg) |
| GET | /api/stocks/universe | Full Nifty 100 list |
| GET | /api/stocks/{ticker}/chart-data | OHLCV for chart modal |
| GET | /api/stocks/{ticker}/indicators | Pre-computed indicators |
| GET | /api/strategies/list | Strategy metadata for cards |
| GET | /api/strategies/results | Current strategy signals (filterable) |
| GET | /api/strategies/summary | Per-strategy aggregate counts |
| GET | /api/paper/dashboard | Portfolio + open count + today's totals |
| GET | /api/paper/trades | Filterable trades list (status, strategy, date) |
| GET | /api/paper/performance | Per-strategy win rates & P&L |
| POST | /api/paper/close/{id} | Manual close at live KITE price |
| POST | /api/paper/reset | Wipe trades + reset cash (requires confirm) |
| GET / POST | /api/broker/config | Get / update data_source + KITE creds (public view masks secrets) |
| POST | /api/broker/test | Verify KITE creds via /user/profile |
| POST | /api/broker/kite-login-url | Build the Zerodha sign-in URL |
| POST | /api/broker/kite-token | Exchange request_token for access_token |
| GET | /api/market/quote | Live quotes (KITE if user is on KITE, else Yahoo EOD) |
| GET | /api/settings | Get all tunable trading parameters with metadata |
| POST | /api/settings | Bulk update — server validates type, range, enum, cross-field |
| GET / POST / DELETE | /api/watchlist | Per-user watchlist CRUD |
12. Safety Guarantees
No real orders — three independent layers
- Code structurally cannot place orders.
KiteClientonly has aget()method — nopost(),put(), ordelete(). TheALLOWED_PATHSwhitelist rejects any path outside read-only market-data endpoints.__callblocks any undefined method invocation with a clear error. - Codebase audit. Zero matches for
place_order,/orders,place_tradeacross the entire repo. The only POST to KITE is/session/token(auth, not orders) and is commented as such. - UI is honest. The Paper Trading toolbar always shows a green "PAPER ONLY · No real orders" badge. The Broker modal repeats this in a green callout when KITE is selected.
Data self-healing
- Stuck-open rows (status='OPEN' but exit_price set) — auto-promoted to CLOSED on every
/api/paper/tradesand/api/paper/dashboardcall. - Stale intraday positions (entry_time < today and still OPEN) — Python sweep runs before every
auto_enter; PHP sweep runs on every API read. Both update bothvirtual_tradesandvirtual_portfolioatomically. - Belt-and-braces filter — Open Positions SQL requires ALL of
status='OPEN' AND exit_time IS NULL AND exit_price IS NULL AND exit_reason IS NULL AND pnl IS NULLso a half-closed row never appears in the open list. - One-shot SQL repair —
database/repair_paper_trades.sqlfor manual cleanup of historical inconsistency.
Credential handling
- Secrets stored in
user_broker_configper-user, never logged. - Frontend only ever receives MASKED versions (
ab••••yz) — full values stay on the server. - Saved credentials shown as masked placeholder in the modal; the masked string is never sent back as a save value.
- Failed login attempts: 5 strikes → 15-min lockout (per
users.locked_until). request_tokenin the URL is stripped viahistory.replaceState()after capture.
13. Troubleshooting
"Intraday trades from yesterday still showing as OPEN"
Likely the paper-close cron didn't fire (server down, cron mis-scheduled). Self-heal will fix on next page load. To confirm:
tail -50 $LOG/paper.log crontab -l | grep paper-close # Should show: 25 15 * * 1-5 $PY $APP paper-close ...
"PAPER trigger skip" appearing in logs
Working as designed. Means a signal fired but the live price had already moved > 0.5% / 0.75% past the trigger — system avoided "chasing" the move. Tune the slippage cap in trader.py (MAX_SLIPPAGE_INTRADAY / MAX_SLIPPAGE_SWING) if too strict.
"KITE token expired or invalid"
KITE access_tokens expire at 06:00 IST daily. Sidebar → Broker → "Login with Zerodha →" to mint a fresh one. The /api/market/quote endpoint silently falls back to Yahoo EOD when this happens — UI shows an amber warning banner.
Wrong times displayed in trade details
MySQL TIMESTAMPs are stored in the server's session timezone (IST). The JS parser treats them as local without appending " UTC". If you see times off by +5:30 hours, server timezone configuration likely changed — check SELECT @@session.time_zone, @@global.time_zone, NOW().
Re-running migrations safely
All migrations are idempotent — they use either CREATE TABLE IF NOT EXISTS or a stored-procedure check against information_schema. Safe to re-run after a code update.
Reset the paper portfolio
curl -X POST http://localhost/api/paper/reset \
-H "Content-Type: application/json" \
-b "PHPSESSID=<your-session>" \
-d '{"confirm":"RESET_CONFIRMED"}'Or simply click Reset in the Paper Trading toolbar.
14. File Map
stockTracker/ ├── api/ │ ├── index.php # Router + auth guard │ ├── config/ │ │ ├── bootstrap.php # Session start │ │ └── database.php # PDO + Redis singletons │ ├── core/ │ │ └── Router.php # GET/POST/DELETE + path params │ ├── lib/ │ │ ├── KiteClient.php # READ-ONLY KITE REST client │ │ └── BrokerConfig.php # Per-user broker config helper │ └── controllers/ │ ├── AuthController.php # Login / logout / check │ ├── ScreenerController.php # Technical screener data │ ├── StockController.php # Universe / chart-data / indicators │ ├── StrategiesController.php # 15-strategy metadata + signals │ ├── PaperTradingController.php # Paper trade lifecycle │ ├── BrokerConfigController.php # KITE creds + token exchange │ ├── MarketController.php # Live quote pass-through │ ├── WatchlistController.php │ └── DiagController.php ├── python/ │ ├── main.py # CLI dispatch (ingest/screen/strategies/paper-close) │ ├── config.py # Dataclass-based settings │ ├── models/ │ │ ├── database.py # get_cursor() context manager │ │ └── cache.py # Redis wrapper │ ├── ingestion/ │ │ ├── worker.py # Async batch ingester (KITE-first) │ │ ├── yahoo_client.py # Yahoo OHLCV (fallback) │ │ └── kite_historical_client.py # KITE historical OHLCV │ ├── screening/ │ │ ├── indicators.py # EMA / RSI / BB / ATR / MACD / Supertrend │ │ ├── strategies.py # 15 strategy functions │ │ └── strategy_engine.py # Concurrent runner + _paper_trade │ └── paper/ │ ├── trader.py # auto_enter / exits / sweep / cooldown │ └── kite_prices.py # Sync KITE LTP for paper-close cron ├── database/ │ ├── schema.sql # Core tables │ ├── nifty100_stocks.sql # Universe seed │ ├── add_users.sql # Auth table │ ├── add_direction_column.sql # LONG/SHORT migration │ ├── add_strategy_signals.sql # Live signals table │ ├── add_paper_trading.sql # virtual_portfolio + virtual_trades │ ├── add_broker_config.sql # user_broker_config │ ├── add_strategy_prices.sql # strategy_entry/exit_price columns │ └── repair_paper_trades.sql # One-shot data cleanup ├── cron/ │ └── stocktracker.cron # ingest / screen / strategies / paper-close / watchdog ├── assets/ │ ├── css/app.css # Sidebar + nav-link styles │ └── js/ │ ├── auth.js # Session guard + global fetch interceptor │ ├── sidebar.js # Sidebar collapse persistence │ ├── api.js # API fetch wrapper │ ├── broker.js # Broker settings modal │ ├── livePrices.js # /api/market/quote helper + DOM overlay │ ├── chart.js # Lightweight-charts modal │ ├── home.js # Dashboard page │ ├── dashboard.js # Screener page (legacy naming) │ ├── strategies.js # Strategy explorer page │ └── paper.js # Paper trading page (5 tabs) ├── login.html ├── index.html (Dashboard) ├── screener.html (Technical Screener) ├── strategies.html (Strategy Explorer) ├── paper-trading.html (Paper Trading Simulator) ├── zero.html (KITE token landing) └── guide.html (this document)
StockTracker · NSE Nifty-100 Swing & Intraday Screener with Paper Trading
Paper trading only — no real orders are ever placed.