IEOD Prompts
Step 1 — Parquet File Storage Rule
Requested:
Define the Parquet file storage rule:
(Masid)_EQ— yearly file for spot/index scripts (underlying = 0)(Masid)_MONTHNAME— monthly file for F&O scripts (underlying > 0)Based on these rules, update
update_fo_fileandupdate_index_file. Renameupdate_index_file→update_EQ_file.IEOD data is no longer stored in Postgres — all intraday data lives in Parquet files only.
Parquet Store Layout:
nseieoddata/
N/NIFTY/
2024/
1_EQ ← NIFTY spot, all of 2024 (masid=1, underlying=0)
1_JANUARY ← NIFTY near-month fut, January expiry
1_FEBRUARY
...
B/BANKNIFTY/
2024/
2_EQ ← BANKNIFTY spot (masid=2, underlying=0)
2_JANUARY
...
Rule summary:
| Script type | underlying | File pattern | Frequency |
|---|---|---|---|
| Spot / Index | 0 | {masid}_EQ | One file per year |
| Futures / Options | > 0 | {masid}_{MONTH_NAME} | One file per expiry month |
Implementation status:
| Item | Status |
|---|---|
Parquet store layout (_EQ yearly + _MONTHNAME monthly) | ✓ Implemented in gdfl/ pipeline |
update_fo_file — writes F&O data to monthly Parquet | ✓ Implemented |
update_index_file → renamed to update_EQ_file | ⚠ Pending — update_index_file still exists in nseIEOD.py; only prints ticker list, insert logic not yet written |
| IEOD data no longer written to Postgres | ✓ scr_nsefo / scr_nseeq staging tables cleared after pack |
Files involved:
backend/nseieod/nseIEOD.py—IEod.update_fo_file(),IEod.update_index_file()(to be renamed)backend/nseieod/gdfl/gdfl_zip_extractor.py— GDFL CSV → Parquet writerbackend/nseieod/gdfl/step1_convert_yearly.py— packs monthly files → yearly_EQ
Step 2 — nseieod CLAUDE.md Rules
Requested:
Include the following rules in
CLAUDE.mdinbackend/nseieod/:
- Always use class
IEodfor IEOD data ingestion and updates- Always use class
Con_Price_Getterfor pulling data from Parquet files and Postgres DB- Any changes made in
backend/nseieod/nseieod.pymust be documented indocs/developer/packages/nseieod/nseieod-main.md
Implementation:
Created backend/nseieod/CLAUDE.md with the following rules:
Rule 1 — IEod is the only entry point for ingestion
Rule 2 — Con_Price_Getter is the only entry point for reading prices
Rule 3 — nseIEOD.py changes must be reflected in nseieod-main.md
Class responsibilities:
| Class | File | Purpose |
|---|---|---|
IEod | nseIEOD.py | Ingests GDFL CSV → Parquet; packs staging DB → Parquet |
Con_Price_Getter | nseIEOD.py | Reads price series from Parquet (and Postgres fallback) |
Never call Parquet files directly from outside nseieod/ — always go through Con_Price_Getter.
Never write intraday data outside of IEod methods.
Files involved:
backend/nseieod/CLAUDE.md— rules file (created)backend/nseieod/nseIEOD.py— contains both classesdocs/developer/packages/nseieod/nseieod-main.md— must be updated whennseIEOD.pychanges
Step 3 — Merge get_ieod_data into Con_Price_Getter
Requested:
Refer to
get_ieod_datafromieod_exp.pyand insert the same logic intoCon_Price_GetterinnseIEOD.py. After review,ieod_exp.pywill be deleted.
What get_ieod_data has that Con_Price_Getter currently lacks:
| Feature | get_ieod_data (ieod_exp.py) | Con_Price_Getter.__get_contract_month |
|---|---|---|
_EQ year cache | ✓ Reads yearly file once, reuses for each month | ✗ Re-reads the full _EQ file on every month iteration |
| Legacy monthly spot fallback | ✓ Falls back to {root}_{MONTH} if _EQ missing | ✗ Returns empty if _EQ not found — no fallback |
| Month name casing | ✓ UPPER() → 1_JANUARY (matches file names) | ✗ Title case January → may miss files |
Takes masid directly | ✓ No script-name string lookup needed | — works differently (resolves via script name + opttype) |
| Flexible date parsing | ✓ Handles YYYY-MM-DD, DD-MM-YYYY, DD/MM/YYYY | — uses pd.to_datetime directly |
Key improvement — eq_cache:
get_ieod_data caches the yearly _EQ file per year so it is only read from
disk once, even when iterating over 12 months in a year query:
eq_cache = {} # year → DataFrame
for month_start in months:
if year not in eq_cache:
eq_cache[year] = pd.read_parquet(eq_path) # read once
chunk = eq_cache[year]
chunk = chunk[chunk['sdate'].dt.month == month_start.month]
Con_Price_Getter.__get_contract_month currently calls pd.read_parquet
inside the loop — reads the same file up to 12 times for a full year query.
Key improvement — legacy fallback:
# get_ieod_data — tries new format first, falls back gracefully
eq_path = os.path.join(year_dir, f"{root_id}_EQ")
if os.path.exists(eq_path):
# use yearly file
...
continue # skip monthly path entirely for spot
# Legacy fallback — pre-2016 monthly spot files
legacy_path = os.path.join(year_dir, f"{root_id}_{month_name}")
Implementation plan:
- Add a
get_prices_by_masid(masid, from_date, to_date)method toCon_Price_Getterthat contains theget_ieod_datalogic (cache + legacy fallback + UPPER month names) - Refactor
__get_contract_monthto call this shared logic internally - Verify output matches
get_ieod_dataon a sample masid/date range - Delete
backend/nseieod/ieod_exp.py - Update
docs/developer/packages/nseieod/nseieod-main.md
Status: ✓ Implemented — ieod_exp.py deleted, logic merged into Con_Price_Getter.
fine_tune Con_Price_Getter
Requested
- Remove
datapathparameter fromCon_Price_Getter— get the path from env viaConfig.IEOD_DATA_PATH. - Create
backend/scratch/getprices.py— fetch NIFTY IEOD data (1-1-2009 → 31-12-2011) usingCon_Price_Getterand save asnifty_2009_2011.csvinConfig.SCRATCH_CSV_PATH.
Changes made
backend/nseieod/nseIEOD.py
Con_Price_Getter.__init__signature changed:# Beforedef __init__(self, script, datapath, fromdate="", ...)# Afterdef __init__(self, script, fromdate="", ...)self.datapathis now set internally fromConfig.IEOD_DATA_PATH— callers no longer pass a path.from app.config import Configadded at module top.
backend/scratch/getprices.py (new file)
cp = Con_Price_Getter(
script = "NIFTY",
fromdate = "1-1-2009",
todate = "31-12-2011",
opttype = "SPOT",
)
df = cp.getprices()
df.to_csv(os.path.join(Config.SCRATCH_CSV_PATH, "nifty_2009_2011.csv"), index=False)
Run from backend/:
python -m scratch.getprices
Implementation status
| Item | Status |
|---|---|
datapath param removed from Con_Price_Getter | ✓ Done |
Config.IEOD_DATA_PATH used internally | ✓ Done |
backend/scratch/getprices.py created | ✓ Done |
Output saved to Config.SCRATCH_CSV_PATH/nifty_2009_2011.csv | ✓ Done |
GDFL Extraction
Requested
Extract daily CSV files from GDFL ZIP archives in CSV_PATH into yearly sub-folders,
then check for missing trading days and write a report.
- Source: ZIP files in
Config.CSV_PATH - Output:
CSV_PATH/{year}/{filename}.csv - Skip all BACKADJUSTED files
- Three file types: Index (
GFDLCM_INDICES_), Equity (GFDLCM_EQ), FO (GFDLNFO_CONTRACT_,GFDLNFO_NIFTY_BNK) - After extraction, compare extracted dates against
get_nse_working_days()fromliquid.py - Write missing dates to
CSV_PATH/missing.txt
Implementation
Created backend/nseieod/gdfl/extract.py (Part 1).
Run from backend/:
python -m nseieod.gdfl.extract # extract 2016-today
python -m nseieod.gdfl.extract --dry-run # preview, no writes
python -m nseieod.gdfl.extract --from 2020 # start from 2020
python -m nseieod.gdfl.extract --from 2016 --to 2022
Output structure:
CSV_PATH/
2016/
GFDLCM_INDICES_01012016.csv
GFDLCM_EQ_01012016.csv
GFDLNFO_NIFTY_BNK_OPT_01012016.csv
...
2017/
...
missing.txt <-- missing dates report per file type
missing.txt format:
GDFL Missing Date Report
Generated : 2026-06-03 11:00:00
Range : 2016-01-01 to 2026-06-03
Index files -- 3 missing date(s)
2016-01-15
2016-03-22
...
Equity files -- 0 missing date(s)
(none -- all working days present)
FO files -- 5 missing date(s)
...
Implementation Status
| Item | Status |
|---|---|
extract.py created in backend/nseieod/gdfl/ | Done |
ZIP scan from Config.CSV_PATH | Done |
| BACKADJUSTED files skipped | Done |
| Index / Equity / FO classification | Done |
| Year subfolder extraction | Done |
Missing date check via get_nse_working_days() | Done |
missing.txt written to CSV_PATH | Done |
--dry-run, --from, --to CLI flags | Done |
GDFL Fix1.0
Problem: Top-level ZIPs contain inner ZIPs (e.g. monthly bundles). The original
code only scanned for .csv at the top level — inner ZIPs were invisible, so nothing was extracted.
Fix: extract.py rewritten with recursive ZIP handling.
- Inner
.zipentries inside any ZIP are opened in memory (io.BytesIO) and recursively scanned - Handles arbitrary nesting depth
- Output log now shows
Inner ZIPs opened: Ncount
Status: Fixed in backend/nseieod/gdfl/extract.py
GDFL Fix1.1
Start Date 1 JUN 2016 End Date 31-10-2024
Do it for Every Year
1.) Get NSE Working from get_nse_working_days(in liquid.py of nseieod) 2.) Check Corresponding GFDL_file Exists in corresponding directory By Knowing the following information
i) from 2016-Mid 2020 Until file prefix has(GFDLNFO_NIFTY_BNK_OPT_) only Nifty and bank nifty optionsdata there(nfo) no EQ!! ii) from Mid 2020 (GFDLNFO_CONTRACT_) IT ALSO has NFO (includes all stocks F&O) Till Mid 2022 iii) from mid 2022 there should 3 files
a.GFDLCM_INDICES_ (Index data) EQ Segment b.) GFDLCM_STOCK_(Stock Data) Stocks EQ c.) GFDLNFO_CONTRACT_(all F&O) contract Pls report if any missing files
to do above task Pls update check-files.py in gdfl folder(over write existing code)
at Last tell me how to run
Enhance Ieod.py 1.1
1.) Pls Create a Helper file under nseieod folder that stores common function
and Insert following function
1.) MarketOpeningTime(Date)
if Date before 2010-01-04 it is 9:55AM other wise it is 9:15 AM
2.) MarketClosingTime(Date) So far 15:30
So for nseIEOD.py Con_Price_Getter Accepts only (fromdate and todate) Whole date
Pls modify this class as it accepts both and date and time
For eg FromDate="2011-01-01 09:30:00: todate:"2011-02-01"
is equal to FromDate="2011-01-01 09:30:00: todate=todate+ " "+MarketClosingTime(todate)
Fix markettime 1.2
on Special Day(which got from Postgres Table ) market time based on Start_time and end_time as per db Pls change MarketOpeningTime(Date) and MarketClosingTime(Date) functions as per that
Filling_Utclass 1.0
Pls Refer ut_Classes.py it has 3 classes which serves refilling missing datas
1.) Resu_Holidays Don't touch this class we will check this latter 2.) FillingMissingTimes
During this Always use helpers.py get_nse_working_days,MarketOpeningTime,MarketClosingTime to get expected bars
it fills 3 Types fill a.) One fill bar fill if any bar missing it fill with pre bar data Here I I need one more improvement if first bar missing (MarketOpeningTime) then fill with next bar
these type filling should be marked as -1 in "filled' columns make change (currently it stores just 1)
b.) For 2-5 Consecutive Minutes
use Linear interpolation on Close
100 100.2 100.4 100.6 100.8 101
Then build synthetic OHLC
Mark these type of with filled=-2
c.) For 5-20 Consecutive Minutes
Then fill with Prev day Pattern (Pls refer __get_prev_day_pattern)
if consecutives bars less than 20 marks this type as filled=-3
if missing bar >20 then fill with filled=-4
3.) Next Restore_Missingdates class
This Class has retrieve Exact eod data (OHLC) from post gres db
then we may get 3 Patteren
i.) Open to (High or low) ii.)(High or low) to (High or low) //Pattern bettewn high and low iii.)(High or low) to close
after identifying these 3 patterns fill with Prev day Pattern with filled=-5
if more than one consecutive days missing fill with filled=-6
Note all these filling must fill with Volume=0
if filling has less than -3 These data clusters must be replaced with data with some other data vendor
Based on above Information Pls change ut_Classes.py
Rawfolder
Before processing each file Copy the existing file in another sub folder raw under IEOD_DATA_PATH
Pls copy along with it's folder structure after copying this we may know which file processed
Checkpriceswithtime
1.) In Recent Changes I dropped Sdetails.py and Check_qc.py So Pls Remove it's documentation if docs folder 2.) Nseieod/getprices now accepts only whole days Pls the that why can also accept time value also So that I can filter prices between 10:00 AM to 12:00 AM(Pls I have changed Con_price_Getter as per that)
LiquidScripts
Create a Table in duck_db to maintain Liquid Scripts
Currently only below these
Liquid scripts
--------------
masid=1 NIFTY spot -> N/NIFTY/{year}/1_EQ
masid=2 BANKNIFTY spot -> B/BANKNIFTY/{year}/2_EQ
Store this In to LiquidScripts table in duck_db
along with spot_start_date,cfut_start_date,fut_start_date,checked_till,last_checked_date
Cfut Means continuous future
StartDate Check
Before checking a data it should know whether it is SPOT or CFuture Underline>0 and monthindex>0 or Future Underline>0 and monthindex=0 based on these Starting date should Retrieved from liquid_scripts from duck_db
purpose of Retrived Start is Check should not performed before StartDate if Start date defined in liquid_scripts
Pls Implement this in check in fix_data
Check_options_Index
I want to audit The Scripts Stored in Parquet files
Parameter
FromDate :2016-06-01
Todate:2020-05-31" Scripts['NIFTY','BANKNIFTY']
As You Know from June 2016 I am Storing Only Banknifty and nifty OPtions(both weekly and monthly) Till May 2020
In this Period I want check Liquidity of ATM,OTM1,OTM2,OTM3,OMT4,ITM1,ITM2,ITM3,IMT4 of Options Contracts
ATM should derived from Prev.day Close
We Can Get EOD Close prices from scr_nseeq_eod for Scripts(masid can be derived from scr_master)
Their Derived contracts can get from below query
SELECT DISTINCT sm.masid, sm.mname, sm.typ, sm.edate, sm.strike, sm.opttyp, sm.monthindex FROM scr_master sm JOIN scr_master nifty ON nifty.mname = 'NIFTY' JOIN scr_nsefo_eod eod ON eod.masid = sm.masid WHERE (sm.masid = nifty.masid OR sm.underly = nifty.masid) AND eod.sdate >= DATE '2016-06-01' AND eod.sdate < DATE '2017-01-01' and sm.typ>3 ORDER BY sm.edate, sm.strike, sm.opttyp, sm.monthindex, sm.mname;
Using similar Query we can get all contracts
from this Information you have check liquity or missing data for ATM,OTM1,OTM2,OTM3,OMT4,ITM1,ITM2,ITM3,IMT4 Data along with EQ,CFut (Like fix_data.py Check)
You can write this in audit.py(later we will merge all on single file)
if need we can fill
Implementation
Created backend/nseieod/audit.py with public audit(from_date, to_date, scripts).
Run from backend/:
python -m nseieod.audit --from 2016-06-01 --to 2020-05-31
python -m nseieod.audit --from 2016-06-01 --to 2016-12-31 --scripts NIFTY
How it works (per root, per trading day):
- ATM from prev-day close — spot EOD close is read from
scr_nseeq_eod(masid = spot). The ATM strike is the nearest available strike to the previous working day's close. - Ladder —
ATM, OTM1-4, ITM1-4taken from the actual strike grid of each live expiry (bothCEandPE→ 18 contracts/expiry). Moneyness is assigned per option type (CE: above ATM = OTM; PE: above ATM = ITM). - Live expiries — every expiry with
edate >= trade_datewithin a 100-day horizon. Weekly vs monthly is derived fromedate(the last expiry of a calendar month = monthlyM), nottyp— inscr_masterall of these options are stored astyp=5. - EOD-gated — a ladder contract is audited only if it actually traded that
day, i.e. it has a row in
scr_nsefo_eod. Deep OTM/ITM strikes that never traded are ignored (not reported as missing data), per the project leader's instruction. - Coverage — each audited contract's intraday Parquet bars
(
{IEOD_DATA_PATH}/{L}/{NAME}/{year}/{root}_{TRADE_MONTH}) are counted vs the full session. Options trade intermittently, so per-minute gap clustering is not used; coverage % is the liquidity signal.
Report (two views):
- [A] NO intraday bars — contracts that traded per EOD yet have zero 1-minute bars in Parquet. These are the genuine missing-data cases.
- [B] Liquidity profile — coverage % (bars / full session) by moneyness
(
n_days, mean, median, min, %<50%, %zero), so ATM-vs-OTM/ITM liquidity is visible at a glance.
The EQ (spot) and CFut (near-month future) coverage is checked by delegating to
fix_data.check for the same roots (non-fatal if the DuckDB catalog is locked).
The full report is tee'd to Config.CSV_PATH/audit_{from}_{to}_{ts}.txt.
Status: Implemented in backend/nseieod/audit.py. Verified on NIFTY +
BANKNIFTY for 2016-06 → 2016-07 (the only window currently converted to
Parquet): 0 missing-intraday contract-days; ATM/near-money median coverage
~80-90%, tailing off for deep ITM as expected. Filling is not implemented
yet ("if need we can fill").
Note: 2017-2020 option Parquet is not yet ingested — run IEod.update_folder()
for those years first (writes to the root store), then re-run the audit.
| Item | Status |
|---|---|
audit.py created | Done |
ATM from prev-day close (scr_nseeq_eod) | Done |
| ATM/OTM1-4/ITM1-4 ladder (CE+PE) from real strike grid | Done |
All live expiries (weekly + monthly from edate) | Done |
EOD-gated on scr_nsefo_eod (ignore untraded deep strikes) | Done |
| Coverage report + liquidity profile | Done |
EQ/CFut check (delegates to fix_data.check) | Done |
| Fill missing/low-liquidity option bars | Pending |
Audit format
For Options I require below format report
| Date | Script | EodPrice | Opt | Strike | Type | missingBars | Time | Filling |
|---|---|---|---|---|---|---|---|---|
| 2016-07-08 | BANKNIFTY | 17321 | CE | 17300 | ATM | 2 | 9:25:00 | -2; 2-5 bar linear interpolation |
| 2016-07-08 | BANKNIFTY | 17321 | CE | 17200 | ITM1 | 2 | 9:25:00 | -2; 2-5 bar linear interpolation |
| 2016-07-08 | BANKNIFTY | 17321 | PE | 17200 | OTM1 | 2 | 9:25:00 | -2; 2-5 bar linear interpolation |
| for Bank nifty Nifty Spot and CFut I require Following format if it is missing more that 3 working days(whole Day) |
| Script | Last Spot IEOD Date | Last Spot Cfut Date |
|---|
Implementation
backend/nseieod/audit.py now prints two additional report sections (run via
python -m nseieod.audit --from ... --to ... --scripts ...):
[Option Gaps] -- one row per consecutive missing-1-minute cluster for
every audited (EOD-traded) ladder contract, in the requested format:
| Date | Script | EodPrice | Opt | Strike | Type | missingBars | Time | Filling |
EodPriceis the previous day's spot close used to derive the ATM strike (same value_ladder()was given).Typeis the moneyness label (ATM,OTM1-4,ITM1-4).missingBars/Timecome fromfix_data._find_clusters()-- the cluster's bar count and start time (H:MM:SS, no leading zero, matching the example).Fillingisfix_data._fill_note()(thefilled-flag convention fromnseieod/CLAUDE.mdrule 7:-1single-bar carry,-22-5 bar interpolation,-36-20 bar previous-day pattern,-4>20 bar -- replace with another vendor), reformatted with a space after the;.
[EQ/CFut Staleness] -- one row per script, only when the spot (IEOD) or
near-month continuous future (CFut) Parquet data is missing more than 3 whole
NSE working days as of to_date:
| Script | Last Spot IEOD Date | Last Spot Cfut Date |
_last_data_date()walks NSE working days backwards fromto_date(viafix_data.check_day) until it finds a day withbar_count > 0.- A script is omitted entirely if both spot and CFut are within 3 working days
of
to_date(i.e. "up to date"). Nonemeans no data was found for that segment anywhere in[from_date, to_date]-- expected for CFut during the 2016-06 -> 2020-05 options-only GDFL era.
Status: Implemented and verified for BANKNIFTY 2016-06-01 -> 2016-06-10 --
[Option Gaps] printed per-cluster rows with Filling codes -1 through -4
across the ladder, and [EQ/CFut Staleness] correctly flagged BANKNIFTY's
near-month CFut as None (no CFut Parquet data in this options-only period).
| Item | Status |
|---|---|
[Option Gaps] table (Date/Script/EodPrice/Opt/Strike/Type/missingBars/Time/Filling) | Done |
[EQ/CFut Staleness] table (Script/Last Spot IEOD Date/Last Spot Cfut Date), >3 working-day gate | Done |
GDFL Detailed Quality Check
Pls Modify nseieod audit.py so that it will generate report (Quality check Report) Pls refer attached CSV
Since Report is so wide Pls generate as reading format
To generate Report You have to following
1.) As You know ATM,OTM1,ITM4 calculated base on Pre.date EOD Close price 2.) Get Current Date OHLC Data from Post gres(Spot) 3.) Get Options Scripts of Both CE&PE (ATM,OTM 1to 4 ,ITM 1to 4) compress that in daily time frame 4.) Check Open,High,Low Not Close(since NSE calculates EOD Close as Average of last 30 MIn it is not LTP where as one minute data would give ltp) 5.) After getting both datasets now Compare two for the Day if it matches Place (for 17500CE-OK) or 17500CE-High Not matching) Like that
if any data (strike Missing ) it should be in both Data sets if missing from GDFL(One minute) Pls report... if any data fould in GDFL but not in EOD That also should be reported
Pls Not it should checked from only ranges given in Parameer also note in 2016 Only Bank nifty has weekly options Nifty has only monthly options
Pls check with out asking much from me
Implementation
backend/nseieod/audit.py now generates a GDFL Detailed Quality Check report.
It is printed first by audit() (every run) and can be run on its own with
--quality-only.
Run from backend/:
python -m nseieod.audit --from 2016-06-01 --to 2016-06-30 --quality-only
python -m nseieod.audit --from 2016-06-01 --to 2016-06-30 --scripts BANKNIFTY --quality-only
python -m nseieod.audit --from 2016-06-01 --to 2020-05-31 # quality + coverage + staleness
How it works (per root, per trading day):
- ATM from prev-day close — the ATM strike and the
ATM, OTM1-4, ITM1-4ladder (CE+PE) are derived from the previous working day's spot EOD close (scr_nseeq_eod), reusing the existing_ladder/_moneynesshelpers. - Current-day spot OHLC — read from
scr_nseeq_eodand shown in each day's header (Spot O/H/L/C ... prevClose ...). - Two daily OHL snapshots per option contract:
- EOD —
sopen/shigh/slowfromscr_nsefo_eod(_eod_option_ohlc). - GDFL — the 1-minute Parquet bars compressed to a daily bar
(
Open= first IST bar'ssopen,High=max(shigh),Low=min(slow)) via_month_option_ohlc.
- EOD —
- Compare Open/High/Low only — Close is intentionally skipped (NSE EOD close
= average of the last 30 min, not the LTP; the 1-minute series gives an
LTP-style close). Exact match required:
OHLC_TOL = 0.0, with both sides rounded to 2 decimals (paise) so the equality is immune to float noise (NSE option prices are multiples of 0.05, so rounding is lossless). - Status per contract —
{strike}{CE/PE} {moneyness}followed by:OK— present in both, O/H/L all match.High/Low/Open Not matching (...)— present in both, with the differing field(s) and both values.MISSING from GDFL (traded per EOD)— EOD row exists, no 1-minute bars.EXTRA in GDFL (not in EOD)— 1-minute bars exist, no EOD row.- Strikes absent from both datasets (deep OTM/ITM never traded) are ignored.
Two outputs:
- Console — reading (tall) layout: grouped
Date → Script → Expiry, one line per contract (all live expiries), with a one-line OK / Mismatch / MissingFromGDFL / ExtraInGDFL summary. Tee'd toConfig.CSV_PATH/audit_{from}_{to}_{ts}.txt. - Wide CSV —
BNFStrikes.csvlayout (the project leader's reference file): one row per trading day for the nearest (front) expiry only, written toConfig.CSV_PATH/{SCRIPT}_strikes_{from}_{to}_{ts}.csv. ColumnsDate, Prev.eod, Expiry, ATM(CE), ATM(PE), OTM1(CE), OTM1(PE), ITM1..., OTM4(CE), OTM4(PE), ITM4(CE), ITM4(PE); each cell is{strike}{CE|PE}-{token}where token ∈OK(O/H/L all match),NotOK(any of O/H/L differs),MISS(EOD but no GDFL),EXTRA(GDFL but no EOD). The file is prefixed with aScript,{name}header line to mirror the reference exactly.
Verified the wide CSV reproduces BNFStrikes.csv cell-for-cell (same columns,
1-Jun-16 date format, front-expiry selection, and strike-per-slot mapping). The
reference's NotOK cells are illustrative placeholders — in the actual Parquet
store the GDFL daily O/H/L matches scr_nsefo_eod (e.g. 2016-06-01 17700CE:
both O/H/L = 72.7/72.7/5.7 over a full 376-bar session → OK).
The 2016 weekly/monthly rule needs no special-casing: NIFTY simply has no
weekly expiries in scr_master for that period, so its ladder only ever resolves
monthly ([M]) expiries, while BANKNIFTY resolves both [W] and [M].
Status: Implemented in backend/nseieod/audit.py. Verified on the ingested
window — BANKNIFTY 2016-06-01 → 2016-06-07: 237 contract-days, all OK (GDFL
daily O/H/L matches scr_nsefo_eod exactly); NIFTY 2016-06 resolves monthly
expiries only; BANKNIFTY 2018-03 (EOD present, no Parquet yet) correctly reports
every ladder contract as MISSING from GDFL.
| Item | Status |
|---|---|
quality_check() + --quality-only CLI flag | Done |
| ATM/OTM1-4/ITM1-4 (CE+PE) ladder from prev-day close | Done |
Current-day spot OHLC from scr_nseeq_eod in header | Done |
| GDFL 1-min → daily O/H/L compression | Done |
EOD O/H/L from scr_nsefo_eod | Done |
| Compare Open/High/Low only (Close skipped by design) | Done |
| MISSING-from-GDFL / EXTRA-in-GDFL reporting | Done |
| Tall "reading" layout (Date → Script → Expiry) + summary | Done |
Wide CSV in BNFStrikes.csv layout (front expiry, per script) | Done |
| 2016 NIFTY monthly-only / BANKNIFTY weekly+monthly | Done (intrinsic) |
Versioninfo
good write a wrapper function that pack(a year or from date and todate ) in nseieod.py
That imports from Extracted .csv file in CSV Path
By Knowing below knowledge
Knowledge
Version=1 from JUN-2016 to 15-may-2020 fo_prefix=GFDLNFO_NIFTY_BNK_OPT_ index_prefix = "" q_prefix = ""
Version =2 from 16 May 2020 to Jan 03 2021
fo_prefix="GFDLNFO_CONTRACT_" index_prefix = "" eq_prefix = ""
Version =3 from Jan 04 2021 to current date fo_prefix="GFDLNFO_CONTRACT_" index_prefix = "GFDLCM_INDICES_" eq_prefix = "GFDLCM_STOCK_"
Pls Note Before Jan 04 2021 Index Spot Files and Cfut One Minute Data GDFL did not provided we have import from other source(only Index and future Data)
from Jan 04 2021 GDFL Provide all data
Important from this data we Have calculate Continous Future data from GDFL Data
Pls add This knowledge in claude.md also
Implementation
Added GDFL_VERSIONS (the table above, as data) and a module-level wrapper
pack_gdfl_range(year=None, fromdate=None, todate=None, datapath=None) to
backend/nseieod/nseIEOD.py, right after the IEod class.
What it does:
- Resolves the requested range from either
year=orfromdate=/todate=. - Splits it at the GDFL version boundaries (
2020-05-15/16,2021-01-03/04). - Within each version segment, splits further by calendar year (extracted
CSVs live under
{Config.CSV_PATH}/{year}/, pergdfl/extract.py). - For each
(version, year)chunk, constructs a freshIEodwith the correctfo_prefix/versionfor that period and calls the existingIEod.pack()(CSV → Parquet directly, noscr_nsefo/scr_nseeqstaging). - A version-3 segment (which also has Index/EQ GDFL files) prints a note
instead of silently skipping them, since
update_index_file()/update_eq_file()are still stubs — only FO import is wired end-to-end.
from nseieod.nseIEOD import pack_gdfl_range
pack_gdfl_range(year=2017)
pack_gdfl_range(fromdate=date(2020, 1, 1), todate=date(2021, 6, 30)) # spans v2 -> v3
Verified: pack_gdfl_range(fromdate=2017-01-02, todate=2017-01-03) correctly
auto-selected version=1, fo_prefix=GFDLNFO_NIFTY_BNK_OPT_; a range spanning
2020-05-10 → 2020-05-20 correctly split into 2 segments at the v1/v2 boundary
(2020-05-15/16), each with the right prefix.
The version/prefix knowledge table was added to nseieod/CLAUDE.md as rule 9,
and rule 8's GDFL coverage-breakup table was corrected to the precise dates
given here (it previously said the EQ-onward period started Mar 2022; the
correct date is 4 Jan 2021).
| Item | Status |
|---|---|
GDFL_VERSIONS table (version/fo_prefix/index_prefix/eq_prefix per period) | Done |
pack_gdfl_range() wrapper — auto version/prefix resolution | Done |
| Splits range at version boundaries + calendar-year boundaries | Done |
Knowledge added to nseieod/CLAUDE.md (rule 9) | Done |
| Rule 8 coverage table corrected (Mar 2022 → 4 Jan 2021) | Done |
| Index/EQ import for version 3 | Pending (update_index_file/update_eq_file still stubs) |
VersionUpdate
Knowledge
Version=1 from JUN-2016 to 15-may-2020 fo_prefix=GFDLNFO_NIFTY_BNK_OPT_ index_prefix = "" q_prefix = ""
Version =2 from 16 May 2020 to Feb 28 2022
fo_prefix="GFDLNFO_CONTRACT_" index_prefix = "" eq_prefix = ""
Version =3 from Mar 01 2022 to current date fo_prefix="GFDLNFO_CONTRACT_" index_prefix = "GFDLCM_INDICES_" eq_prefix = "GFDLCM_STOCK_"
Pls Note Before Mar 01 2022 Index Spot Files and Cfut One Minute Data GDFL did not provided we have import from other source(only Index and future Data)
from Mar 01 2022 GDFL Provide all data
Important from this data we Have calculate Continous Future data from GDFL Data
Pls add This knowledge in claude.md also
Implementation
Corrected GDFL_VERSIONS in backend/nseieod/nseIEOD.py — the v2/v3 boundary
moved from 2021-01-03/04 to 2022-02-28/03-01, per this updated knowledge:
version | Date range | fo_prefix | index_prefix | eq_prefix |
|---|---|---|---|---|
| 1 | 2016-06-01 → 2020-05-14 | GFDLNFO_NIFTY_BNK_OPT_ | (none) | (none) |
| 2 | 2020-05-15 → 2022-02-28 | GFDLNFO_CONTRACT_ | (none) | (none) |
| 3 | 2022-03-01 → current | GFDLNFO_CONTRACT_ | GFDLCM_INDICES_ | GFDLCM_STOCK_ |
Verified against disk: csvs/2021/ has zero Index/Equity files (only
GFDLNFO_CONTRACT_, ~193 files for the whole year) — confirming GDFL
genuinely didn't provide Index/Equity until Mar 2022, matching this correction
exactly. Re-running scratch/find_missing_gdfl_csv.py (no code changes needed
-- it reads GDFL_VERSIONS directly) dropped its missing-file count for
2021-2024 from 810 to 236, eliminating the false "Index/Equity missing in all
of 2021" noise the earlier (incorrect) boundary produced.
nseieod/CLAUDE.md rules 8 and 9 updated to match (also caught and fixed a
stale v1/v2 boundary in rule 9's table that an earlier edit had missed).
| Item | Status |
|---|---|
GDFL_VERSIONS v2/v3 boundary corrected (2021-01 → 2022-03) | Done |
nseieod/CLAUDE.md rules 8 + 9 updated | Done |
find_missing_gdfl_csv.py re-verified against corrected boundary | Done (no code change needed) |
Continues futures
Good Now I want convert Index and Stock Future in to Continues Future Format
for Example Nifty Continues futures Name with suffix "-" and Roman Letter of month index it will have continues future for update 3 months
Like NIFTY-I NIFTY-II NIFTY-III
you should write function (or class) in nseieod.py which accepts (fromdate (optional) and todate(optional) which copies date from futures with expiry date to continues future format if from date omitted from date would next working date of lastly updated continues future (with month index) if todate id omitted then it is last trading date of nse
Steps to convert
select distinct underly from scr_master where (typ=4 or typ=3) and monthindex=0 order by underly;
this give Underly list for C future 's Underlying
begin loop in this data Frame
Step-2 (Under Loop Level1) select masid,edate,underly,mname from scr_master where (typ=4 or typ=3) and monthindex=0 order by underly,edate; This data Frame masid of each future contracts
you can filter this data frame with Underline also edate should greater or equal to fromdate
Step-3
(Under Loop Level 2)
Once Underline and Edate filtered and sorted Edate
Select top 3 Expiry Dates along with their Masid(Set 1)
then select masid,edate,underly,mname from scr_master where monthindex>0 and monthindex<=3 and underly={Underly} order by monthindex (set 2)
1.)Now open data (from parquet file ) of masid from set 1 (first record) and copy data to masid and month index =1 and save to parquet file 2.)open data (from parquet file ) of masid from set 1 (second record) and copy data to masid and month index =2 and save to parquet file 3.)open data (from parquet file ) of masid from set 1 (third record) and copy data to masid and month index =3 and save to parquet file
do this process un till Trading date lower or equal to Expiry date (of First record)
Once Trading date crosses Expirydate Choose Next 3 Records from (set 1)
then continue the process till todate to complete underlying data
Step -4
then to same process for other Underlying (remaing)
Since it is lengthy process set some parameter that stops the process to verify the conversion
Implementation
Added build_cfut(fromdate, todate, underlyings, max_days) to
backend/nseieod/nseIEOD.py as a module-level function (same style as
pack_gdfl_range).
Run from backend/:
# Verify first 5 days for NIFTY+BANKNIFTY only
python -c "from nseieod.nseIEOD import build_cfut; build_cfut(fromdate='2022-03-01', todate='2022-03-31', max_days=5)"
# Full backfill for all underlyings
python -c "from nseieod.nseIEOD import build_cfut; build_cfut(fromdate='2022-03-01')"
# Resume (fromdate omitted -- auto-detects last CFut bar date)
python -c "from nseieod.nseIEOD import build_cfut; build_cfut()"
# One underlying
python -c "from nseieod.nseIEOD import build_cfut; build_cfut(fromdate='2022-03-01', underlyings=[1])"
How it works:
-
Step 1 —
SELECT DISTINCT underly FROM scr_master WHERE (typ=4 OR typ=3) AND monthindex=0gives all underlyings with real futures. Filtered byunderlyings=param if set. -
Step 2 — For each underlying, fetch all real contracts ordered by
edate, and the CFut slots (monthindex=1/2/3) fromscr_master. -
fromdate auto-detect — if
fromdate=None, scans Parquet files for the latest bar date already written for the near-month CFut slot (monthindex=1), then starts from the next NSE working day. Falls back to earliest contract edate if no data yet. -
Step 3 (day loop) — for each trading day:
- Picks top 3 contracts with
edate >= trade_date(near / 2nd / 3rd month) - Reads the Parquet shard for that day once (
{underly}_{IST_MONTH}) - For each of the 3 slots: filters bars for
real_masid, replacesmasidwithcfut_masid, writes back via merge-write (deduplicates on(masid, sdate)) - When
trade_datecrosses the near-month expiry, the next iteration naturally picks the next set of 3 contracts (no explicit roll logic needed)
- Picks top 3 contracts with
-
max_days — stops after N trading days per underlying for verification. Progress printed every 20 days.
Parquet layout (unchanged):
CFut bars land in the same monthly shard as the real contracts
({data_path}/{L}/{name}/{year}/{underly}_{MONTH_NAME}) but carry the
CFut masid (e.g. 3 for NIFTY-I). Con_Price_Getter already reads these
by filtering on masid, so CFut retrieval requires no changes.
| Item | Status |
|---|---|
build_cfut() in backend/nseieod/nseIEOD.py | Done |
| Auto fromdate from last CFut bar | Done |
| 3-slot rolling window (near/2nd/3rd month) | Done |
| All underlyings or filtered by list | Done |
max_days verification stop | Done |
| Parquet merge-write (dedup on masid+sdate) | Done |
EodIEodsetup
Good from JUN 2026 we changed Data Vendor from GDFL to EODIEOD
Since it's format totally different from GDFL CSV format and we already has code for converting data from GDFL format to parquet basket Instead writing fresh code to convert for EODIEOD format we will convert data od EODIeod to GDFL format
You need to write a script that convert EodIEOD Data from GDFL format under folder \backend\nseieod\eodieod
let me to share What I observed from it's CSV
before that we have two stages of implementation 1.) convert whole June 2026 month to GDFL format 2.) Convert daily eodieod file (csv)
****** IMPORTANT BEFORE CONVERTING IT CHECK scr_master and scr_master,scr_nseeq_eod and scr_nsefo_eod TO ENSURE EODDATA DOWNLOADED IF IT NOT DOWNLOADED IT ASK USER TO DOWNLOAD IT WITH THAT IT SHOULD NOT PROCEED********* (add this in claude.md as a rule)
let look 4 major files
All these 4 files comes with as .rar format
1.) a.)NSE_Indices-2606.rar(June 2026) b.) NSE Indices IEOD - 10-07-2026.rar(daily sync) 2.) a.)IEOD_Option-2606.rar(June 2026) b.) Option_IEOD - 10-07-2026.rar(daily sync) 3.) a.)Cash_IEOD_Gold-2606.rar(June 2026) b.)Cash_IEOD - 10-07-2026.rar(daily sync) 4.) a.)IEOD_Future_Gold-2606.rar(June 2026) b.)Future_IEOD - 10-07-2026.rar(daily sync)
here after we assume (June 2026 ) files have all June 2026 Data and Files like (having date Like 10-07-2026 DD-MM-YYYY) have one minute data for particular day
Let use zoom in each files 1.) NSE_Indices file under folder we have many files each contains one minutes data for the period but names are different so we have convert the data using naming convention table
I have compiled a file in "D:\kss\stock\tradeentry-VPS\csvs\EODIEOD2026\eodieod_index_name_mapping.csv" Pls ingest this in namechanges table in duckdb (one time setup)
by using this namechanges table we can combine all these file in to single GDFL format file (Indices)
2.) Next We look at IEOD_Option it contains one minute data for an options contract
here is format
NSESymbol+YYMMMM(expiry)+Strike+OPtTyp(CE or PE)
since file name gives all information it is easy to combine all in one file
While converting this you should check whether it creates any shadow Expiry data.. for this Purpose only I have added rule that EOD records must be updated before converting EODIEOD Data
Though we can Allow Create any missing records in scr_master you should tell where are records going to inserted after the conversion
Also need to check whether any shadow Expiry Date(that should be avoided) 3.) Next EODIEOD Eq files(Cash_IEOD*.rar) When converting it should omit the records not in scr_nseeq_eod table
other is wise it straight forward
4.) Now it time to look FUT files it Crucial Part of conversion
In GDFL We have Generated Continues Future format
But in EODIEOD Provides only C-futures Data from which we have create normal Future Data (after checking namechanges)
also NIFTY_F1,NIFTY_F2,NIFTY_F3 should converted to NIFTY-I,NIFTY-II,NIFTY-III
from this records you have create Records for Normal Future (Here Only Month Expiries considered Not any weekly Expiries)
Be Carefull on shadow Expires
5.) here short Todo List on priority basis
i.) Ingest D:\kss\stock\tradeentry-VPS\csvs\EODIEOD2026\eodieod_index_name_mapping.csv in Name changes Duck _db Table ii.)Then check all 4 types of files presented iii.) check EOD Data updates in conversion date range iv.) the proceed convert as per instruction given above v.) All converted CSV should shored in CSV_PATH under EODIEOD/converted folder vi.) Write a short Script than ingest converted CSV in parquet basket
Implementation
New package backend/nseieod/eodieod/ converts EODIEOD data into GDFL format
so the existing IEod packer and build_cfut() are reused unchanged. Key
discovery: EODIEOD rows are already GDFL-shaped (<ticker>,<date>,<time>,<open>, <high>,<low>,<close>,<volume>[,<OI>]), so conversion = rename tickers +
reformat date (MM/DD/YYYY -> DD/MM/YYYY) + combine per-instrument
files into GDFL per-day files.
Run from backend/:
python -m nseieod.eodieod.run setup # todo i (one-time)
python -m nseieod.eodieod.run check --month 2026-06 # prechecks only
python -m nseieod.eodieod.run convert --month 2026-06 # stage 1 (whole month)
python -m nseieod.eodieod.run convert --date 2026-07-10 # stage 2 (daily file)
python -m nseieod.eodieod.run ingest --month 2026-06 # pack -> Parquet
# then: build_cfut(fromdate='2026-06-01', todate='2026-06-30') -> NIFTY-I/II/III
Modules & mapping to the todo list:
| File | Responsibility |
|---|---|
setup.py (i) | Ingest eodieod_index_name_mapping.csv into name_changes(source='EODIEOD') (skips unresolved symbols) |
rario.py | Reads EODIEOD .rar files directly (WinRAR UnRAR.exe / 7-Zip; bulk-extract to temp, process, delete) |
precheck.py (ii, iii) | 4-.rar presence + tool check + mandatory EOD gate (refuses if scr_master/scr_nseeq_eod/scr_nsefo_eod don't cover every working day) |
convert.py (iv, v) | 4 converters -> GDFL day CSVs in CSV_PATH/EODIEOD/converted/, per-type progress bar, timing, full text report; shadow/new-contract report |
ingest.py (vi) | Pack converted CSVs -> Parquet via IEod.pack() per segment + direct CFut ingest from EODIEOD {SYM}_F1/F2/F3 |
run.py | CLI orchestrator (setup/check/convert/ingest, --month/--date, --dry-run) |
Conversion rules per type:
- Index —
{eodieod_symbol}.csv(e.g..NSEI) mapped vianame_changes(EODIEOD)to canonicalmname, emitted as{mname}.NSE_IDX(resolved by_resolve_index_masid). Unmapped symbols are dropped and reported. - Option —
{SYM}{YYMMDD}{STRIKE}{CE|PE}->{SYM}{DDMMMYY}{STRIKE}{CE|PE}(e.g.NIFTY30JUN2625000CE). Regex constrains YY/MM/DD so symbols ending in digits (NIFTYNXT50) aren't mis-split; strike may be fractional (CANBK...136.8CE). Reports new contracts (auto-inserted at pack) and possible shadow expiries (rule 11). - Cash/EQ —
{SYM}.csv->{SYM}.NSE; omits symbols not inscr_nseeq_eod. - Future —
{SYM}_F{1,2,3}continuous futures serve two outputs:convertemits real monthly contracts{SYM}{DDMMMYY}FUTby mapping each day's slot to the n-thscr_mastermonthly expiry (typ IN (3,4), index=typ3 / stock=typ4).ingestmaps the same_F1/F2/F3directly onto CFut masids (scr_master.monthindex1/2/3 -> NIFTY-I/II/III). Nobuild_cfut()— EODIEOD provides continuous futures natively (unlike GDFL), so reconstructing them would be redundant and could distort the series if the vendor's roll differs.build_cfutstays GDFL-era-only.
Reads from .rar directly: the 4 EODIEOD archives are placed under
CSV_PATH/EODIEOD2026/ (name must contain the stage tag) and read in place — no
manual extraction. Decimal-strike options and per-type console progress bars are
supported.
Verified: rar-based check_files (all 4 archives + UnRAR.exe); real
extraction of the Future .rar (641 files) + convert_futures (2.37M rows/9s);
decimal-strike round-trip through IEod.generatemnamever2
(CROMPTON28JUL26292.5PE -> masid match); CFut masid mapping
(NIFTY_F1/F2/F3 -> 3/3603/3604, BANKNIFTY -> 4/3598/4988) and _read_fut_df;
version=4 prefix defaults + backward-compat of version=1/3 unchanged.
Origin-tagged filenames (version=4): converted/packed files now use
EODIEOD* prefixes instead of GFDL* (e.g. EODIEODCM_INDICES_02062026.csv,
not GFDLCM_INDICES_02062026.csv), so the data source is visible on disk even
after packing. IEod.__init__ gained a version=4 profile (EODIEOD)
alongside the existing GDFL versions 1-3 — same ticker parsing as v3 (else: # version 3 in generatemnamever2 already covers any version != 2), only the
default fo_prefix/index_prefix/eq_prefix differ, driven by three new
nseIEOD.py constants (EODIEOD_FO_PREFIX/EODIEOD_INDEX_PREFIX/
EODIEOD_EQ_PREFIX) that convert.py's _Emitter and ingest.py's IEod()
construction both import — single source of truth, can't drift apart.
index_prefix/eq_prefix also became real constructor parameters (previously
hardcoded inside __init__, only overridable by poking the instance
attribute after construction, which ingest.py used to do). version=4 is
deliberately not added to GDFL_VERSIONS/pack_gdfl_range() — that
automation is a date-driven scan for real GDFL CSVs, unrelated to EODIEOD's
manually-invoked flow. Full detail:
docs/developer/packages/nseieod/nseieod-main.md.
Also noted per project leader: EODIEOD's actual roll-forward policy for
{SYM}_F1/F2/F3 (whether it switches to the next contract exactly on
expiry, like convert_futures's scr_master-expiry-based assumption, or a
few days early) cannot be verified for the June-2026 window — no
independent expired-contract reference data is available for that month.
Deferred to a spot-check once daily EODIEOD syncs are running against fresh
(not-yet-expired) contracts, where the roll can be observed directly instead
of inferred.
Status: Implemented in backend/nseieod/eodieod/ + nseieod/CLAUDE.md
rules 9 (cross-reference) and 12.
| Item | Status |
|---|---|
i. eodieod_index_name_mapping.csv -> name_changes(EODIEOD) (setup.py) | Done |
ii. 4 .rar presence + tool check (precheck.check_files) | Done |
iii. EOD-updated gate (precheck.check_eod, hard-stop) | Done |
| iv. 4 converters (Index/Option/Cash/Future) -> GDFL-shaped CSVs | Done |
v. Output to CSV_PATH/EODIEOD/converted/ + full text report | Done |
vi. Ingest converted CSV -> Parquet (ingest.py, IEod version=4) | Done |
Reads .rar directly (rario.py, no manual extraction) | Done |
| Decimal-strike options + per-type progress bars | Done |
Direct CFut ingest (F1/F2/F3 -> NIFTY-I/II/III; no build_cfut) | Done |
Origin-tagged filenames (EODIEOD* prefix, IEod version=4) | Done |
| Shadow-expiry / new-contract reporting (rule 11) | Done |
nseieod/CLAUDE.md rule 12 (EOD-before-convert) | Done |
EODIEOD converter
Good it is time write a single scripts that converts EODIEOF files then ingests in parquet bucket then fixdata.filldata for masid 1,2,3,4 and 987106 and report any big misses in to log to report Data vendor lastly give CLI
Implementation
New backend/nseieod/eodieod/pipeline.py — one call does convert → ingest →
fix_data.filldata() (masid 1, 2, 3, 4, 987106 by default = NIFTY, BANKNIFTY,
NIFTY-I, BANKNIFTY-I, India VIX — all already-registered liquid_scripts
roots) → a vendor-issue report of any "big miss".
Run from backend/:
python -m nseieod.eodieod.run pipeline --month 2026-06
python -m nseieod.eodieod.run pipeline --date 2026-07-10
python -m nseieod.eodieod.run pipeline --month 2026-06 --dry-run # stop after convert
python -m nseieod.eodieod.run pipeline --month 2026-06 --masids 1,2 # override the masid list
"Big miss" definition — reused the project's own existing threshold rather
than inventing a new one: a whole trading day with zero bars, or an intraday
gap of more than 20 consecutive missing bars (fill flag < -3, nseieod/ CLAUDE.md rule 7 — "low-confidence, should be replaced with another vendor's
data"). filldata(apply=True) already patches these with a synthetic
previous-day-pattern fill so the store isn't literally empty, but a gap that
size means EODIEOD itself failed to deliver real data for that stretch — that
is what gets escalated to the vendor-issue report, separate from the small,
self-healed gaps (1-bar carry, 2-5 bar interpolation, 6-20 bar pattern) that
don't need reporting anywhere.
Two output files per run, both under CSV_PATH/EODIEOD/converted/:
pipeline_log_{tag}_{timestamp}.txt— the full run's console output (every stage, every masid's detailedfix_datadashboard), viafix_data. _tee_stdout.vendor_report_{tag}_{timestamp}.txt— just the filtered "big miss" rows (masid, date, time range, bar count, fill note), formatted for handing directly to the data vendor.
Verified: dry-run against the real .rar files (EOD gate pass → convert
all 4 types, 0 unparsed, 0 shadow expiries, correctly stops before ingest);
filldata() for all 5 default masids against already-ingested, already-clean
June 2026 data (0 clusters each, matching the earlier standalone fix_data. check result); the report-writing logic itself with synthetic big-miss data
(a 30-bar gap and a whole-missing day both correctly flagged and formatted; a
5-bar gap — below the 20-bar threshold — correctly excluded).
Fixed one bug surfaced by wiring convert.py's _Progress bar under fix_data ._tee_stdout: it assumed sys.stdout.isatty() always exists, which crashed
under the tee wrapper (no isatty attribute). Now defensive: getattr(sys. stdout, "isatty", lambda: False)().
| Item | Status |
|---|---|
Single script: convert → ingest → filldata(masid list) (pipeline.py) | Done |
| Default masid list 1,2,3,4,987106 (India VIX confirmed already registered) | Done |
Big-miss definition reuses rule 7's filled < -3 threshold | Done |
| Vendor-issue report file (filtered, big misses only) | Done |
| Full run log file (tee'd console output) | Done |
CLI (run.py pipeline subcommand, --dry-run/--masids) | Done |
_Progress.isatty() robustness fix (needed for the tee wrapper) | Done |
Correction — purpose is DAILY update; sequential guard + update CLI
Follow-up from the project leader: the pipeline's actual purpose is a daily update run once EODIEOD's sync for that date is ready, not an arbitrary-date tool — it must refuse to convert any date while an earlier one is still unfilled, and must verify all 4 source files are present.
What was found: check_files()'s 4-file check was already correct in
isolation. The real gap was that nothing stopped a later, available date
from being processed while an earlier date had no source .rar at all —
confirmed for real, not hypothetical: csvs/EODIEOD2026/ has daily files for
01-07-2026 and 10-07-2026 but none for 02–09-07-2026.
convert --date 10-07-2026 would have silently stranded that gap forever.
Fix:
- New sequential guard (
pipeline.check_sequential()) — refuses unless the requested date is exactly the next pending NSE working day after the checkpoint.pipeline --date/--monthnow enforce it by default (--no-sequentialto override for a deliberate backfill). - New
pipeline.run_daily_update()/ CLIrun.py update(no date args) — the actual daily-update entry point. Auto-resolves the checkpoint, processes pending working days one at a time, stops cleanly (not an error) the moment a day's 4 files aren't present yet. - Bug found and fixed along the way: the checkpoint mechanism
(
fix_data._record_checked_till()) calledupdate_checked_till(root=root, ...), but that function's parameter is actually namedmasid— every call raisedTypeError, silently swallowed by a bareexcept: pass.checked_tillhad never been successfully recorded. Fixed (masid=root); all 3 anchor roots (NIFTY, BANKNIFTY, India VIX) manually baselined to2026-06-30afterward, since June was already independently verified complete multiple times.
Run for July (project leader will run this themselves):
cd backend
python -m nseieod.eodieod.run update
No --date/--month needed — it will process 2026-07-01 (files present),
then correctly stop at 2026-07-02 reporting it as the next pending date
with no .rar files found yet.
Verified (read-only, no real conversion executed):
get_checkpoint()→2026-06-30;next_pending_date()→2026-07-01check_sequential(2026-07-10)→ refuses, correctly citing2026-07-01as the actual next pending datecheck_sequential(2026-07-01)→ allowscheck_files()independently confirmed:01-07-2026and10-07-2026OK,02-07-2026correctly flagged as fully missing (all 4 types)
| Item | Status |
|---|---|
Sequential guard (check_sequential, get_checkpoint, next_pending_date) | Done |
pipeline --date/--month enforce it by default (--no-sequential override) | Done |
run_daily_update() + CLI run.py update (no date args, auto-catchup) | Done |
_record_checked_till() keyword-argument bug (root= → masid=) | Done |
Anchor roots (1, 2, 987106) baselined to 2026-06-30 | Done |
Verified against real July gap (02–09-07-2026 missing) — guard blocks correctly | Done |
| Live July run | Not run — reserved for the project leader |
Structured logging — loguru, no Sentry, no text-based reports
Follow-up: TradeEntry is growing into an algo backtesting + execution +
accounts-management platform with no real log management (just ad hoc .txt
report files). Discussed the options (Sentry, structlog vs loguru, self-hosted
vs SaaS, VPS-crash-specific logging) — landed on loguru only, lightweight,
no Sentry for this stage of the project.
Added: Config.LOG_PATH (LOCAL_LOG_PATH/VPS_LOG_PATH, defaults to
backend/logs/) and backend/app/logging_config.py — one shared logger
(loguru), console + daily-rotated JSON-lines file (30-day retention,
enqueue=True for the parallel Parquet writes from rule 10a). See
backend/CLAUDE.md rule 3.
Then removed every standalone text-report file (project leader's call —
console print()s stay, but no more .txt artifacts):
pipeline.py'svendor_report_{tag}_{ts}.txtandpipeline_log_{tag}_{ts}.txt(thefix_data._tee_stdout-based full-run tee)convert.py'sconvert_report_{tag}_{ts}.txt(_write_report_file)fix_data.py's CLIfix_data_{check,filldata}_*.txt(_tee_stdout/_Tee/_report_path, all now dead code, removed)
Every category those files used to hold is now a structured log event instead
(same information, queryable instead of a flat file):
vendor_data_gap (per gap, includes the fill-flag note), eodieod_convert_complete
- one event per non-empty category (
cash_symbols_omitted,index_symbols_dropped,option_filenames_unparsed,option_new_expiries,option_possible_shadow_expiries, ...),fix_data_scan_gaps_found+ per-masidfix_data_masid_gaps,fix_data_intraday_fill,fix_data_whole_day_restore,fix_data_could_not_restore.
Verified: fix_data.check() against already-clean June 2026 data — no new
.txt file created, correct fix_data_scan_no_gaps event logged;
_log_convert_report/vendor-gap logging tested with synthetic data (all
categories fire at the right log level, note field correctly carries the
fill-flag annotation that used to be in the txt report).
| Item | Status |
|---|---|
Config.LOG_PATH + app/logging_config.py (loguru, console + JSON-lines, no Sentry) | Done |
Removed pipeline.py text reports (vendor + full-run log) | Done |
Removed convert.py text report (_write_report_file) | Done |
Removed fix_data.py CLI text reports (_tee_stdout/_Tee/_report_path) | Done |
| Structured log events replacing every removed report's content | Done |
.gitignore updated for backend/logs//*.jsonl | Done |