@Tradehull_Imran “”"
main.py - Orchestrator for RS Breakout v3 Nifty500
Runs on Dhan Dext scheduler. Paper mode by default.
Flow each scan cycle:
1. Load Nifty500 universe
2. Fetch live quotes
3. Score all six conditions
4. Rank by momentum → top-N shortlist
5. For each shortlisted symbol: check 3-min breakout
6. If all conditions + breakout pass → size position → place (paper or live)
7. Monitor positions → SL / target / square-off
“”"
import json
import logging
import math
import sys
import time
from datetime import datetime, date, timedelta
import config as cfg
import dhan_client as dhan
from strategy import (
evaluate_all_conditions,
check_breakout,
calc_position_size,
passes_chart_quality_filter,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s \[%(levelname)s\] %(name)s: %(message)s",
handlers=\[logging.StreamHandler(sys.stdout)\],
)
logger = logging.getLogger(“main”)
state = {
"trades_today": 0,
"symbol_trades": {},
"open_positions": {},
"daily_pnl": 0.0,
"halted": False,
"prev_shortlist": set(),
"paper_orders": \[\],
"scan_count": 0,
}
NIFTY500_SYMBOLS: list = []
NIFTY500_SYMBOLS_LIST = “”“RELIANCE,BHARTIARTL,HDFCBANK,ICICIBANK,TCS,SBIN,BAJFINANCE,LT,LICI,INFY,KOTAKBANK,HINDUNILVR,SUNPHARMA,ADANIPORTS,HCLTECH,TITAN,AXISBANK,MARUTI,ADANIPOWER,ULTRACEMCO,ITC,ADANIENT,M&M,HAL,ETERNAL,JSWSTEEL,NTPC,ONGC,BAJAJ-AUTO,BEL,BAJAJFINSV,COALINDIA,DIVISLAB,POWERGRID,NESTLEIND,ASIANPAINT,HINDZINC,TATASTEEL,DMART,SHRIRAMFIN,GRASIM,ADANIGREEN,EICHERMOT,HINDALCO,TVSMOTOR,WIPRO,IOC,INDIGO,SOLARINDS,SBILIFE,DLF,MOTHERSON,TECHM,TORNTPHARM,PIDILITIND,HYUNDAI,ADANIENSOL,TMCV,TRENT,PNB,UNIONBANK,JIOFIN,BHEL,VBL,ICICIAMC,ABB,BOSCHLTD,TATACAP,CGPOWER,CHOLAFIN,BAJAJHLDNG,IDEA,LTM,BPCL,CUMMINSIND,BANKBARODA,POWERINDIA,SIEMENS,GROWW,HDFCLIFE,INDIANB,ZYDUSLIFE,POLYCAB,INDHOTEL,TATAPOWER,APOLLOHOSP,LGEINDIA,BRITANNIA,LENSKART,CIPLA,GAIL,LAURUSLABS,CANBK,PFC,PAYTM,JINDALSTEL,DRREDDY,HEROMOTOCO,LLOYDSME,INDUSTOWER,MARICO,MEESHO,TMPV,IRFC,HDFCAMC,LODHA,TATACONSUM,MUTHOOTFIN,ABCAPITAL,GVT&D,ENRIN,NYKAA,MANKIND,GMRAIRPORT,UNITDSPR,AUROPHARMA,PERSISTENT,AMBUJACEM,VEDL,LUPIN,GODREJCP,ASHOKLEY,OFSS,FEDERALBNK,COFORGE,JSWENERGY,IDBI,SHREECEM,BHARTIHEXA,BHARATFORG,ICICIGI,MAXHEALTH,RECLTD,MCX,NAUKRI,MAZDOCK,JSWINFRA,OIL,HINDPETRO,NHPC,AUBANK,SRF,NTPCGREEN,DIXON,SAIL,WAAREEENER,PHOENIXLTD,INDUSINDBK,HAVELLS,BAJAJHFL,DABUR,WELCORP,IDFCFIRSTB,ICICILIFE,GLENMARK,UNOMINDA,PRESTIGE,BANKINDIA,LTF,ALKEM,NAM-INDIA,TORNTPOWER,OBEROIRLTY,NMDC,ATGL,MAHABANK,APARINDS,GICRE,JSL,APLAPOLLO,MOTILALOFS,RADICO,NATIONALUM,IOB,BERGEPAINT,SCHAEFFLER,FORTIS,RBLBANK,ABBOTINDIA,KALYANKJIL,ATHERENERG,BIOCON,SBICARD,SUZLON,COROMANDEL,MRF,YESBANK,IPCALAB,LINDEINDIA,MPHASIS,TATACOMM,POLICYBZR,SONACOMS,GLAND,FLUOROCHEM,SWIGGY,ANTHEM,GODREJPROP,CPPLUS,PIRAMALFIN,SUNDARMFIN,AEGISLOG,COLPAL,FACT,SUPREMEIND,UPL,PETRONET,AIIL,HDBFS,MFSL,LTTS,AJANTPHARM,VMM,BALKRISIND,GLAXO,360ONE,TIINDIA,NAVINFLUOR,PREMIERENE,HINDCOPPER,M&MFIN,PATANJALI,RVNL,PAGEIND,JKCEMENT,BDL,ANANDRATHI,HFCL,ASTRAL,POONAWALLA,HEXT,KEI,IRCTC,PTCIL,KPRMILL,THERMAX,PWL,CRISIL,NH,EMCURE,ASTERDM,AIAENG,ITCHOTELS,KARURVYSYA,3MINDIA,MEDANTA,NLCINDIA,SHYAMMETL,ENDURANCE,VOLTAS,STARHEALTH,CONCOR,GODREJIND,PIIND,BLUESTARCO,LALPATHLAB,NUVAMA,DALBHARAT,UBL,COCHINSHIP,SAILIFE,ESCORTS,HUDCO,WOCKPHARMA,MRPL,PNBHOUSING,EXIDEIND,IREDA,REDINGTON,KIMS,TATAINVEST,DELHIVERY,HONAUT,IKS,SYRMA,CRAFTSMAN,UCOBANK,LICHSGFIN,AEGISVOPAK,MANAPPURAM,TATATECH,JUBLFOOD,SCHNEIDER,NIACL,ACMESOLAR,CENTRALBK,GODFRYPHLP,KIRLOSENG,ABSLAMC,ACUTAAS,ANGELONE,PPLPHARMA,GODIGIT,HSCL,NETWEB,APOLLOTYRE,NEULANDLAB,GABRIEL,ZFCVINDIA,RRKABEL,BANDHANBNK,CHOLAHLDNG,GILLETTE,TATAELXSI,MSUMI,SUNTV,ASAHIINDIA,CGCL,IIFL,AWL,KPIL,CUB,CHENNPETRO,ACC,FORCEMOT,KAYNES,GESHIP,HBLENGINE,GRSE,DATAPATTNS,PFOCUS,AMBER,ITI,SAGILITY,TIMKEN,JYOTICNC,FSL,IGL,WELSPUNLIV,CEMPRO,DEEPAKNTR,GRANULES,URBANCO,IRB,CARBORUNIV,THELEELA,CASTROLIND,AADHARHFC,ABDL,AFFLE,SJVN,NBCC,BELRISE,EIHOTEL,SUMICHEM,BRIGADE,KAJARIACER,PINELABS,TENNIND,RAMCOCEM,ECLERX,CHALET,COHANCE,CAPLIPOINT,FINCABLES,CESC,ANANTRAJ,TBOTEK,PFIZER,CREDITACC,IGIL,ATUL,EMMVEE,EMAMILTD,ELGIEQUIP,CAMS,IFCI,JSWCEMENT,SARDAEN,AARTIIND,BEML,CHOICEIN,ONESOURCE,TRAVELFOOD,BAYERCROP,ERIS,DCMSHRIRAM,JINDALSAW,JUBLPHARMA,SYNGENE,ZYDUSWELL,TATACHEM,POLYMED,CHAMBLFERT,KPITTECH,DEVYANI,MINDACORP,DEEPAKFERT,VIJAYA,VTL,NATCOPHARM,TRITURBINE,HONASA,USHAMART,PARADEEP,OLAELEC,KFINTECH,INDGN,PVRINOX,JSWDULUX,ENGINERSIN,CARTRADE,BALRAMCHIN,GMDCLTD,ZENTEC,NAVA,GRAPHITE,PGEL,SPLPETRO,CIEINDIA,NIVABUPA,FIVESTAR,LTFOODS,SAMMAANCAP,GALLANTT,TEGA,CYIENT,CONCORDBIO,ARE&M,CROMPTON,SOBHA,ANURAS,J&KBANK,CEATLTD,CCL,ACE,RAINBOW,GPIL,NUVOCO,CANHLIFE,ABREL,BIKAJI,JBMA,DOMS,ZENSARTECH,EIDPARRY,SCI,NSLNISP,PCBL,RKFORGE,UTIAMC,HOMEFIRST,TRIDENT,INOXWIND,JMFINANCIL,MGL,WHIRLPOOL,BLUEDART,SIGNATURE,JAINREC,GRAVITA,APTUS,INDIACEM,TECHNOE,JUBLINGREA,INDIAMART,CANFINHOME,SAREGAMA,ELECON,IRCON,TARIL,RITES,IEX,TITAGARH,JKTYRE,AAVAS,KEC,JPPOWER,SBFC,LEMONTREE,ABLBL,BLS,BSOFT,BLUEJET,INTELLECT,BATAINDIA,AFCONS,FIRSTCRY,BBTC,OLECTRA,MMTC,CLEAN,SWANCORP,JWL,TEJASNET,TTML,RAILTEL,NCC,RHIM,RPOWER,NEWGEN,SONATSOFTW,ZEEL,SAPPHIRE,ABFRL,LATENTVIEW,MAPMYINDIA,HEGAM,BSE,CDSL”“”.split(“,”)
def load_universe():
*# Universe is embedded (500 Nifty500 constituents, sourced 2026-10-26).*
*# Live NSE CSV fetch fails from the cloud runner (SSL), so no network call here.*
symbols = \[s.strip().upper() for s in NIFTY500_SYMBOLS_LIST if s.strip()\]
logger.info("Loaded %d Nifty500 symbols (embedded list)", len(symbols))
return symbols
def normalise_quote(raw):
if not raw:
return None
try:
ohlc = raw.get("ohlc") or {}
q = {
"ltp": float(raw.get("last_price") or raw.get("lastTradedPrice") or 0),
"open": float(ohlc.get("open") or 0),
"high": float(ohlc.get("high") or 0),
"low": float(ohlc.get("low") or 0),
"close": float(ohlc.get("close") or 0),
"volume": float(raw.get("volume") or 0),
"avgprice": float(raw.get("average_price") or 0),
"year_high": float(raw.get("52_week_high") or 0),
"buyquantity": float(raw.get("buy_quantity") or 0),
"sellquantity": float(raw.get("sell_quantity") or 0),
"change_per": float(raw.get("net_change") or 0),
"timestamp": raw.get("last_trade_time") or datetime.now().isoformat(),
}
if q\["ltp"\] <= 0:
return None
if q\["avgprice"\] <= 0 and q\["high"\] > 0:
q\["avgprice"\] = (q\["high"\] + q\["low"\] + q\["ltp"\]) / 3
return q
except Exception as e:
logger.debug("normalise_quote error: %s", e)
return None
_sec_id_cache = {}
_scrip_map = {}
def load_scrip_master():
*# Dhan's public instrument master (NSE equities) -> symbol to security id*
import csv, io, requests
r = requests.get("https://images.dhan.co/api-data/api-scrip-master.csv", timeout=60)
r.raise_for_status()
for row in csv.DictReader(io.StringIO(r.text)):
if row.get("SEM_EXM_EXCH_ID") == "NSE" and row.get("SEM_SEGMENT") == "E" and row.get("SEM_INSTRUMENT_NAME") == "EQUITY":
key = (row.get("SEM_TRADING_SYMBOL") or "").strip().upper()
sid = (row.get("SEM_SMST_SECURITY_ID") or "").strip()
if key and sid:
\_scrip_map\[key\] = sid
logger.info("Scrip master loaded: %d NSE equities", len(\_scrip_map))
def get_security_id(symbol):
if not \_scrip_map:
try:
load_scrip_master()
except Exception as ex:
logger.error("Scrip master load failed: %s", ex)
return None
sid = \_scrip_map.get(symbol.strip().upper())
if sid:
\_sec_id_cache\[symbol\] = sid
return sid
def market_time(fmt=“%H:%M”):
return datetime.now().strftime(fmt)
def is_market_open():
t = market_time()
return "09:15" <= t <= "15:30"
def is_entry_allowed():
return market_time() < cfg.NO_NEW_ENTRIES_AFTER
def is_square_off_time():
return market_time() >= cfg.SQUARE_OFF_TIME
def risk_gate_ok(symbol, equity):
if state\["halted"\]:
return False, "system_halted"
if state\["daily_pnl"\] <= -(equity \* cfg.MAX_DAILY_LOSS_PCT_OF_EQUITY / 100):
state\["halted"\] = True
return False, "daily_loss_limit_hit"
if len(state\["open_positions"\]) >= cfg.MAX_OPEN_POSITIONS:
return False, f"max_open_positions={cfg.MAX_OPEN_POSITIONS}"
if state\["trades_today"\] >= cfg.MAX_TRADES_PER_DAY:
return False, f"max_trades_per_day={cfg.MAX_TRADES_PER_DAY}"
sym_count = state\["symbol_trades"\].get(symbol, 0)
if sym_count >= cfg.MAX_TRADES_PER_SYMBOL_PER_DAY:
return False, f"max_trades_per_symbol={cfg.MAX_TRADES_PER_SYMBOL_PER_DAY}"
if not is_entry_allowed():
return False, f"after_no_entry_time={cfg.NO_NEW_ENTRIES_AFTER}"
return True, "ok"
def paper_order(symbol, qty, entry, stop, target, tag):
oid = f"PAPER-{symbol}-{int(time.time())}"
record = {
"order_id": oid, "symbol": symbol,
"qty": qty, "entry": entry,
"stop": stop, "target": target,
"tag": tag,
"ts": datetime.now().isoformat(),
}
state\["paper_orders"\].append(record)
logger.info("\[PAPER\] %s", json.dumps(record))
return oid
def live_order(symbol, security_id, qty, entry, stop, target):
if not (cfg.MODE == "live" and cfg.LIVE_TRADING_ENABLED):
logger.error("live_order called but live mode disabled -- aborting")
return None
result = dhan.place_order(
security_id=security_id,
exchange_segment="NSE_EQ",
transaction_type="BUY",
quantity=qty,
order_type=cfg.ORDER_TYPE,
product_type=cfg.PRODUCT_TYPE,
price=entry,
tag=f"RSB-{symbol}",
)
if result and result.get("orderId"):
oid = result\["orderId"\]
logger.info("\[LIVE\] ORDER PLACED symbol=%s qty=%d entry=%.2f "
"stop=%.2f target=%.2f order_id=%s",
symbol, qty, entry, stop, target, oid)
return oid
logger.error("\[LIVE\] ORDER FAILED symbol=%s result=%s", symbol, result)
return None
def scan_once():
global NIFTY500_SYMBOLS
state\["scan_count"\] += 1
if not is_market_open():
logger.info("Market closed -- skipping scan %d", state\["scan_count"\])
return
if is_square_off_time():
\_square_off_all()
return
if not NIFTY500_SYMBOLS:
NIFTY500_SYMBOLS = load_universe()
if not NIFTY500_SYMBOLS:
logger.error("Universe empty -- cannot scan")
return
funds_data = dhan.get_funds()
equity = 0.0
if funds_data:
equity = float(funds_data.get("availabelBalance")
or funds_data.get("availableBalance", 0))
if equity <= 0:
logger.warning("Equity=%.2f -- position sizing will fail", equity)
scored = \[\]
missing = 0
*# Batch-fetch quotes (Dhan marketfeed accepts many ids per call)*
sids = \[\]
for symbol in NIFTY500_SYMBOLS:
sid\_ = get_security_id(symbol)
if sid\_:
sids.append(sid\_)
else:
missing += 1
quotes = {}
for k in range(0, len(sids), 200):
chunk = \[int(x) for x in sids\[k:k + 200\]\]
time.sleep(1.2)
resp = dhan.\_post("/marketfeed/quote", {"NSE_EQ": chunk})
if resp:
quotes.update(((resp.get("data") or {}).get("NSE_EQ")) or {})
logger.info("Quotes fetched: %d of %d", len(quotes), len(sids))
for symbol in NIFTY500_SYMBOLS:
sid = get_security_id(symbol)
if not sid:
continue
raw = quotes.get(str(sid))
if not raw:
missing += 1
continue
q = normalise_quote(raw)
if not q:
missing += 1
continue
cond = evaluate_all_conditions(q)
if cond\["momentum"\] is not None:
scored.append({"symbol": symbol, "sid": sid, "quote": q, "cond": cond})
logger.info("Scan #%d: %d/%d scored, %d missing",
state\["scan_count"\], len(scored), len(NIFTY500_SYMBOLS), missing)
if not scored:
return
scored.sort(key=lambda x: x\["cond"\].get("momentum") or -math.inf, reverse=True)
for i, s in enumerate(scored\[:cfg.LOG_TOP_N\]):
c = s\["cond"\]
logger.info("RANK %02d | %-12s | mom=%.2f | rs=%.2f | sm=%.2f | "
"br=%.3f | om=%.2f | dist=%.2f%% | pass=%s",
i+1, s\["symbol"\],
c.get("momentum") or 0, c.get("rs_score") or 0,
c.get("smart_momentum") or 0, c.get("buying_ratio") or 0,
c.get("open_move") or 0,
c.get("distance_to_year_high_pct") or 999, c\["passed"\])
shortlist_now = set(s\["symbol"\] for s in scored\[:cfg.SHORTLIST_TOP_N\])
for candidate in scored\[:cfg.SHORTLIST_TOP_N\]:
symbol = candidate\["symbol"\]
cond = candidate\["cond"\]
sid = candidate\["sid"\]
q = candidate\["quote"\]
if symbol in state\["open_positions"\]:
continue
if not cond\["passed"\]:
logger.info("REJECT %s | %s", symbol, " | ".join(cond\["reasons"\]))
continue
if cfg.REQUIRE_TWO_CONSECUTIVE_SCANS:
if symbol not in state\["prev_shortlist"\]:
logger.info("STABILITY_SKIP %s -- not in prev shortlist", symbol)
continue
ok, reason = risk_gate_ok(symbol, equity)
if not ok:
logger.info("RISK_GATE %s: %s", symbol, reason)
continue
ltp = q\["ltp"\]
today_str = date.today().strftime("%Y-%m-%d")
candles = dhan.get_candles(sid, "NSE_EQ",
from_date=today_str,
to_date=today_str,
interval=str(cfg.CANDLE_INTERVAL_MINUTES))
if not candles:
logger.info("NO_CANDLES %s", symbol)
continue
*# ── Chart Quality Filter (CQF) ─────────────────────────────────────────────*
if cfg.CQF_ENABLED:
hist_to = today_str
hist_from = (date.today() - timedelta(days=300)).strftime("%Y-%m-%d")
try:
daily_candles = dhan.get_candles(sid, "NSE_EQ",
from_date=hist_from,
to_date=hist_to,
interval="1440") *# daily*
except Exception as ex:
logger.warning("CQF daily fetch failed for %s: %s -- skipping", symbol, ex)
continue
year_high = candidate.get("high", q.get("high", 0))
cqf = passes_chart_quality_filter(
daily_candles or \[\], year_high)
logger.info("CQF %s: passed=%s | %s", symbol, cqf\["passed"\], cqf\["reason"\])
if not cqf\["passed"\]:
continue
*# ───────────────────────────────────────────────────────────────────────────*
breakout = check_breakout(candles)
logger.info("BREAKOUT %s: confirmed=%s | %s",
symbol, breakout\["confirmed"\], breakout\["reason"\])
if not breakout\["confirmed"\]:
continue
entry_price = ltp
stop_price = breakout\["breakout_candle"\]\["low"\]
if stop_price >= entry_price:
logger.info("REJECT %s: stop=%.2f>=entry=%.2f", symbol, stop_price, entry_price)
continue
qty, err = calc_position_size(equity, entry_price, stop_price)
if qty <= 0:
logger.info("REJECT %s: qty=0 (%s)", symbol, err)
continue
rr = cfg.REWARD_RISK_RATIO
target_price = entry_price + rr \* (entry_price - stop_price)
logger.info("SIGNAL %s | entry=%.2f stop=%.2f target=%.2f qty=%d",
symbol, entry_price, stop_price, target_price, qty)
if cfg.MODE == "paper":
oid = paper_order(symbol, qty, entry_price, stop_price,
target_price, f"RSB-{symbol}")
elif cfg.MODE == "live" and cfg.LIVE_TRADING_ENABLED:
oid = live_order(symbol, sid, qty, entry_price, stop_price, target_price)
else:
logger.warning("Live mode not fully enabled -- trade skipped")
continue
if oid:
state\["open_positions"\]\[symbol\] = {
"order_id": oid, "entry": entry_price,
"stop": stop_price, "target": target_price, "qty": qty,
}
state\["trades_today"\] += 1
state\["symbol_trades"\]\[symbol\] = (
state\["symbol_trades"\].get(symbol, 0) + 1)
state\["prev_shortlist"\] = shortlist_now
def monitor_positions():
for symbol, pos in list(state\["open_positions"\].items()):
sid = get_security_id(symbol)
if not sid:
continue
raw = dhan.get_quote(sid)
q = normalise_quote(raw) if raw else None
if not q:
continue
ltp = q\["ltp"\]
if ltp <= pos\["stop"\]:
logger.info("SL_HIT %s ltp=%.2f stop=%.2f", symbol, ltp, pos\["stop"\])
\_exit_position(symbol, pos, ltp, "SL")
elif ltp >= pos\["target"\]:
logger.info("TARGET_HIT %s ltp=%.2f target=%.2f", symbol, ltp, pos\["target"\])
\_exit_position(symbol, pos, ltp, "TARGET")
def _exit_position(symbol, pos, exit_price, reason):
pnl = (exit_price - pos\["entry"\]) \* pos\["qty"\]
state\["daily_pnl"\] += pnl
logger.info("EXIT %s @ %.2f reason=%s pnl=%.2f cumulative_pnl=%.2f",
symbol, exit_price, reason, pnl, state\["daily_pnl"\])
del state\["open_positions"\]\[symbol\]
def _square_off_all():
if not state\["open_positions"\]:
return
logger.info("SQUARE_OFF_TIME -- closing all %d positions",
len(state\["open_positions"\]))
for symbol, pos in list(state\["open_positions"\].items()):
sid = get_security_id(symbol)
raw = dhan.get_quote(sid) if sid else None
q = normalise_quote(raw) if raw else None
ltp = q\["ltp"\] if q else pos\["entry"\]
\_exit_position(symbol, pos, ltp, "SQUARE_OFF")
def run():
logger.info("=" \* 60)
logger.info("RS BREAKOUT v3 NIFTY500 | mode=%s | live_enabled=%s",
cfg.MODE, cfg.LIVE_TRADING_ENABLED)
if cfg.MODE == "live" and cfg.LIVE_TRADING_ENABLED:
logger.warning("LIVE TRADING ACTIVE -- real orders will be placed")
logger.info("=" \* 60)
NIFTY500_SYMBOLS\[:\] = load_universe()
if not NIFTY500_SYMBOLS:
logger.error("Cannot start -- universe empty")
return
logger.info("Universe: %d symbols loaded", len(NIFTY500_SYMBOLS))
*# Wait until market opens (09:15)*
while market_time() < "09:15":
logger.info("Waiting for market open 09:15 (now %s)", market_time())
time.sleep(30)
scan_interval = 60
*# Continuous loop: keep running every scan_interval until market close (15:30)*
while is_market_open():
try:
if not is_square_off_time():
scan_once()
monitor_positions()
except Exception as e:
logger.exception("Cycle error (will retry next cycle): %s", e)
logger.info("Cycle complete. Open positions: %d | Trades today: %d | PnL: %.2f",
len(state\["open_positions"\]), state\["trades_today"\], state\["daily_pnl"\])
time.sleep(scan_interval)
logger.info("Market closed (%s). Exiting run().", market_time())
if _name_ == “_main_”:
run()