1. How a campaign flows
| Step | What happens |
|---|---|
| Create | You set budget, target, stop loss, price band, sector, automation and execution mode. Status becomes draft. |
| Research / Start | The engine filters a universe by price band and sector, then scores each candidate (with AI if a provider key is set). Status becomes researching, and every candidate is saved as SELECTED or REJECTED with a reason. |
| Plan review | In Approval mode status becomes ready and waits for you. In full/semi auto it buys immediately. |
| Execute | Risk checks run per position (cash, position cap, trades/day, daily loss, confidence). Passing checks create positions in pending, which become open on fill. |
| Monitor | The background loop reconciles order status, enforces the intraday exit window, and checks targets and stop losses as prices update. |
| Exit | Positions close on target, stop loss, trailing stop, campaign target, daily loss limit, intraday forced exit, or your manual close. Status becomes closed with gross, estimated charges and net P&L. |
2. Dashboard — create campaign
| Option | What it does |
|---|---|
| Name | Label for the campaign. Campaigns are fully isolated: separate budget, rules, positions and history. |
| Budget ₹ | Maximum this campaign may spend. Remaining cash is tracked per campaign. |
| Profit Target % | Goal as a percentage of budget. Combined with Target Mode it decides when the whole campaign exits. |
| Stop Loss % | Per-position exit level below entry. 3% on a ₹100 entry exits near ₹97. |
| Trailing Stop % | Raises the stop as price makes new highs. 0 disables. At 2%, ₹100 → ₹120 lifts the stop to ≈₹117.60. |
| Min AI Confidence % | Only candidates rated at or above this may be bought. Higher means fewer, stronger positions. |
| Max Positions | Caps simultaneous open positions. The budget is split across the planned positions. |
| Daily Loss Limit ₹ | Circuit breaker: once realised losses reach it, buying stops and open positions are closed. |
| Price Category | Share-price band filter (below ₹20, ₹20–50, ₹50–100, ₹100–250, ₹250–500, ₹500–1,000, ₹1,000+, or Low/Mid/High/All). |
| Sector | All, banking, IT, energy or telecom. |
| Trade Type | Delivery (held overnight) or Intraday (squared off the same session, subject to the cutoff times). |
| Target Mode | Whole Campaign Target (exit everything at the goal) or Each Stock Target (apply the target per position). |
| Automation | Research Only · Approval Required · Semi Auto · Full Auto. |
| Execution | Paper Trading (simulated fills) or Angel One Live (real orders, gated). |
| Campaign Chat Instruction | Natural-language brief. Parsed for budget, target %, stop loss %, confidence %, max positions, price band, sector and intraday/delivery. |
3. Dashboard panels
| Panel | Meaning |
|---|---|
| Market status | IST session state (OPEN 09:15–15:30 on trading days) and the phase: entries_allowed before the intraday cutoff, manage_only after it, then exit_window for the forced square-off. |
| Headline stats | Campaign count · positions open/pending/closing · realised net P&L (after estimated charges) · unrealised P&L on open positions · whether live trading is LOCKED or UNLOCKED. |
| Campaigns | Budget, remaining cash, target, trade type, automation level, status. Open to control the campaign. |
| Live trades | Positions currently open, waiting for a fill, or mid-exit. |
| Recent orders | Raw order log with broker name and broker order id, so you can confirm what was really sent. |
| Trade history | Closed trades with net P&L, exit reason and whether the trade came from a campaign or was manual. |
4. Campaign page
| Control | What it does |
|---|---|
| Research / Start | Runs the AI scan and produces the plan. Blocked while the campaign is paused. |
| Approve & Execute Plan | Shown when a plan is ready; places the approved positions after risk checks. |
| Pause / Resume | Stops new automated actions, then restores monitoring. Existing positions stay visible either way. |
| Stop / Enable Auto Buy | Blocks or allows new positions. Stopping it keeps current positions monitored. |
| Stop / Enable Auto Sell | Disables or restores automatic target and stop-loss exits. With auto-sell off you must close positions manually. |
| Close All Positions | Submits exits for every open position and completes the campaign. |
| Campaign chat | Change rules or issue commands in plain English (pause, resume, stop buy, close all, research, "target 7%", "minimum 85% confidence", "max 3 companies"). Everything is stored on the timeline. |
| Active rules | The live rule set currently applied to this campaign. |
| AI research plan | Every scored candidate with price, confidence, rating, SELECTED/REJECTED and the reason. The best place to judge AI stock-picking quality. |
| Live / pending trades | Entry, last price, target, stop. Paper Price Tick simulates a price move — crossing target or stop fires the exit rules. Close Manually exits now. |
| Trade history | Gross P&L, estimated charges, net P&L and exit reason per closed trade. |
| Campaign timeline | Audit trail: scans, selections, rejections, risk blocks, submissions, fills, closures. |
5. Status glossary
| Status | Meaning |
|---|---|
| draft | Campaign created, nothing researched yet. |
| researching | A scan is in progress. |
| ready | Plan prepared and waiting for your approval. |
| waiting | No candidate passed the current rules. |
| active | Positions are being monitored. |
| paused | New automated actions suspended. |
| completed | Everything is closed and the campaign is flat. |
| stopped | Halted by the daily loss limit. |
| failed | The last run raised an error; see the timeline. |
| pending → open → closing → closed | Trade lifecycle: submitted, filled, exit submitted, exited. rejected means the broker or a risk check refused it. |
6. Settings reference
| Setting | Purpose |
|---|---|
| Deployment & network — Where this app is running, what the outside world sees, and which URL and IP are registered with the broker. The panel above detects these automatically; these fields are the ones you want saved. | |
PUBLIC_BASE_URL | The https:// URL this app is served from, e.g. https://trade.example.com. Leave blank and the app derives one from whatever address you are browsing on. |
ANGEL_STATIC_IP | The public IPv4 you whitelisted in the SmartAPI portal. Saved here so the app can warn you the moment your IP drifts, which would silently start rejecting live orders. |
ANGEL_REDIRECT_URL | The redirect URL registered against your API key in the portal. Recorded here so the portal value and this deployment stay in sync. |
PUBLIC_IP_LOOKUP_URL | Keyless HTTPS endpoint used to discover the IP the internet sees. Clear it if you would rather not call a third party; detection then reports the IP as unverified. |
| Safety & realism — The master kill switch and how honestly paper fills are modelled. | |
TRADING_ENABLED | Master switch for OPENING positions. Turning this off blocks new buys but still allows exits, so a halt can never trap you in a position. Persisted, so it survives a restart. |
PAPER_SLIPPAGE_BPS | Fills are moved against you by this many basis points (1 bp = 0.01%). 5 is a realistic default for liquid NSE names. Set 0 for frictionless, optimistic fills. |
| AI / Research — Which AI provider rates stocks, and how many calls it may make. | |
LLM_PROVIDER | Which AI provider rates stocks. "auto" uses the first provider that has a key saved. |
LLM_RESEARCH_ENABLED | When on, the AI rates candidates and overrides the built-in placeholder scores. When off, only the offline model is used. |
GEMINI_API_KEY | Google AI Studio key (starts with AIza). Free tier available at aistudio.google.com/apikey. (secret — masked in the UI) |
GEMINI_MODEL | e.g. gemini-2.5-flash (fast/cheap) or gemini-2.5-pro (stronger). |
OPENAI_API_KEY | Alternative to Gemini. Starts with sk-. (secret — masked in the UI) |
OPENAI_MODEL | e.g. gpt-4o-mini. |
ANTHROPIC_API_KEY | Alternative to Gemini/OpenAI. Starts with sk-ant-. (secret — masked in the UI) |
ANTHROPIC_MODEL | e.g. claude-3-5-sonnet-latest. |
LLM_MAX_CANDIDATES | Caps how many stocks the AI rates per research run. This is the only real driver of AI cost: each call is a fraction of a paisa, so the cap is about speed and rate limits more than money. See AI usage & cost below. |
USD_INR_RATE | Used to show estimated AI spend in rupees. Set it to your card rate so the cost panel matches your statement. |
TRADINGAGENTS_ENABLED | Advanced multi-agent research. Requires the tradingagents package plus its own keys. Falls back to the simple AI rating on error. |
| Price feed & market data — Where live prices come from, and the historical window the Fetch fresh data button downloads. Without a feed the exit rules can never fire on their own; without real history a backtest cannot tell you anything. | |
QUOTE_SOURCE | Where live prices come from. none = manual price ticks only. replay = walk one bar per poll through data/history CSVs (great for watching exit rules fire). angel = real Angel One last-traded prices. |
QUOTE_REFRESH_SECONDS | How often to pull fresh prices. This is what lets target, stop-loss and trailing-stop rules fire without manual ticks. |
QUOTE_REPLAY_SOURCE | Where replay bars come from. auto prefers data/history/*.csv, then Yahoo Finance, then Alpha Vantage, then synthetic. |
HISTORY_REFRESH_DAYS | Default window for the Fetch fresh data buttons on Backtest and here. 400 calendar days is roughly 18 months of trading sessions. |
QUOTE_REPLAY_DAYS | How many daily bars to load per symbol for replay mode. |
QUOTE_REPLAY_LOOP | Restart from the first bar when replay runs out of history. |
| Notifications — Webhook, Telegram and email alerts for events that matter. | |
NOTIFY_ENABLED | Master switch for outbound notifications. Alerts are best-effort and never interrupt trading. |
NOTIFY_EVENTS | Comma-separated keys: position_opened, position_closed, campaign_completed, daily_loss_limit, order_rejected, global_halt, intraday_force_exit, risk_block, warning, error. |
NOTIFY_WEBHOOK_URL | Receives a JSON POST with app, key, level, title, message and campaign_id. Works with Slack/Discord/ntfy style endpoints. |
NOTIFY_TELEGRAM_BOT_TOKEN | From @BotFather. Needs NOTIFY_TELEGRAM_CHAT_ID too. (secret — masked in the UI) |
NOTIFY_TELEGRAM_CHAT_ID | The chat, group or channel id to post into. |
NOTIFY_SMTP_HOST | e.g. smtp.gmail.com. Needs from/to addresses; set user+password to authenticate. |
NOTIFY_SMTP_PORT | 587 for STARTTLS (default), 465 for implicit TLS, 25 for plain. |
NOTIFY_SMTP_USER | Usually the full email address. |
NOTIFY_SMTP_PASSWORD | An app password, not your normal account password. (secret — masked in the UI) |
NOTIFY_EMAIL_FROM | Sender address. |
NOTIFY_EMAIL_TO | Recipient address. |
| Angel One broker — Credentials and the safety gates for real-money orders. | |
DEFAULT_BROKER | Paper simulates fills locally. Live routes real orders to Angel One. |
ALLOW_LIVE_TRADING | Master live switch. Also needs the acknowledgement text and a registered static IP. |
LIVE_TRADING_ACK | Must be exactly: I_UNDERSTAND_LIVE_TRADING_RISK |
ANGEL_API_KEY | From the Angel One SmartAPI developer portal. (secret — masked in the UI) |
ANGEL_CLIENT_CODE | Your Angel One login ID, e.g. A123456. |
ANGEL_PASSWORD | Your Angel One trading password (not the app PIN). (secret — masked in the UI) |
ANGEL_TOTP_SECRET | The base32 seed behind your 2FA QR code, used to generate the 6-digit code at login. (secret — masked in the UI) |
ANGEL_STATIC_IP_REGISTERED | Set true only after registering your server IP with Angel One, otherwise live orders are rejected. |
ANGEL_INSTRUMENT_MASTER_URL | Angel One symbol/token master used to resolve NSE tradingsymbols. |
| Risk & runtime — Defaults for new campaigns and the daily session timetable. | |
DEFAULT_TARGET_PCT | Pre-filled target for new campaigns. A target is a goal, never a guarantee. |
DEFAULT_STOP_LOSS_PCT | Pre-filled stop loss for new campaigns. |
DEFAULT_MAX_DAILY_LOSS | Pre-filled daily loss cap. When hit, new buying stops and open positions are closed. |
DEFAULT_MAX_OPEN_TRADES | Pre-filled cap on simultaneous positions. |
MONITOR_INTERVAL_SECONDS | How often the background loop reconciles orders and checks targets. Minimum 5 seconds. |
INTRADAY_STOP_NEW_ENTRIES | IST clock time (HH:MM) after which no new intraday position is opened. Default 14:45. |
INTRADAY_FORCE_EXIT | IST clock time (HH:MM) when intraday positions are squared off automatically. Default 15:05. |
MARKET_HOLIDAYS | Comma-separated ISO dates treated as closed, e.g. 2026-01-26,2026-03-04 |
| Dashboard access — Login protection for the web UI. | |
AUTH_MAX_FAILURES | Per source IP, within the lockout window. |
AUTH_LOCKOUT_SECONDS | How long a source IP is blocked after too many failed logins. |
DASHBOARD_REFRESH_SECONDS | How often the dashboard polls for updated prices and P&L. 0 disables auto-refresh. |
AUTH_ENABLED | Keep on for any internet-facing deployment. Turn off only on a trusted local machine. |
ADMIN_USERNAME | HTTP Basic username. |
ADMIN_PASSWORD | Change this from CHANGE_ME_NOW before exposing the app. (secret — masked in the UI) |
| System — Read-only runtime information. | |
APP_ENV | Informational label reported by /health. |
DATABASE_URL | SQLite file path. Changing this requires editing .env and restarting, because the engine is bound at startup. |
Currently active AI provider: gemini.
7. AI research modes
| Mode | Behaviour |
|---|---|
| No key configured | Offline placeholder scores. These are not real signals — use them only to exercise the workflow. |
| Gemini / OpenAI / Anthropic key | Up to LLM_MAX_CANDIDATES candidates per scan are rated by the model, which returns a rating, a confidence and a one-sentence reason. Results are cached per symbol per day to avoid repeat billing. |
| TradingAgents enabled | The multi-agent research graph overrides the simple rating. Needs its own package and provider keys; on failure the platform falls back instead of trading on an unvalidated signal. |
8. HTTP API
| Endpoint | Purpose |
|---|---|
GET /health | Unauthenticated health probe used by Docker. |
GET /, GET /campaign/{id} | Dashboard and campaign pages. |
GET /settings, GET /help | This page and the settings page. |
GET /api/market/status, GET /api/market/candidates | Session state and candidate lists. |
POST /api/campaigns, GET /api/campaigns | Create and list campaigns. |
POST /api/campaigns/{id}/start|execute-plan|pause|resume|close-all|stop-buy|start-buy|stop-sell|start-sell | Campaign control. |
POST /api/campaigns/{id}/chat | Natural-language campaign instruction. |
PATCH /api/campaigns/{id}/rules | Change targets, stops, confidence and position cap. |
POST /api/trades/manual | Manual buy outside the campaign logic. |
POST /api/trades/{id}/price, POST /api/trades/{id}/close | Price tick (paper) and manual exit. |
GET /api/trades, GET /api/orders | Trade and order listings. |
GET /api/broker/funds | The balance strip in the header: paper figures from the database, plus the real Angel One balance when credentials exist. Read-only, so it works while live trading is still locked; ?refresh=1 bypasses the cache. |
GET /api/broker/account | Live funds, holdings and positions (locked until live is enabled). |
GET/POST /api/settings, POST /api/settings/test/{llm|broker|database} | Read, save and connection-test settings. |
GET /backtest, GET /backtest/{id} | Backtest page and a saved run. |
POST /api/backtest/run | Run a backtest (or a signal comparison with compare:true). |
GET /api/backtest/runs, DELETE /api/backtest/runs/{id} | List and delete saved runs. |
GET /api/ai/status | Which AI provider is active. |
GET /api/summary | Compact dashboard figures used by auto-refresh. |
GET /api/global/status, POST /api/global/halt, POST /api/global/resume | Kill switch. Halt optionally flattens every open position. |
GET /api/quotes/status, POST /api/quotes/refresh, POST /api/quotes/reset | Price feed state, forced refresh, and replay rewind. |
GET /api/notify/status, POST /api/notify/test | Notification channels and a real test alert. |
GET /api/export/trades.csv, GET /api/export/orders.csv, GET /api/export/campaign/{id}.csv | CSV downloads for records and tax. |
POST /api/settings/test/{llm|broker|database|notify|quotes} | Live connection tests from the Settings page. |
9. Backtesting ("would this have worked?")
Open Backtest in the navigation. The engine replays daily history one session at a time and drives the same code that runs paper trading — the real target/stop/trailing logic, the real risk checks and the real Indian charge model — against a throwaway database, so a backtest can never touch your live campaigns.
| Option | What it does |
|---|---|
| Symbols | Comma-separated NSE tickers to replay. |
| Sessions to replay | How many sessions to walk; the first few serve as SMA warm-up. |
| Entry signal | momentum (fast SMA above slow SMA), mean_reversion (buys dips below the slow SMA), random (a deliberate no-edge control), ai (your LLM rates each symbol each session). |
| Price source | auto tries CSV → Yahoo Finance → Alpha Vantage → synthetic. Pin it to csv after fetching data so a run can never quietly use synthetic prices, or to yahoo to pull real prices on each run. |
| Fast / Slow SMA window | Moving-average lengths used by the momentum and mean-reversion signals. |
| Target / stop loss / trailing / confidence / max positions / daily loss | The same risk rules as a live campaign. |
| Target mode | Per-position targets, or stop the whole run when the campaign goal is reached. |
| Trade type | Delivery holds across sessions; intraday squares off at the end of every simulated session. |
| Run backtest / Compare signals | Run one signal, or replay several over the same window alongside buy & hold. |
How to read the result. The metric that matters most is Excess vs buy & hold. Beating nothing is not an achievement: if a signal cannot beat simply holding the same stocks, the trading costs and timing are destroying value. The random control is the second check — a real signal should beat it. Always check the source badge: if it says SYNTHETIC DATA, the numbers are a plumbing test, not evidence.
Step-by-step: your first real backtest
- Open Backtest in the navigation.
- In the Fetch fresh data panel, leave Symbols as
ALL(or type e.g.SBIN,RELIANCE,INFY), set Days of history to400, pick Yahoo Finance and overwrite, then press Fetch fresh data. No API key is needed. The table below then shows every symbol on disk with its row count and date range — confirm it says 2024-09-23 to 2026-09-23 or similar, not “nothing on disk”. - Scroll to Run a backtest. Set Symbols to symbols you actually fetched, and Price source to
csv— pinning it means the run cannot silently fall back to synthetic prices. - Set a first Entry signal of
momentum, keep Target 10% / Stop 3%, and press Run backtest. - Read the result. The badge must say
source: csv. If it says SYNTHETIC DATA the run is a plumbing test only. - Now press Compare signals. This replays momentum, mean-reversion and a random control over the same window next to buy & hold. The honest question is whether your signal beats both buy & hold and the random control.
- Saved runs appear under Saved runs; each one opens at
/backtest/<id>with its equity curve and full trade log, and exports are available from there.
A real example from this deployment. With 13 NSE symbols fetched from Yahoo (501 real sessions, 2024-09-23 → 2026-09-23), momentum with a 10% target and 3% stop returned −36.5% against −12.9% for buy & hold — 51 trades, 27.5% win rate, profit factor 0.54, ₹5,525 of charges and a 47% drawdown. That is what testing is for: the strategy lost money and lost to simply holding, so it should not be trusted with real money as configured.
Supplying data by hand instead. You can skip the fetch button and drop one CSV per symbol into data/history/ named after the ticker, e.g. data/history/SBIN.csv. A date column and a close column are required (full OHLCV exports and yfinance-style Adj Close headers are handled; unusable rows are skipped and reported). Save an ALPHA_VANTAGE_API_KEY in Settings to use the alpha source instead. overwrite replaces a symbol’s whole window; append merges new days into the existing file and keeps older bars.
10. How the campaign decides what to buy — and what the AI costs
You do not tell it which stock to buy, and it cannot read your mind. Selection is a filter, not a guess:
- Shortlist. The campaign starts from the platform’s tracked NSE universe and narrows it by your Price category and Sector.
- Score every candidate. With an AI key saved, each candidate (up to Max AI calls per scan) is sent to the provider, which returns a rating (BUY/OVERWEIGHT/HOLD/UNDERWEIGHT/SELL/REVIEW), a confidence 0–100 and a one-sentence reason. With no key, the offline placeholder model scores them instead — those scores are not signals.
- Approve. A candidate is approved only if its confidence is at or above your Minimum AI confidence and its rating is not SELL or UNDERWEIGHT. The rest are rejected, with the reason recorded in the activity feed.
- Rank and cap. Approved candidates are sorted by confidence and the top Max positions are kept. Budget is then split evenly across them.
- Risk-check each order. Cash, position cap, trades-per-day, market session and the intraday entry cutoff are all still enforced. A blocked order is logged as a risk block rather than silently skipped.
- Execute. Whether that final step happens automatically is the Automation level:
| Automation level | What happens after research |
|---|---|
research | Research only. Never places an order — a plan is produced for you to read. |
approval | Waits for you to press Approve & Execute Plan. Nothing is bought until you do. |
full_auto | Buys on its own, within every rule above, as soon as research approves something. |
Ways to steer it without giving it a ticker. Price category, sector, minimum confidence, max positions and trade type are the real controls: raising minimum confidence to 85% and capping max positions at 2 produces “fewer, higher-conviction, cheaper stocks”. Campaign chat understands plain English (“research banking shares, minimum 85% confidence, target 7%”) and applies those same fields. To name the stock yourself, use Buy a specific stock on the campaign page: it places your chosen symbol in this campaign, with your target and stop, and every risk rule still applies.
What the AI actually costs. Billing is per token, and this platform’s research prompt is tiny — about 149 input tokens and 60 output tokens per stock. On gemini-2.5-flash (list price $0.30 per million input, $2.50 per million output) one full scan of 5 stocks is roughly ₹0.086. A thousand campaigns would still be under ₹100. It does not cost ₹1,000 per campaign, and the common failure mode is the opposite of what people fear: the risk is surprise volume, not price per call. The real controls are Max AI calls per scan (caps calls per run), the per-symbol per-day cache (researching the same stock twice in a day is billed once), and provider rate limits. Gemini keys from Google AI Studio also include a free daily request quota, so light testing can cost nothing at all.
Measured spend is shown on Settings → AI usage & cost: real token counts taken from each provider response, spend per campaign, and the most recent calls. Set USD_INR_RATE to your card rate so the rupee figure matches your statement. Your provider invoice remains authoritative.
11. Step-by-step: paper campaign, then a live order
Stage 1 — paper, with real prices.
- Settings → AI / Research: choose
gemini, paste the key, save, then press Test AI provider. Confirm AI research status shows the provider. - Settings → Price feed: set Price source to
replayand Replay data source tocsvso the campaign can actually exit on its own using the history you fetched. - Backtest → Fetch fresh data for your symbols (if you have not already).
- Dashboard → create the campaign: Paper Trading, budget, target, stop, minimum confidence, max positions, price category, sector, and Automation level =
full_autoif you want it to trade by itself. - Open the campaign and press Research / Start. Watch the activity feed: every rating, every rejection reason, and each order appear in order.
- Let the monitor loop run. With the replay feed, positions will hit their target or stop and close themselves; each exit appears in the feed with its net P&L after charges and slippage.
- Judge it: real or placeholder research? What did the AI reasons say? Did anything beat a simple buy & hold? Compare against a backtest over the same symbols before believing it.
Stage 2 — going live, deliberately. Live orders stay impossible until all five gates are green (Settings → Live trading safety gates): ALLOW_LIVE_TRADING=true, LIVE_TRADING_ACK exactly I_UNDERSTAND_LIVE_TRADING_RISK, ANGEL_STATIC_IP_REGISTERED=true, all four Angel One credentials present, and the whitelisted IPv4 saved as ANGEL_STATIC_IP so drift can be detected.
- Read section 12 and register your server’s static IP with Angel One — the panel in Settings shows exactly which address that is. Then fill in
ANGEL_API_KEY,ANGEL_CLIENT_CODE,ANGEL_PASSWORDand theANGEL_TOTP_SECRETseed (not the 6-digit code), plus the same IPv4 asANGEL_STATIC_IP. - Press Test Angel One login. It signs in and reads your balance but never places an order, so it is safe while live is still locked.
- Switch Price source to
angeland confirm Test price feed passes — live exits depend on real prices. - Open the gates one at a time, until the header shows LIVE UNLOCKED.
- Start with a deliberately small budget and Max positions of 1, and Automation level =
approvalso you press the button for every order until you trust it. - Place one order, then verify it on the broker’s own order book against the campaign’s order log. Charges shown here are estimates; the contract note is authoritative.
- Only then move to
full_auto. Keep Notifications on and keep the Halt all trading button within reach — it blocks new buys everywhere while still allowing exits.
12. Deploying it: laptop, LAN or VPS
The correct values for the broker differ completely depending on where this runs, and a machine behind a router cannot know its own public address. So Settings → Where this app is running detects it and shows its reasoning.
| Shown | What it is and why you need it |
|---|---|
| Detected environment | A best-effort label (cloud_vps, container, behind_nat, public_host) with a confidence level and the evidence behind it. Cloud hosts are identified from the hypervisor’s own identity string; containers from /.dockerenv, cgroups and Kubernetes variables. A home machine behind NAT is genuinely unprovable, so it is labelled at medium confidence rather than guessed at. |
| Public IP | The address the internet sees — the one your broker will see. Found via a keyless HTTPS lookup, because behind a router the machine only knows its private address. Clear PUBLIC_IP_LOOKUP_URL if you would rather not call a third party; detection then reports it as unverified. |
| Local IP | This machine’s address inside its own network (e.g. 192.168.1.24). Useful on a LAN, and never the address to whitelist with a broker. |
| MAC address | The physical hardware address of each adapter, read from getmac on Windows or /sys/class/net on Linux. Useful for identifying the machine on a network, for MAC-based DHCP reservations (so a home IP stops changing), and for your own records. |
What to type into the portal. The SmartAPI “Add App” form has five fields, and the panel gives the value for each of them for one deployment, not a menu: App Name, Redirect URL, Post back URL (blank), Primary Static IP, Secondary Static IP (blank). It picks local or VPS from how you are reaching the app, and you can switch the target if you are filling the form in for a server you have not moved to yet. Press Copy beside any value and paste it straight in.
The Redirect URL is required by the form. This app authenticates with TOTP, so it never calls that URL — but the path /auth/callback is a real route on this server, so what you register resolves instead of 404ing. A registered URL that is a dead end is a trap: it looks correct until someone tries the web login flow. The static IP is the same value whether you run locally or on a VPS, and that is the part most setups get wrong: Angel One whitelists the address your orders leave from, so an app on a laptop places orders from the home connection’s public IP. Locally that address changes, and the portal allows only one static-IP change per week — which is why a laptop is fine for testing and a poor foundation for live orders.
Ignore the X-ClientPublicIP header. The installed SmartAPI SDK (1.5.5) sets that header to a hardcoded IP inside a finally block — and a finally always runs — so the value it reports is not your machine's address. If you ever see an unfamiliar IP in a broker support ticket, that is why. Angel One enforces the whitelist against the source IP your orders actually come from, which is the address the panel detects. The panel checks the installed SDK for this and warns you when it finds it, so the correction disappears by itself if upstream fixes it.
Static IP, and why drift matters. Angel One now requires orders to come from a whitelisted static IPv4. Only order and GTT calls need it — login, market data and historical data work from any address. So a drifting IP produces the worst possible failure: everything looks healthy, and only order placement fails. Save the address as ANGEL_STATIC_IP and this app compares it against reality on every Settings visit, warning you the moment they diverge. Two further details worth knowing: only IPv4 is supported (an IPv6-only egress cannot be registered), and after registering you should make one successful API call from that exact IP within 24–48 hours to confirm it.
| Scenario | What to do |
|---|---|
| Laptop, testing only — or a local run you keep using | Leave live trading locked and use the sheet’s local answers. A home public IP is usually dynamic and often shared behind CGNAT, so it is a poor basis for a live setup; the panel says so rather than quietly handing you an address that will stop working. |
| Home or office server, live trading | Ask your ISP for a static IPv4, or reserve the LAN address by MAC and use a VPN with a fixed egress IP. Then register that public IP, save it as ANGEL_STATIC_IP, and confirm the app reports it as matching. A mobile hotspot or a router reboot will otherwise break order placement while login keeps working. |
| VPS | Put TLS in front (Caddy or Nginx) so you can register an https:// redirect URL, set PUBLIC_BASE_URL to that domain, whitelist the VPS’s public IPv4, and keep AUTH_ENABLED on. Never leave the dashboard password at its default. |
| Container or Kubernetes | Whitelist the egress IP of the cluster, which is often not the node IP and sometimes not stable. Confirm with Test deployment & static IP before trusting it. |
Press Test deployment & static IP in Settings → Test connections to re-run detection on demand. It fails loudly when the IP your broker will see no longer matches the registered one, because that is the failure that matters.
13. Editing settings and clearing data
Every field has a ✕ button. It empties a text field, or restores a number, switch or dropdown to the value declared in config.py — a number cannot be blank, so “clear” reasonably means “back to the default”. Marked fields turn amber and say what will happen; press undo to cancel. Nothing is written until you press Save settings. Read-only fields get no button because they are rejected server-side.
A subtlety worth knowing, because it explains a bug you could otherwise hit: a secret field left blank means “keep the current value”, which is why it is not the same as pressing ✕. Blank protects a stored key from being wiped by an unrelated save. ✕ is the explicit instruction to remove it.
The Danger zone (Settings, at the bottom) is the only place that deletes data:
| Button | What it removes |
|---|---|
| Download backup (JSON) | Every row from every table as JSON. Do this first — it is the only way back. |
| Clear all trading data | Campaigns, trades, orders, research recommendations, campaign chat, campaign events, backtest runs, AI usage records and the audit log. Never touches .env, and never places a broker order. |
| Close positions, then clear | Submits an exit for every open position, waits for it to be recorded, then clears. Refused while live trading is enabled. |
| Clear everything + reset settings | The above, and every setting returns to its default in config.py — including API keys and broker credentials. |
A tick box also deletes the downloaded data/history/*.csv price files, so backtests and replay runs start from nothing.
Two safety rules, both deliberate. Each button needs two clicks — the first arms it and explains what will happen, the second performs it — so no single stray click can destroy anything, and there is no magic string to type. And a clear is refused while any position is open or pending: deleting those rows would not close anything, it would only lose track of money still on the market. Forcing past that is itself refused while live trading is enabled.
One design note the app takes seriously: .env rewrites drop duplicate keys rather than updating only the first one. With two lines for the same key it is whichever the loader reads last that wins, so a save could report success, look correct in the running process, and then silently revert on the next restart.
14. Operating it safely
| Control | What it does |
|---|---|
| Halt all trading | Blocks opening positions on every campaign by switching off auto-buy and clearing TRADING_ENABLED. Exits stay allowed, so a halt can never trap you in a position. The change is written to .env and survives a restart. |
| Halt + close all positions | The same halt, then submits an exit for every open position immediately. |
| Resume trading | Re-allows entries. Each campaign keeps its own auto-buy state, so re-enable that separately if you want it. |
| Refresh prices now | Forces one price update for open positions outside the normal schedule. |
| Reset replay feed | Rewinds the replay price source to its first bar. |
| Export trades / orders CSV | Downloads the full trade and order history for records or tax. A per-campaign export is on each campaign page. |
| Auto-refresh | Polls the summary endpoint so prices, P&L and the market pill stay current without reloading. Interval is DASHBOARD_REFRESH_SECONDS; 0 disables it. |
| Login lockout | After AUTH_MAX_FAILURES bad logins from one IP, that IP is refused with HTTP 429 for AUTH_LOCKOUT_SECONDS. A successful login clears the counter. |
15. Price feed and paper realism
Why a feed matters. Target, stop-loss and trailing-stop rules are evaluated when a price arrives. If prices only arrive when you click a button, the campaign cannot exit on its own. Setting QUOTE_SOURCE makes it autonomous.
| Source | Behaviour |
|---|---|
none | Default. Manual price ticks only. Safe, but nothing is automatic. |
replay | Walks one daily bar per poll through data/history/<SYMBOL>.csv, falling back to the synthetic generator so it works immediately. This is the easiest way to watch the exit rules actually fire end to end. |
angel | Real last-traded prices from Angel One, batched into one market-data call per poll. Requires working broker credentials. |
Slippage. PAPER_SLIPPAGE_BPS moves every paper fill against you by that many basis points — buys fill slightly higher, sells slightly lower. At 0 a round trip is free, which flatters results; the 5 bps default models a realistic spread. The backtester uses the same broker, so the same friction applies there.
Notifications. With NOTIFY_ENABLED on, alerts go to a webhook, Telegram and/or email. NOTIFY_EVENTS selects which events fire, using keys like position_opened, position_closed, daily_loss_limit, global_halt and error. Sends are best-effort: a failing channel is reported but never blocks a trade.
16. Known limitations
- Live quotes must be configured. A price feed exists (see section 15), but it defaults to
none, which means prices only change on manual ticks and the exit rules cannot fire unattended. SetQUOTE_SOURCEtoreplayorangel. - Angel quotes need live credentials. Reading last-traded prices uses the same Angel One session as order placement, so the broker side must be configured before the
angelfeed can work. - Backtest results depend entirely on your data. Press Fetch fresh data first: it downloads real NSE daily bars from Yahoo Finance, so the honest path needs no key. Without any history the run falls back to a synthetic random walk, and synthetic results are mechanically valid but strategically meaningless.
- Yahoo Finance is an unofficial source. It needs no key and works today, but it is not a licensed feed and can rate-limit or change shape. For anything you intend to trade on, use your broker’s data or a paid vendor.
- AI cost figures are estimates. Token counts come from the provider’s own response, but the prices are a table in the code and your invoice is authoritative. Re-check the table when a provider changes pricing.
- The tracked universe is small. Selection only ever considers the platform’s built-in NSE list, so “what is the best stock in India” is not a question it can answer. Add symbols to
app/services/market.pyto widen it. - Holidays are manual.
MARKET_HOLIDAYSis an operator-maintained list. - Environment detection is best-effort. A machine behind NAT cannot prove its own public identity, and any hypervisor can present a cloud-looking identity string, so section 12’s label is evidence with a stated confidence rather than a fact. The public IP lookup also needs outbound HTTPS; without it, detection says “unverified” instead of guessing.
- A dynamic public IP will break live orders. The app can detect that the address drifted and warns you, but it cannot stop the change. Live trading wants a genuinely static egress IP, ideally with TLS and a domain.
- Backups are exports, not restores. The Danger zone writes a complete JSON dump, but there is no import button — bringing data back means re-inserting rows yourself. Keep the file somewhere off the machine.
- Single instance only. SQLite suits one process on one VPS. Multiple replicas need PostgreSQL first.
- Charges are estimates. The broker contract note is authoritative.