Introduction
Saras tracks over 400 daily trade ideas from SEBI-registered financial advisors — scraping Telegram channels, Twitter, YouTube, and research reports at scale. The platform gives retail traders a data-driven lens to evaluate advisor quality before following them.
But there's a natural question that follows: if we're already computing who the best advisors are, why not trade on it?
That's the question that kicked off this project. I led the end-to-end design and build of Saras's internal prop trading system — a fully automated pipeline that ingests advisor signals, scores them against a set of custom analytics, and executes live orders through Dhan's broker API.
This post is a deep dive into the architecture, the unique metrics we invented along the way, and the engineering decisions that made it production-ready.
The Problem We Were Solving
Saras aggregates trade signals from hundreds of SEBI-registered advisors across India. These signals arrive via MongoDB — parsed from Telegram, Twitter, and other social channels by our data collection layer. Each signal has:
- Entry price, stoploss, target(s)
- Instrument (NSE equity, Index Futures, Stock Options, etc.)
- Advisor identity and historical track record
The challenge isn't finding signals. It's knowing which signals to trust and how much capital to deploy.
At the start, we'd evaluate advisors informally — win rate, average return. But these basic metrics lie. An advisor with 80% win rate and -30% drawdown on a 1 Crore portfolio is not a good advisor. We needed something better.
System Architecture
The system has two major loops: an analytics pipeline (offline, runs on every ingestion) and a live trading engine (real-time, runs during market hours).
Every component has a clear responsibility. The analytics pipeline runs asynchronously via RQ (Redis Queue) and never blocks the API. The trading engine runs as a long-lived async Python process, separate from the API entirely.
The Data Pipeline
Before any metrics or trading rules can exist, raw advisor signals need to be cleaned and enriched. Our pipeline runs 9 sequential steps on every CSV/JSON upload:
Feature Engineering is where the interesting work happens. For every trade, we derive:
- Position sizing (lots, margin, leverage) against a ₹5L corpus
- Holding period in hours and days
- Time session (Morning / Midday / Afternoon / After Hours in IST)
- Market context:
is_intraday,is_expiry_day(Thursday) - Outcome flags:
is_winning_trade,target_hit,stoploss_hit
Regime Mapping is handled by a separate Go microservice we call the regime service. It classifies each trading day into one of five regimes:
- Trending Bullish
- Trending Bearish
- Sideways
- Sideways Bullish
- Sideways Bearish
This dimension matters a lot. An advisor who crushes it in Trending Bullish regimes might be terrible in sideways markets. Segmenting by regime is what lets us build trustworthy rules.
The 6-Dimensional Rule Engine
Most systems look at advisor performance at one level: "how does advisor X do overall?" We go deeper.
Every trading rule is the intersection of 6 dimensions:
Advisor × Category × Period × Regime × Day of Week × Time Session
| Dimension | Values |
|---|---|
| Advisor | Any SEBI-RA tracked on Saras |
| Category | NSE_EQ, Index Futures, Stock Futures, Index Options, Stock Options |
| Period | Intraday, Swing, Positional |
| Regime | Trending Bullish, Trending Bearish, Sideways, Sideways Bullish, Sideways Bearish |
| Day of Week | Monday–Friday |
| Time Session | Morning (09:15–11:30), Midday (11:30–13:30), Afternoon (13:30–15:30) |
A rule only activates for live trading if it clears all quality gates: minimum trades, win rate, Sharpe ratio, profit factor, and max drawdown thresholds — all category-specific.
We also generate mini rules (Advisor × Category × Period, optionally with Regime) as a broader signal when full 6D data is sparse.
The Metrics That Actually Matter
Here's where things get interesting. Standard metrics — win rate, average return — are useful but incomplete. We built several novel metrics that have proven to be much better predictors of live performance.
1. Corpus Replay System
This is the centerpiece of our analytics engine.
The problem with legacy metrics: If you evaluate each trade independently against a fixed ₹5L capital, the drawdown numbers are fictional. Concurrent trades don't compete for the same capital pool, so you never see the real risk.
Our solution: Replay all of an advisor's trades over a 90-day rolling window against a shared ₹1 Crore virtual corpus. Trades compete for capital, margins are deducted on entry, and the corpus value updates on every exit.
From this single shared equity curve, we derive:
- True Maximum Drawdown (MDD): Peak-to-trough decline on the actual corpus, not compounded trade percentages
- Corpus-based Sharpe Ratio: Calculated from real portfolio returns, not isolated per-trade excess returns
- Corpus Volatility: Standard deviation of per-trade corpus changes — reflects concurrent trade effects
This also handles capital exhaustion: if 20 concurrent Index Futures trades each need ₹5L margin, only 20 can execute. Trades 21+ are skipped and recorded as trades_skipped_capital. The legacy system would pretend they all ran — wildly overstating the advisor's capacity.
| Metric | Legacy | Corpus-Based |
|---|---|---|
| Capital Model | Isolated ₹5L per trade | Shared ₹1Cr pool |
| MDD | Compounded return % (fictional) | True corpus drawdown |
| Concurrent trades | Independent, no interaction | Compete for capital |
| Sharpe Ratio | From isolated returns | From portfolio returns |
2. MAE/MFE — Measuring Trade Quality, Not Just Outcomes
Maximum Adverse Excursion (MAE) and Maximum Favorable Excursion (MFE) are intra-trade risk metrics powered by tick-by-tick price data from our AccelPix service.
MAE tells you: how much pain did you endure before the trade closed?
MFE tells you: what was the peak opportunity available during the trade?
Both are expressed as R-multiples (multiples of initial risk: |entry - stoploss|):
MAE % = (worst price move against position / R) × 100
MFE % = (best price move in favor of position / R) × 100
The ratio of these two gives us the Pain/Reward Ratio — a better signal than win rate alone:
Pain/Reward = Average MAE % ÷ Average MFE %
A Pain/Reward below 0.3 is excellent. Above 0.8 is a strategy that requires enduring large drawdowns to capture modest gains. Not something you want to trade in size.
We also compute percentile distributions (75th, 90th) for both metrics — useful for understanding tail risk in a rule.
3. Post-SL and Post-Target Extensions
These metrics answer a question most systems never even ask:
- Post-SL Extension: After a stop loss was hit, how much further did price move against the position in the next 30 minutes?
- Post-Target Extension: After a target was hit, how much further did price move in favor in the next 30 minutes?
Post-SL Extension = (stoploss - worst_price_in_30min_window) / entry_price × 100
Consistently negative Post-SL extensions indicate stop hunting — price is triggering stops and then reversing. This tells you to either widen stops or move them off obvious levels.
Consistently large Post-Target extensions mean you're leaving money on the table — trailing stops would serve better than fixed targets.
These insights directly feed our execution strategy: advisors with high stop-hunt signals get their SL levels adjusted in live trading.
4. WSE/WTE — Trades Without Stops or Targets
Not all advisors use explicit stop losses. For trades where stoploss is NULL, we compute Without SL Extension (WSE) — the worst adverse price move during the trade's lifetime, expressed as a percentage of entry price.
Similarly, Without Target Extension (WTE) measures the peak favorable move for trades without a defined target.
WSE % = (entry_price - lowest_tick) / entry_price × 100 [for BUY positions]
WTE % = (highest_tick - entry_price) / entry_price × 100 [for BUY positions]
These metrics exist because ignoring trades-without-SL from your risk model is dangerous. If the average WSE for an advisor is 15%, you need to factor that into your position sizing even if their explicit SL-hit rate looks fine.
5. Recency Weighting (Exponential Decay)
Performance that happened 6 months ago should count less than last week. We use exponential decay with a 10-day half-life for all weighted metrics:
weight = exp(-ln(2) / half_life × days_ago)
A trade from today → weight = 1.0
A trade from 10 days ago → weight = 0.5
A trade from 20 days ago → weight = 0.25
This produces two parallel sets of metrics:
- Unweighted: Raw historical average
- Weighted: Recent-trend-adjusted
The gap between them — WWR-UWR Divergence (Weighted Win Rate minus Unweighted Win Rate) — is one of our most predictive signals.
6. Trust Flag Classification
Divergence = Weighted Win Rate - Unweighted Win Rate
| Divergence | Trust Flag | Meaning |
|---|---|---|
| > +5% | UNTRUSTED | Suspiciously hot streak |
| 0% to +5% | TRUSTED | Improving trend |
| -5% to 0% | WATCHLIST | Slight decline |
| < -5% | UNTRUSTED | Deteriorating performance |
Both extremes are flagged as untrusted. A large positive divergence often indicates a lucky recent streak that won't persist, not a genuine improvement.
7. Crowd Confidence Scoring
This is the most unique metric we built — and it's specific to the prop trading flow.
Saras tracks signals from many advisors simultaneously. If 12 out of 14 advisors with open NIFTY positions are bullish right now, that consensus carries real information. We built a per-underlying crowd ledger that computes live sentiment:
bullish_confidence = open_bullish_count / (open_bullish_count + open_bearish_count)
crowd_confidence_score = max(bullish_confidence, bearish_confidence)
The score is computed from open positions only — not historical or closed ones. This reflects current live market conviction.
When a qualifying trade aligns with the crowd direction AND the crowd score exceeds the threshold (default 75%), it receives a capital multiplier boost.
Direction classification handles the option direction inversion correctly:
| Category | Position | Option Type | Sentiment |
|---|---|---|---|
| Index/Stock Futures | BUY | — | Bullish |
| Index/Stock Options | BUY | CE | Bullish |
| Index/Stock Options | BUY | PE | Bearish |
| Index/Stock Options | SELL | CE | Bearish |
| Index/Stock Options | SELL | PE | Bullish |
Getting this table right matters — a PE option BUY is a bearish bet, not bullish.
The Live Trading Engine
The trading engine runs as a long-lived async Python process — entirely separate from the API. It manages 6 concurrent tasks:
Pre-Market Setup (08:45 IST)
Every trading day, before the trading worker starts, a pre-market worker runs a 5-step sequential setup:
- Renew Dhan token (API call + TOTP fallback via
pyotp) - Download and load the Dhan scrip master into Redis
- Validate ticker →
security_idmappings - Run eligibility evaluation (normal + reverse)
- Initialize daily corpus from yesterday's remaining capital + P&L
When all 5 steps succeed, it sets a Redis readiness flag. The trading worker won't start processing signals until this flag is set.
Signal Processing Flow
When a new signal arrives via MongoDB change stream:
Crowd sentiment recording happens before any rule matching or gating. This is intentional — we want the crowd ledger to reflect all advisor activity, not just signals that pass our filters.
Dhan Super Orders
For each trade, we use Dhan's Super Order API — a single atomic request that places three legs simultaneously:
- Entry order (limit or market)
- Stop-loss leg (automatically triggers on fill)
- Target leg (automatically triggers on fill)
This means once the entry fills, both the SL and target are already on the exchange. There's no window where the position is unprotected.
Multi-Target Execution and Trailing SL Chain
When an advisor posts multiple targets for the same signal (e.g., "NIFTY 24500 T1: 24600, T2: 24700, T3: 24800"), we split capital equally across legs. Each leg is a separate Super Order — same trade_group_id, different leg_number.
When the first target hits, we implement a trailing SL chain:
For a BUY with 3 targets at 142, 171, 200 (entry ~127):
- T1 (142) hits → siblings' SL moves to 127 (entry, now risk-free)
- T2 (171) hits → remaining sibling's SL moves to 142 (previous target)
- Final trade exits at 200 or the tightened 142 SL, never below breakeven
The Reverse Strategy
Some advisors are consistently wrong — and that's valuable too.
We maintain a separate adverse eligibility config with inverted thresholds: avg return must be below threshold, volatility above, profit factor below. Advisors qualifying here are candidates for the reverse strategy.
When a signal from a reverse-eligible advisor arrives and no normal eligible rule matches:
- Direction is flipped (BUY → SELL, SELL → BUY)
- SL and target are swapped
- Capital is drawn from a separate adverse corpus allocation
Even followup signals are handled with swapped logic:
Advisor updates SL → we modify our TARGET leg (not our SL)
Advisor updates Target → we modify our SL leg
It's elegant in its perversity — the advisors who are wrong are just as useful as the ones who are right.
The Corpus Service (Live Capital Tracking)
During live trading, a separate CorpusService manages the actual capital allocation — distinct from the analytics CorpusEngine which handles offline simulation.
The daily corpus is initialized each morning from yesterday's remaining capital plus realized P&L. Within the day, it's segmented by instrument category (Index Futures, Stock Options, etc.) with configurable percentage allocations.
corpus_check_budget(category, required_margin)
├── check: category allocation not exhausted
├── check: total corpus > required_margin
└── return: approved capital
on_trade_entry: deploy_capital(category, margin)
on_trade_exit: release_capital(category, margin) + book_pnl(pnl)
This prevents overexposure in a single category even if there are many qualifying signals.
Daily Data Ingestion (GitHub Actions)
We also have a GitHub Actions workflow that runs daily at market close to ingest the previous day's trade data automatically. This keeps the analytics pipeline fresh without manual intervention.
# .github/workflows/daily_ingest.yml
schedule:
- cron: '30 10 * * 1-5' # 4:00 PM IST (10:30 UTC), Mon-Fri
The ingestion worker fetches yesterday's trades, processes them through the full 9-step pipeline, and refreshes all metrics and rules.
Infrastructure
The full system runs on AWS:
| Component | Service |
|---|---|
| Application | EC2 t3.large (2 vCPU, 8GB) |
| Database | AWS RDS PostgreSQL 17 |
| Cache + Queue | Redis 7 (Docker, same EC2) |
| Regime Service | Go binary on same EC2 (port 8081) |
| Signal Source | MongoDB Atlas |
| Broker | Dhan API (REST + WebSocket) |
| Notifications | WuzAPI (WhatsApp) + Telegram Bot |
All services run on a shared saras_network Docker bridge. The production docker-compose brings up:
saras_api— FastAPI on port 8000, behind Nginxsaras_worker— RQ bulk processing workersaras_trading_worker— live trading async loopsaras_premarket_worker— daily 08:45 IST setupsaras_notification_worker— PostgreSQL LISTEN → WhatsApp/Telegram
The Notification System
Every trade lifecycle event fires a PostgreSQL NOTIFY via a PL/pgSQL trigger on the executed_trades table. A standalone worker listens on that channel and dispatches to WuzAPI (WhatsApp) or Telegram as fallback.
This architecture means notifications are decoupled from the trading engine entirely. A notification failure never affects trade execution.
Trade INSERT/UPDATE
→ PL/pgSQL trigger → pg_notify('trade_event', JSON payload)
→ notification/worker.py (asyncpg LISTEN)
→ build_message() → send_wuzapi() or send_telegram()
Events covered: ENTRY_PLACED, ENTRY_FILLED, ENTRY_REJECTED, STOPLOSS_HIT, TARGET_HIT, EXIT_COMPLETED, SL_UPDATED.
The Dashboard
The analytics dashboard is built in Next.js with server-side rendering and React Query for data fetching. It surfaces everything — from the 6D trading rules table to live crowd sentiment, corpus equity curves, and individual trade P&L breakdowns.
Key pages:
- Leaderboard — Advisors ranked by corpus-adjusted Sharpe, MDD, weighted win rate
- Rule Explorer — Filter rules by any combination of the 6 dimensions, sortable by MAE/MFE and Pain/Reward
- Live Dashboard — Real-time corpus value, active trades, crowd sentiment per ticker
- Simulation — Three-step eligibility filter → category discovery → position-sized backtest
- Trade History — Every executed trade with full metadata: crowd score, is_reverse, leg number, entry/exit, P&L
What Actually Made Us Money
The metrics that consistently separate profitable rules from noise:
-
Corpus MDD < -15% — Hard gate. We don't trade rules where the simulated portfolio blew out even once in 90 days.
-
Pain/Reward < 0.4 — Trades that require minimal pain to achieve the reward. Rules with P/R > 0.5 tend to have fat-tail losses that aren't visible in average return figures.
-
Trust Flag = TRUSTED — WWR > UWR, divergence between 0 and +5%. Rules in WATCHLIST occasionally slip through on fundamentals but get smaller size. UNTRUSTED rules never trade.
-
Post-SL Extension < -0.5% on average — Systematically negative means the stoploss is being hunted. We widen the SL by the expected extension before placing. This alone improved fill quality significantly.
-
Crowd Confidence > 75% with alignment — When 12 of 14 advisors are bullish on NIFTY and we have a qualifying BUY rule, we boost capital. The consensus signal is alpha.
The reverse strategy has also been surprisingly effective. There's a segment of advisors who are technically bad — consistently below 40% win rate, high volatility — but they're consistent enough that fading them is profitable. The corpus math still applies: we cap SL/target at 30% to prevent any single bad signal from taking an outsized chunk.
What I'd Do Differently
Event-driven architecture over polling. The MongoDB change stream is excellent, but some parts of the pipeline still use polling intervals. A proper event bus (Kafka or Pulsar) would let us scale the analytics workers independently of ingestion volume.
Separating regime by instrument. Right now, regime is a market-wide classification. But NIFTY regime and RELIANCE equity regime can diverge. Per-ticker regime classification would improve rule precision significantly.
Options-specific metrics. IV rank, days-to-expiry decay, and theta effects aren't captured in our current metrics. For options-heavy advisors, these matter enormously and we're missing signal.
Streaming corpus updates. The corpus replay currently reruns from scratch on every ingestion. For advisors with 200+ trades in the 90-day window, this is expensive. Incremental corpus updates with delta processing would halve the compute.
Tech Stack Summary
| Layer | Technology |
|---|---|
| API Framework | FastAPI + Uvicorn |
| Background Jobs | RQ (Redis Queue) |
| Database | PostgreSQL 17 |
| ORM | SQLAlchemy 2.0 |
| Migrations | Alembic |
| Cache + Queue | Redis 7 |
| Validation | Pydantic v2 |
| Data Processing | Pandas + NumPy + SciPy |
| HTTP Client | httpx (async) |
| Regime Service | Go |
| Dashboard | Next.js + React Query + Recharts |
| Broker | Dhan (REST + WebSocket) |
| Signal Source | MongoDB Atlas (Change Streams) |
| Notifications | WuzAPI + Telegram Bot |
| Infrastructure | AWS EC2 + RDS, Docker Compose |
| Logging | Loguru |
Conclusion
Building this system end-to-end forced me to think carefully about what "good" metrics actually mean in trading. Win rate is a starting point. Corpus-adjusted Sharpe and MDD are more honest. Pain/Reward ratio, trust flags, and crowd confidence are where the edge actually lives.
The Saras platform (saras.market) tracks advisor quality so retail investors can make better decisions. The prop trading engine takes that same intelligence and automates the execution — with every safety gate we could think of: kill switches, corpus caps, price validation, eligibility double-gating, and real-time alerts.
The architecture is opinionated but deliberate. Python for the analytics-heavy pipeline, Go for the high-frequency regime service, Next.js for the dashboard, Dhan for execution. Each tool was chosen because it was the best fit — not because it was fashionable.
If you're building something similar — analytics-driven automated trading in Indian markets — I hope this walkthrough saved you some of the hard-won lessons.