Effective configuration, read-only. Edit the files in
config/ and rerun tc run.costs.yaml modified 2026-10-18 07:17
# Transaction cost tables. Percentages are of trade value unless noted. VERIFY against the broker's
# current charge sheet and NSE circulars before relying on expectancy numbers.
broker: zerodha
products:
CNC: # equity delivery
brokerage_pct: 0.0
brokerage_cap_inr: 0
stt_buy_pct: 0.1
stt_sell_pct: 0.1
exchange_txn_pct: 0.00297
sebi_fee_pct: 0.0001
stamp_duty_buy_pct: 0.015
gst_pct: 18 # applied to brokerage + exchange txn + sebi fee
dp_charge_sell_inr: 15.34 # per scrip per sell day
MIS: # equity intraday
brokerage_pct: 0.03
brokerage_cap_inr: 20
stt_buy_pct: 0.0
stt_sell_pct: 0.025
exchange_txn_pct: 0.00297
sebi_fee_pct: 0.0001
stamp_duty_buy_pct: 0.003
gst_pct: 18
dp_charge_sell_inr: 0
fundamentals.yaml modified 2026-10-18 07:17
# How fundamentals are presented on the stock page (Fundamentals card) and the screener. Presentation only:
# the shield (shield_rules.yaml) and the F sub-score (scoring.yaml) decide; this file decides what the reader sees.
# Field names are the engine's own (see trendlyne.yaml parameter_map for how each is sourced).
# applies_to filters work like shield rules: a row is hidden when the stock's sector_type / bucket does not match.
# explain: one sentence a first-time reader sees on hover / in the Learn page.
# rule: optional shield rule id to show on the row when that rule tests a derived field (e.g. growth_trajectory).
source_label: Trendlyne
groups:
- title: Quality
fields:
- { field: roce_3y_avg_pct, label: ROCE, hint: 3-yr average, unit: "%", decimals: 1,
explain: Return on capital employed. Operating profit per rupee the business ties up. Above 12% clears the shield; 20%+ is excellent. }
- { field: roe_3y_avg_pct, label: ROE, hint: 3-yr average, unit: "%", decimals: 1,
explain: Return on equity. Profit per rupee of shareholder money. Above 12% clears the shield. }
- { field: debt_to_equity, label: Debt / equity, unit: "×", decimals: 2, applies_to: { sector_type: [non_financial] },
explain: Total borrowings divided by equity. Below 0.5 means the business is not leaning on lenders. }
- { field: capital_adequacy_ratio_pct, label: Capital adequacy, unit: "%", decimals: 1, applies_to: { sector_type: [financial] },
explain: Capital a lender holds against its risk-weighted loans. Regulators want roughly 11%; the shield asks for 15%. }
- { field: net_npa_pct, label: Net NPA, unit: "%", decimals: 2, applies_to: { sector_type: [financial] },
explain: Bad loans after provisions, as a share of the loan book. Below 1% is clean. }
- title: Growth
fields:
- { field: revenue_cagr_3y_pct, label: Revenue growth, hint: 3-yr CAGR, unit: "%", decimals: 1, rule: growth_trajectory,
explain: Compound annual growth in sales over three financial years. The shield wants both revenue and profit compounding. }
- { field: pat_cagr_3y_pct, label: Profit growth, hint: 3-yr CAGR, unit: "%", decimals: 1, rule: growth_trajectory,
explain: Compound annual growth in net profit over three financial years. The shield wants both revenue and profit compounding. }
- title: Who owns it
fields:
- { field: promoter_holding_pct, label: Promoter holding, unit: "%", decimals: 1,
explain: Share held by the founders or parent. Skin in the game; the shield wants 40% for mid caps and 50% for small caps. }
- { field: promoter_pledge_pct, label: Promoter pledge, hint: of their holding, unit: "%", decimals: 1,
explain: Share of the promoter stake pledged as loan collateral. Pledges above 2% can force selling when the price falls. }
- { field: fii_pct, label: Foreign institutions, hint: FII, unit: "%", decimals: 1,
explain: Foreign portfolio investors. }
- { field: dii_pct, label: Domestic institutions, hint: DII, unit: "%", decimals: 1,
explain: Mutual funds, insurers and other Indian institutions. }
- { field: fii_dii_pct, label: FII + DII, unit: "%", decimals: 1,
explain: Combined institutional ownership. Above 10% means professionals have done the diligence. }
- { field: fii_dii_change_4q_pp, label: Institutional change, hint: 4 quarters, unit: " pp", decimals: 2, signed: true,
explain: Change in institutional holding over the last four quarters, in percentage points. Steady selling is a warning. }
# Columns added to the screener table, in order (fields from the groups above).
screener_columns: [roe_3y_avg_pct, debt_to_equity]
explanation: >
Fundamentals are the balance-sheet side of the picture: how profitable the business is, whether it is growing,
how much it owes, and who owns it. The shield turns the worst of these into hard disqualifiers; the F sub-score
rewards the best. Ratios are annual and update when results are filed; holdings update each quarter.
holidays.yaml modified 2026-10-18 07:17
# Exchange calendar for the "Markets Open / Closed" chip in the dashboard header.
# Source: NSE trading-holiday circulars for the Capital Market segment (nseindia.com > Resources > Exchange
# communication > Holidays). NSE publishes next year's list in December; add the new year here, no code change.
# Dates missing from this file are treated as normal trading days, so a year not listed will show weekends only.
timezone: Asia/Kolkata
source: https://www.nseindia.com/resources/exchange-communication-holidays
hours: # IST, equity cash segment
pre_open: "09:00"
open: "09:15"
close: "15:30"
holidays:
# 2026 (NSE circulars NSE/CMTR/71200 series; 15 Jan added by addendum NSE/CMTR/72260)
- {date: 2026-01-15, name: Municipal Corporation General Elections}
- {date: 2026-01-26, name: Republic Day}
- {date: 2026-02-15, name: Mahashivratri} # Sunday
- {date: 2026-03-03, name: Holi}
- {date: 2026-03-21, name: Id-Ul-Fitr (Ramadan Eid)} # Saturday
- {date: 2026-03-26, name: Shri Ram Navami}
- {date: 2026-03-31, name: Shri Mahavir Jayanti}
- {date: 2026-04-03, name: Good Friday}
- {date: 2026-04-14, name: Dr. Baba Saheb Ambedkar Jayanti}
- {date: 2026-05-01, name: Maharashtra Day}
- {date: 2026-05-28, name: Bakri Id}
- {date: 2026-06-26, name: Muharram}
- {date: 2026-08-15, name: Independence Day} # Saturday
- {date: 2026-09-14, name: Ganesh Chaturthi}
- {date: 2026-10-02, name: Mahatma Gandhi Jayanti}
- {date: 2026-10-20, name: Dussehra}
- {date: 2026-11-08, name: Diwali Laxmi Pujan} # Sunday; Muhurat session, see special_sessions
- {date: 2026-11-10, name: Diwali-Balipratipada}
- {date: 2026-11-24, name: Prakash Gurpurb Sri Guru Nanak Dev}
- {date: 2026-12-25, name: Christmas}
# Sessions held on an otherwise closed day, with their own hours (IST).
special_sessions:
- {date: 2026-02-01, name: Union Budget special session, open: "09:15", close: "15:30"} # Sunday, NSE/CMTR/72349
# Muhurat trading 2026-11-08: NSE announces the one-hour evening slot by circular in late October. Add it here:
# - {date: 2026-11-08, name: Muhurat trading, open: "18:15", close: "19:15"}
policy_tags.yaml modified 2026-10-18 07:17
# Hand-curated Government priority / incentivised sector mapping (v1). Refresh quarterly.
# A symbol is tagged if it appears in `symbols` or its `industry` (from the NSE index file) matches a sector.
sectors:
- { name: Electronics manufacturing (PLI), industries: ["Consumer Durables", "Industrial Manufacturing"] }
- { name: Renewable energy, industries: ["Power"] }
- { name: Defence, industries: ["Aerospace & Defense"] }
- { name: Semiconductors, industries: [] }
- { name: Electric vehicles / auto PLI, industries: ["Automobile and Auto Components"] }
symbols: {} # explicit overrides, e.g. { "DIXON": "Electronics manufacturing (PLI)" }
scoring.yaml modified 2026-10-18 07:17
# Composite = sum(weights[k] * subscore_k). Each sub-score is 0..10, built from weighted components.
# A component maps a raw field to 0..10 by piecewise-linear interpolation over `map: [[x, y], ...]`,
# clamped at both ends (absolute thresholds, not rank — see docs/05-decisions.md).
weights: { F: 0.30, T: 0.25, M: 0.20, B: 0.25 }
setup_threshold: 8.0
watchlist_threshold: 6.5
neutral_score: 5.0 # used when a sub-score has no data (e.g. M in v1)
policy_multiplier: # Govt priority / PLI tag
applies_to: M
tagged: 1.10
cap: 10.0
F:
components:
- { field: roce_3y_avg_pct, weight: 0.30, map: [[12, 4], [20, 8], [30, 10]] }
- { field: roe_3y_avg_pct, weight: 0.30, map: [[12, 4], [20, 8], [30, 10]] }
- { field: revenue_cagr_3y_pct, weight: 0.20, map: [[0, 2], [10, 6], [20, 10]] }
- { field: pat_cagr_3y_pct, weight: 0.20, map: [[0, 2], [10, 6], [25, 10]] }
T:
components:
- { field: ret20_pct, weight: 0.40, map: [[-5, 2], [0, 5], [10, 9], [20, 10]] }
- { field: dist_ema50_pct, weight: 0.30, map: [[-5, 2], [0, 5], [8, 9], [15, 10]] }
- { field: rel_volume, weight: 0.30, map: [[1.0, 4], [1.5, 7], [2.5, 10]] }
B:
min_trades: 8
insufficient_sample_score: 4.0
components:
- { field: win_rate_pct, weight: 0.60, map: [[40, 2], [50, 5], [60, 8], [70, 10]] }
- { field: expectancy_r, weight: 0.40, map: [[0, 3], [0.3, 6], [0.6, 10]] }
M:
components:
- { field: broker_consensus_score, weight: 0.50, map: [[0, 0], [10, 10]] }
- { field: sentiment_score, weight: 0.50, map: [[0, 0], [10, 10]] }
settings.yaml modified 2026-10-18 07:17
# Global knobs. Everything here is read at runtime; no code change needed to tune.
capital:
deployed_inr: 50000000 # ~Rs 5 Cr
risk_per_trade_pct: 0.75 # % of capital lost if the stop is hit
max_position_pct: 10 # cap on a single name as % of capital
max_adv_participation_pct: 2 # order value cap as % of 20-session avg traded value
max_open_positions: 12
holding:
product: CNC # CNC = delivery (swing). MIS = intraday. Drives the cost table.
max_sessions: 5 # time-stop: exit at close of this many sessions after entry
entry: next_open # next_open | next_close (where the simulated fill happens)
risk:
atr_period: 14
stop_atr_multiple: 1.5 # stop = entry - k * ATR
target_r_multiple: 2.0 # first target = entry + R * (entry - stop) => R:R 1:2
extra_targets_r: [3.0] # additional targets, in R multiples
min_reward_risk: 2.0 # cards below this are suppressed
entry_zone_atr_fraction: 0.25 # entry zone = reference ± fraction * ATR
backtest:
lookback_sessions: 250
slippage_pct: 0.10 # per side, on top of the broker cost table
require_healthy_fundamentals: true # count only signals on days the shield passed (point-in-time)
fundamentals_fill: ffill_bfill # ffill_bfill (fields with no earlier value are back-filled; mild look-ahead) | ffill
stop_before_target_same_bar: true # conservative tie-break when both are touched in one bar
data:
fundamentals_provider: trendlyne_mcp # trendlyne_mcp | none (config in trendlyne.yaml)
provider: nse # nse (free bhavcopy, unadjusted) | kite (Zerodha Kite Connect, pending)
kite:
api_key_env: KITE_API_KEY
access_token_env: KITE_ACCESS_TOKEN
handoff:
brokers: [zerodha, groww]
kite_publisher_api_key_env: KITE_PUBLISHER_API_KEY # optional; enables pre-filled basket links
schedule:
timezone: Asia/Kolkata
earliest_run: "18:30"
retry_until: "22:00"
shield_rules.yaml modified 2026-10-18 07:17
# The "Wizard's Shield". Any failing rule disqualifies the stock (composite capped at disqualified_score_cap).
# Each rule: field (from the fundamentals/ownership snapshot), op, value, optional applies_to filters
# (bucket: large|mid|small, sector_type: financial|non_financial), on_missing: fail|skip (default fail).
disqualified_score_cap: 4.9
rules:
- id: de_ratio
description: Debt-to-Equity < 0.5 for non-financials
field: debt_to_equity
op: "<"
value: 0.5
applies_to: { sector_type: [non_financial] }
- id: car
description: Capital adequacy ratio > 15% for banks/NBFCs
field: capital_adequacy_ratio_pct
op: ">"
value: 15
applies_to: { sector_type: [financial] }
- id: net_npa
description: Net NPA < 1.0% for banks/NBFCs
field: net_npa_pct
op: "<"
value: 1.0
applies_to: { sector_type: [financial] }
- id: promoter_holding_mid
description: Promoter holding >= 40% (mid caps; waived for large caps per decision 2026-10-17)
field: promoter_holding_pct
op: ">="
value: 40
applies_to: { bucket: [mid] }
- id: promoter_holding_small
description: Promoter holding >= 50% (small caps)
field: promoter_holding_pct
op: ">="
value: 50
applies_to: { bucket: [small] }
- id: promoter_pledge
description: Promoter pledge < 2% of promoter holding
field: promoter_pledge_pct
op: "<"
value: 2
- id: institutional_holding
description: FII + DII > 10% (mid and small caps)
field: fii_dii_pct
op: ">"
value: 10
applies_to: { bucket: [mid, small] }
- id: institutional_trend
description: FII + DII not falling by more than 0.5 pp over 4 quarters
field: fii_dii_change_4q_pp
op: ">="
value: -0.5
applies_to: { bucket: [mid, small] }
on_missing: skip
- id: surveillance
description: Not in any ASM / GSM stage
field: in_asm_gsm
op: "=="
value: false
- id: audit_clean
description: No audit qualification or mid-term auditor resignation in last 3 FY
field: audit_red_flag_3y
op: "=="
value: false
on_missing: skip # TEMPORARY (decision 2026-10-17): no data source yet; revert to fail once sourced
- id: roce_3y
description: 3-year average ROCE >= 12% (non-financials)
field: roce_3y_avg_pct
op: ">="
value: 12
applies_to: { sector_type: [non_financial] }
- id: roe_3y
description: 3-year average ROE >= 12%
field: roe_3y_avg_pct
op: ">="
value: 12
- id: us_revenue
description: US revenue share < 5% (mid and small; waived for large caps per decision 2026-10-17)
field: us_revenue_share_pct
op: "<"
value: 5
applies_to: { bucket: [mid, small] }
on_missing: skip
- id: growth_trajectory
description: Positive revenue and PAT compounding over last 3 FY
field: revenue_pat_positive_cagr_3y
op: "=="
value: true
on_missing: skip
signals.yaml modified 2026-10-18 07:17
# Technical signal score shown on the screener and stock pages. Each check is a pandas expression over the
# OHLCV + indicator columns from triggers.yaml. Points sum to the score; labels are what the reader sees.
# This is deliberately simple and explainable; the composite conviction score (scoring.yaml) sits on top.
bullish_at: 2 # score >= 2 => bullish
bearish_at: -2 # score <= -2 => bearish
volume_confirm_x: 1.5 # breakout counts as volume-confirmed at >= this multiple of 20-day average volume
volume_spike_x: 2.0 # "volume spike" for breadth counts
explanation: >
Score sums trend (close vs 50-day average), MACD vs its signal line, RSI momentum and 20-day
breakouts/breakdowns. +2 or more is bullish, -2 or less is bearish. A breakout is volume-confirmed when
volume is at least 1.5x its 20-day average. Informational only, not advice.
checks:
- { id: trend_up, when: "close > sma50", points: 1, label: "Price above 50-DMA" }
- { id: trend_down, when: "close < sma50", points: -1, label: "Price below 50-DMA" }
- { id: macd_up, when: "macd_line > macd_signal", points: 1, label: "MACD above signal" }
- { id: macd_down, when: "macd_line < macd_signal", points: -1, label: "MACD below signal" }
- { id: rsi_up, when: "rsi14 > 55", points: 1, label: "RSI momentum positive (>55)" }
- { id: rsi_down, when: "rsi14 < 45", points: -1, label: "RSI momentum negative (<45)" }
- { id: rsi_ob, when: "rsi14 > 70", points: 0, label: "RSI overbought (>70)" }
- { id: rsi_os, when: "rsi14 < 30", points: 0, label: "RSI oversold (<30)" }
- { id: breakout, when: "close > hh20", points: 1, label: "Close above 20-day high" }
- { id: breakdown, when: "close < ll20", points: -1, label: "Close below 20-day low" }
- { id: vol_confirm, when: "volume >= 1.5 * vol_avg20", points: 0, label: "Volume above 1.5x 20-day average" }
trendlyne.yaml modified 2026-10-18 07:17
# Trendlyne MCP as the fundamentals/ownership source. Requires an MCP plan (Max recommended: 1,000 calls/day).
# Secrets never live here: the server URL (which carries your subscription identity) comes from the env var below.
connection:
url_env: TRENDLYNE_MCP_URL # paste the "MCP Server URL" from Trendlyne > profile > MCP page
bearer_token_env: TRENDLYNE_MCP_TOKEN # optional; only if Trendlyne issues a separate token/header
timeout_seconds: 60
budget:
max_calls_per_run: 800 # stay under the plan's daily cap (Max = 1,000/day)
symbols_per_call: 10 # server limit: 10 stock codes per call
params_per_call: 10 # server limit: 10 parameter tokens per call
# Tool names pinned after `tc mcp-discover` on 2026-10-17.
tools:
search: { hint: "search_entities", name: search_entities }
parameters: { hint: "get_stock_parameter_values", name: get_stock_parameter_values }
ownership: { hint: "get_ownership_deals_insider", name: get_ownership_deals_insider_sast }
overview: { hint: "get_overview_news_corp", name: get_overview_news_corp_events }
param_search: { hint: "search_financial_parameters", name: search_financial_parameters }
# get_stock_parameter_values accepts <= 10 stock codes and <= 10 parameter tokens per call.
# Our field name -> Trendlyne masked parameter token(s), tried in order. Resolved via
# search_financial_parameters on 2026-10-17 (see docs/08-trendlyne-mapping.md).
parameter_map:
debt_to_equity: ["debtcea"] # Total Debt to Total Equity Annual
roce_3y_avg_pct: ["roceaave3"] # ROCE Annual 3Yr Avg %
roe_3y_avg_pct: ["roeaave3"] # ROE Annual 3Yr Avg %
revenue_cagr_3y_pct: ["rev3a"] # Revenue Annual 3Yr Growth %
pat_cagr_3y_pct: ["ni3a"] # Net Profit 3Yr Growth %
capital_adequacy_ratio_pct: ["capitaladequacyratiosa"] # CAR Annual % (the Qtr token returns 0)
net_npa_pct: ["nnparatq", "nnparata"] # Net NPA ratio % Qtr, Annual
promoter_holding_pct: ["prompct"] # Promoter holding latest %
promoter_pledge_pct: ["prompledge"] # Promoter pledge % of holding, Qtr
fii_pct: ["fiihold"] # FII holding current Qtr %
mf_pct: ["mfhold"] # MF holding current Qtr %
institutional_pct: ["instihold"] # Institutional holding current Qtr %
fii_dii_change_4q_pp: ["instipct4q"] # Institutional holding change 4Qtr (pp)
# The parameter tool returns sections titled with abbreviated labels, in arbitrary order, so sections are
# matched to tokens by label. Observed labels (2026-10-17); unknown labels fall back to fuzzy matching against
# the helping_text returned by search_financial_parameters, and are logged.
labels:
debtcea: ["Total Debt to Total Equity Ann."]
roceaave3: ["ROCE Ann. 3Y Avg %"]
roeaave3: ["ROE Ann. 3Y Avg %"]
rev3a: ["Rev. Ann. 3Y Growth %"]
ni3a: ["Net Profit 3Y Growth %"]
capitaladequacyratiosa: ["Capital Adequacy Ratios Ann. %"]
nnparatq: ["Net NPA ratio % Qtr"]
nnparata: ["Net NPA % Ann. %"]
prompct: ["Promoter holding latest %"]
prompledge: ["Promoter holding pledge percentage % Qtr"]
fiihold: ["FII holding current Qtr %"]
mfhold: ["MF holding current Qtr %"]
instihold: ["Institutional holding current Qtr %"]
instipct4q: ["Institutional holding change 1Y %", "Institutional holding change 4Qtr %"]
derived: # computed after fetch (Python expressions over fetched fields)
fii_dii_pct: "institutional_pct" # Trendlyne 'Institutional' = FII + DII (verified: TCS 9.07 + 13.4 ~ 22.5)
revenue_pat_positive_cagr_3y: "revenue_cagr_3y_pct > 0 and pat_cagr_3y_pct > 0"
# get_ownership_deals_insider_sast(type=shareholding) returns ~6 quarters of promoter / pledge / FII / DII /
# institutional history. One call per symbol; builds point-in-time ownership snapshots for the backtest.
ownership_history:
enabled: true
filing_lag_days: 21 # as_of = quarter end + lag (shareholding patterns are due within 21 days)
triggers.yaml modified 2026-10-18 07:17
# Technical triggers are rules, not code. Indicators are computed first (by name), then each trigger's
# `entry` expression is evaluated with pandas DataFrame.eval over the OHLCV columns plus indicator columns.
# Available indicator fns: sma, ema, rolling_max, rolling_min, rolling_mean, atr, rsi, pct_change.
# `shift: 1` means "as of the previous session" (use it to avoid including today's bar in a breakout level).
indicators:
- { name: hh20, fn: rolling_max, column: high, window: 20, shift: 1 }
- { name: vol_avg20, fn: rolling_mean, column: volume, window: 20, shift: 1 }
- { name: tv_avg20, fn: rolling_mean, column: traded_value, window: 20, shift: 1 }
- { name: atr14, fn: atr, window: 14 }
- { name: ema20, fn: ema, column: close, window: 20 }
- { name: ema50, fn: ema, column: close, window: 50 }
- { name: rsi14, fn: rsi, column: close, window: 14 }
- { name: ret20, fn: pct_change, column: close, window: 20 }
- { name: ll20, fn: rolling_min, column: low, window: 20, shift: 1 }
- { name: sma20, fn: sma, column: close, window: 20 }
- { name: sma50, fn: sma, column: close, window: 50 }
- { name: sma200, fn: sma, column: close, window: 200 }
- { name: macd_line, fn: macd_line, column: close }
- { name: macd_signal, fn: macd_signal, column: close }
- { name: macd_hist, fn: macd_hist, column: close }
- { name: chg1d, fn: day_change_pct, column: close }
triggers:
- id: breakout_20d_volume
enabled: true
description: Close above prior 20-session high with volume > 1.5x the prior 20-session average
entry: "close > hh20 and volume > 1.5 * vol_avg20"
- id: ema_pullback
enabled: false
description: Uptrend (close > EMA50), dips to EMA20 and closes back above it
entry: "close > ema50 and low <= ema20 and close > ema20"
combine: any # any | all (how multiple enabled triggers are merged into one signal)
universe.yaml modified 2026-10-18 07:17
# Universe construction. Buckets are filled from an NSE index list, filtered by liquidity, then
# ranked by average traded value. target_count is a ceiling, not a promise (see docs/04-caveats.md §3).
traded_value_window_sessions: 125 # ~6 months
buckets:
large:
target_count: 20
source_index: NIFTY 50
min_avg_traded_value_cr: 0
mid:
target_count: 50
source_index: NIFTY MIDCAP 100
min_avg_traded_value_cr: 50
small:
target_count: 100
source_index: NIFTY SMALLCAP 250
min_avg_traded_value_cr: 10
overrides:
include: {} # symbol: bucket e.g. { "KAYNES": small }
exclude: [] # symbols never to trade