Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -36,30 +36,61 @@ SYSTEM_STATUS = {
|
|
| 36 |
}
|
| 37 |
UPDATE_LOCK = threading.Lock()
|
| 38 |
|
| 39 |
-
#
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
"
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
"
|
| 47 |
-
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
def get_root_symbol(desc: str, symbol: str) -> str:
|
| 51 |
-
"""Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
|
| 52 |
try:
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def update_master_db():
|
| 62 |
-
global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
|
| 63 |
|
| 64 |
if not UPDATE_LOCK.acquire(blocking=False):
|
| 65 |
return
|
|
@@ -70,6 +101,10 @@ def update_master_db():
|
|
| 70 |
|
| 71 |
headers = { "User-Agent": "Mozilla/5.0" }
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
# 1. NSE F&O
|
| 74 |
url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
|
| 75 |
df_nse = pd.read_csv(
|
|
@@ -105,25 +140,95 @@ def update_master_db():
|
|
| 105 |
|
| 106 |
temp_db = {}
|
| 107 |
|
|
|
|
| 108 |
for _, row in df.iterrows():
|
| 109 |
-
sym = row['Symbol']
|
| 110 |
-
desc = row['Desc']
|
| 111 |
exch = row['Exch']
|
| 112 |
|
| 113 |
-
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
try:
|
| 129 |
exp = int(row['Expiry'])
|
|
@@ -145,24 +250,29 @@ def update_master_db():
|
|
| 145 |
except:
|
| 146 |
pass
|
| 147 |
|
| 148 |
-
temp_db[
|
| 149 |
"s": sym, "e": exp, "k": strike, "t": opt_type
|
| 150 |
})
|
| 151 |
|
| 152 |
final_db = {}
|
| 153 |
search_list = []
|
| 154 |
|
| 155 |
-
for
|
|
|
|
|
|
|
|
|
|
| 156 |
sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
|
| 157 |
-
final_db[
|
| 158 |
"spot": data["spot"],
|
| 159 |
"exch": data["exch"],
|
| 160 |
"items": sorted_items
|
| 161 |
}
|
|
|
|
|
|
|
| 162 |
search_list.append({
|
| 163 |
-
"root":
|
| 164 |
"exch": data["exch"],
|
| 165 |
-
"display":
|
| 166 |
})
|
| 167 |
|
| 168 |
MASTER_DB = final_db
|
|
@@ -259,17 +369,21 @@ def search_symbol(q: str = Query(..., min_length=1)):
|
|
| 259 |
return JSONResponse([])
|
| 260 |
|
| 261 |
query = q.upper()
|
| 262 |
-
|
| 263 |
-
if
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
| 266 |
return JSONResponse(results[:20])
|
| 267 |
|
| 268 |
@app.get("/api/chain")
|
| 269 |
def get_chain(symbol: str):
|
| 270 |
-
root = symbol.upper()
|
| 271 |
if root in MASTER_DB:
|
| 272 |
return JSONResponse(MASTER_DB[root])
|
|
|
|
|
|
|
| 273 |
return JSONResponse({"error": "Symbol not found"}, status_code=404)
|
| 274 |
|
| 275 |
@app.get("/api/refresh")
|
|
@@ -725,7 +839,7 @@ HTML_TEMPLATE = """
|
|
| 725 |
const status = await res.json();
|
| 726 |
if(status.ready) {
|
| 727 |
document.getElementById('loader').style.display = 'none';
|
| 728 |
-
if(!CURRENT_ROOT) loadSymbol('NIFTY');
|
| 729 |
} else {
|
| 730 |
document.getElementById('loaderMsg').innerText = status.message.toUpperCase();
|
| 731 |
setTimeout(checkStatus, 2000);
|
|
@@ -764,7 +878,9 @@ HTML_TEMPLATE = """
|
|
| 764 |
document.getElementById('loader').style.display = 'flex';
|
| 765 |
|
| 766 |
try {
|
| 767 |
-
|
|
|
|
|
|
|
| 768 |
if(!res.ok) throw new Error("Symbol not found");
|
| 769 |
const data = await res.json();
|
| 770 |
|
|
@@ -775,9 +891,13 @@ HTML_TEMPLATE = """
|
|
| 775 |
|
| 776 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 777 |
|
| 778 |
-
if(SPOT_SYM
|
|
|
|
|
|
|
|
|
|
| 779 |
|
| 780 |
-
|
|
|
|
| 781 |
|
| 782 |
setupExpiries();
|
| 783 |
document.getElementById('loader').style.display = 'none';
|
|
@@ -863,7 +983,11 @@ HTML_TEMPLATE = """
|
|
| 863 |
IS_POLLING = true;
|
| 864 |
|
| 865 |
try {
|
| 866 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 868 |
|
| 869 |
if(kList.length > 0) {
|
|
|
|
| 36 |
}
|
| 37 |
UPDATE_LOCK = threading.Lock()
|
| 38 |
|
| 39 |
+
# Global Cache for Indices/Stocks Spot Mapping
|
| 40 |
+
SPOT_MAPPING = {}
|
| 41 |
+
|
| 42 |
+
def load_spot_mapping(headers):
|
| 43 |
+
"""
|
| 44 |
+
Downloads Capital Market (CM) files to map Descriptions to exact Spot Symbols.
|
| 45 |
+
This replaces hardcoded INDEX_MAP.
|
| 46 |
+
"""
|
| 47 |
+
global SPOT_MAPPING
|
| 48 |
+
temp_map = {}
|
| 49 |
+
|
|
|
|
|
|
|
| 50 |
try:
|
| 51 |
+
# 1. NSE CM (Indices & Equities)
|
| 52 |
+
# We need this to find 'NSE:NIFTY50-INDEX' from description 'Nifty 50'
|
| 53 |
+
url_nse = "https://public.fyers.in/sym_details/NSE_CM.csv"
|
| 54 |
+
df_nse = pd.read_csv(
|
| 55 |
+
url_nse, usecols=[1, 9, 13],
|
| 56 |
+
names=['Desc', 'Symbol', 'Inst'],
|
| 57 |
+
header=0, on_bad_lines='skip', storage_options=headers
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# 2. BSE CM (Sensex etc)
|
| 61 |
+
url_bse = "https://public.fyers.in/sym_details/BSE_CM.csv"
|
| 62 |
+
df_bse = pd.read_csv(
|
| 63 |
+
url_bse, usecols=[1, 9, 13],
|
| 64 |
+
names=['Desc', 'Symbol', 'Inst'],
|
| 65 |
+
header=0, on_bad_lines='skip', storage_options=headers
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# Combine and Process
|
| 69 |
+
df_all = pd.concat([df_nse, df_bse], ignore_index=True)
|
| 70 |
+
|
| 71 |
+
for _, row in df_all.iterrows():
|
| 72 |
+
desc = str(row['Desc']).strip().upper()
|
| 73 |
+
sym = str(row['Symbol']).strip()
|
| 74 |
+
|
| 75 |
+
# Key Logic: Map the Description to the Symbol
|
| 76 |
+
# This handles 'NIFTY 50' -> 'NSE:NIFTY50-INDEX'
|
| 77 |
+
# And 'RELIANCE INDUSTRIES LTD' -> 'NSE:RELIANCE-EQ'
|
| 78 |
+
if desc not in temp_map:
|
| 79 |
+
temp_map[desc] = sym
|
| 80 |
+
|
| 81 |
+
# Also map the short cleaning name for Indices (e.g., "NIFTY 50")
|
| 82 |
+
# Fyers FO Desc often matches the start of CM Desc
|
| 83 |
+
if "-INDEX" in sym:
|
| 84 |
+
temp_map[desc] = sym
|
| 85 |
+
|
| 86 |
+
SPOT_MAPPING = temp_map
|
| 87 |
+
print(f">>> [BG TASK] Spot Mapping Loaded: {len(SPOT_MAPPING)} symbols")
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Error loading CM files: {e}")
|
| 91 |
|
| 92 |
def update_master_db():
|
| 93 |
+
global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX, SPOT_MAPPING
|
| 94 |
|
| 95 |
if not UPDATE_LOCK.acquire(blocking=False):
|
| 96 |
return
|
|
|
|
| 101 |
|
| 102 |
headers = { "User-Agent": "Mozilla/5.0" }
|
| 103 |
|
| 104 |
+
# Phase 1: Load Spot/Index Definitions (CM Files)
|
| 105 |
+
load_spot_mapping(headers)
|
| 106 |
+
|
| 107 |
+
# Phase 2: Load Derivatives (FO Files)
|
| 108 |
# 1. NSE F&O
|
| 109 |
url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
|
| 110 |
df_nse = pd.read_csv(
|
|
|
|
| 140 |
|
| 141 |
temp_db = {}
|
| 142 |
|
| 143 |
+
# Iterate over derivatives to group them
|
| 144 |
for _, row in df.iterrows():
|
| 145 |
+
sym = row['Symbol'] # e.g., NSE:NIFTY24JANFUT
|
| 146 |
+
desc = row['Desc'] # e.g., NIFTY 50 24 JAN FUT
|
| 147 |
exch = row['Exch']
|
| 148 |
|
| 149 |
+
# --- LOGIC TO FIND ROOT EXACTLY AS IT IS ---
|
| 150 |
|
| 151 |
+
# 1. Try to find underlying based on Description
|
| 152 |
+
# Fyers FO Desc: "NIFTY 50 24 JAN..."
|
| 153 |
+
# We check if "NIFTY 50" exists in our SPOT_MAPPING
|
| 154 |
+
root_spot = ""
|
| 155 |
+
desc_upper = desc.strip().upper()
|
| 156 |
+
|
| 157 |
+
# Fast Check: Check common index names first or substring match
|
| 158 |
+
# This dynamic lookup replaces the INDEX_MAP
|
| 159 |
+
matched_spot = None
|
| 160 |
+
|
| 161 |
+
# Optimization: Check valid index names
|
| 162 |
+
# Extract potential name parts (e.g. "NIFTY 50" from "NIFTY 50 24...")
|
| 163 |
+
desc_parts = desc_upper.split(' ')
|
| 164 |
+
|
| 165 |
+
# Try 1 word, 2 words, 3 words combinations to find match in SPOT_MAPPING
|
| 166 |
+
# e.g. "NIFTY", "NIFTY 50"
|
| 167 |
+
for i in range(1, 4):
|
| 168 |
+
if i > len(desc_parts): break
|
| 169 |
+
check_name = " ".join(desc_parts[:i])
|
| 170 |
+
if check_name in SPOT_MAPPING:
|
| 171 |
+
matched_spot = SPOT_MAPPING[check_name]
|
| 172 |
+
# Prefer Indices over Equities for similar names if needed,
|
| 173 |
+
# but usually unique.
|
| 174 |
+
if "-INDEX" in matched_spot:
|
| 175 |
+
break
|
| 176 |
+
|
| 177 |
+
if matched_spot:
|
| 178 |
+
root_spot = matched_spot
|
| 179 |
+
else:
|
| 180 |
+
# Fallback for Stocks (RELIANCE, HDFCBANK)
|
| 181 |
+
# If no CM description match (rare), construct standard Equity symbol
|
| 182 |
+
# e.g. NSE:RELIANCE24... -> NSE:RELIANCE-EQ
|
| 183 |
+
try:
|
| 184 |
+
# Strip Exchange
|
| 185 |
+
parts = sym.split(':')
|
| 186 |
+
raw_sym = parts[1] if len(parts) > 1 else sym
|
| 187 |
|
| 188 |
+
# Heuristic: Extract alpha prefix before numbers/dates
|
| 189 |
+
# Simple regex-like logic manually
|
| 190 |
+
import re
|
| 191 |
+
# Match standard FO format: SYMBOL + DD + MON...
|
| 192 |
+
# We just take the non-digit prefix for most stocks
|
| 193 |
+
m = re.match(r"([A-Z\-\&]+)", raw_sym)
|
| 194 |
+
if m:
|
| 195 |
+
short_name = m.group(1)
|
| 196 |
+
if exch == "NSE":
|
| 197 |
+
root_spot = f"NSE:{short_name}-EQ"
|
| 198 |
+
elif exch == "BSE":
|
| 199 |
+
root_spot = f"BSE:{short_name}"
|
| 200 |
+
elif exch == "MCX":
|
| 201 |
+
# MCX often uses the Future as reference, leave blank to let client decide
|
| 202 |
+
root_spot = ""
|
| 203 |
+
except:
|
| 204 |
+
root_spot = sym # Fallback
|
| 205 |
+
|
| 206 |
+
# If we found a valid root spot (e.g. NSE:NIFTY50-INDEX), use that as the Key
|
| 207 |
+
# This ensures the Search returns the EXACT Instrument ID.
|
| 208 |
+
|
| 209 |
+
# If MCX/Commodity, we often group by the root name (CRUDEOIL)
|
| 210 |
+
# because Spot symbols aren't always traded or consistent.
|
| 211 |
+
if exch == "MCX":
|
| 212 |
+
# Use simple root name for grouping key, but empty spot implies logic in v16.txt handles it
|
| 213 |
+
match_mcx = desc_upper.split(' ')[0]
|
| 214 |
+
db_key = match_mcx # e.g. CRUDEOIL
|
| 215 |
+
# v16.txt logic: "If Spot is missing, default to Future".
|
| 216 |
+
# So we leave spot empty for MCX unless we really know it.
|
| 217 |
+
if not root_spot: root_spot = ""
|
| 218 |
+
else:
|
| 219 |
+
# For NSE/BSE, the Key IS the Spot Symbol (The "Exact Value")
|
| 220 |
+
if root_spot:
|
| 221 |
+
db_key = root_spot
|
| 222 |
+
else:
|
| 223 |
+
# Fallback to description first word if all else fails
|
| 224 |
+
db_key = desc_parts[0]
|
| 225 |
+
|
| 226 |
+
if db_key not in temp_db:
|
| 227 |
+
temp_db[db_key] = {
|
| 228 |
+
"spot": root_spot,
|
| 229 |
+
"exch": exch,
|
| 230 |
+
"items": []
|
| 231 |
+
}
|
| 232 |
|
| 233 |
try:
|
| 234 |
exp = int(row['Expiry'])
|
|
|
|
| 250 |
except:
|
| 251 |
pass
|
| 252 |
|
| 253 |
+
temp_db[db_key]["items"].append({
|
| 254 |
"s": sym, "e": exp, "k": strike, "t": opt_type
|
| 255 |
})
|
| 256 |
|
| 257 |
final_db = {}
|
| 258 |
search_list = []
|
| 259 |
|
| 260 |
+
for key, data in temp_db.items():
|
| 261 |
+
# Filter out junk groups with no items
|
| 262 |
+
if not data["items"]: continue
|
| 263 |
+
|
| 264 |
sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
|
| 265 |
+
final_db[key] = {
|
| 266 |
"spot": data["spot"],
|
| 267 |
"exch": data["exch"],
|
| 268 |
"items": sorted_items
|
| 269 |
}
|
| 270 |
+
|
| 271 |
+
# Search Result: Display the Key exactly as it is (e.g. NSE:NIFTY50-INDEX)
|
| 272 |
search_list.append({
|
| 273 |
+
"root": key, # This is sent to v16.txt
|
| 274 |
"exch": data["exch"],
|
| 275 |
+
"display": key # User sees this
|
| 276 |
})
|
| 277 |
|
| 278 |
MASTER_DB = final_db
|
|
|
|
| 369 |
return JSONResponse([])
|
| 370 |
|
| 371 |
query = q.upper()
|
| 372 |
+
# Filter: Check if query is in Root or Display
|
| 373 |
+
results = [x for x in SEARCH_INDEX if query in x['root'] or query in x['display']]
|
| 374 |
+
|
| 375 |
+
# Priority sorting: Starts with query first
|
| 376 |
+
results.sort(key=lambda x: 0 if x['root'].startswith(query) else 1)
|
| 377 |
+
|
| 378 |
return JSONResponse(results[:20])
|
| 379 |
|
| 380 |
@app.get("/api/chain")
|
| 381 |
def get_chain(symbol: str):
|
| 382 |
+
root = symbol.upper() # This will now receive "NSE:NIFTY50-INDEX" directly
|
| 383 |
if root in MASTER_DB:
|
| 384 |
return JSONResponse(MASTER_DB[root])
|
| 385 |
+
# Fallback: Try identifying simple root if full symbol fails (compatibility)
|
| 386 |
+
# But usually, v16 sends exactly what search returns
|
| 387 |
return JSONResponse({"error": "Symbol not found"}, status_code=404)
|
| 388 |
|
| 389 |
@app.get("/api/refresh")
|
|
|
|
| 839 |
const status = await res.json();
|
| 840 |
if(status.ready) {
|
| 841 |
document.getElementById('loader').style.display = 'none';
|
| 842 |
+
if(!CURRENT_ROOT) loadSymbol('NIFTY 50');
|
| 843 |
} else {
|
| 844 |
document.getElementById('loaderMsg').innerText = status.message.toUpperCase();
|
| 845 |
setTimeout(checkStatus, 2000);
|
|
|
|
| 878 |
document.getElementById('loader').style.display = 'flex';
|
| 879 |
|
| 880 |
try {
|
| 881 |
+
// Encode root because it may contain characters like ':'
|
| 882 |
+
const encRoot = encodeURIComponent(root);
|
| 883 |
+
const res = await fetch(`/api/chain?symbol=${encRoot}`);
|
| 884 |
if(!res.ok) throw new Error("Symbol not found");
|
| 885 |
const data = await res.json();
|
| 886 |
|
|
|
|
| 891 |
|
| 892 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 893 |
|
| 894 |
+
if(!SPOT_SYM && FUT_SYMS.length > 0) {
|
| 895 |
+
// Fallback if spot is empty (e.g. MCX), use Future
|
| 896 |
+
SPOT_SYM = FUT_SYMS[0].s;
|
| 897 |
+
}
|
| 898 |
|
| 899 |
+
const isIndex = SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ') || SPOT_SYM.startsWith('BSE:');
|
| 900 |
+
document.getElementById('spotLabel').innerText = isIndex ? "SPOT PRICE" : "FUT REFERENCE";
|
| 901 |
|
| 902 |
setupExpiries();
|
| 903 |
document.getElementById('loader').style.display = 'none';
|
|
|
|
| 983 |
IS_POLLING = true;
|
| 984 |
|
| 985 |
try {
|
| 986 |
+
// Ensure SPOT_SYM is valid (not empty) before requesting
|
| 987 |
+
let symbolsToFetch = [];
|
| 988 |
+
if(SPOT_SYM && SPOT_SYM.length > 2) symbolsToFetch.push(SPOT_SYM);
|
| 989 |
+
FUT_SYMS.forEach(f => { if(f.s) symbolsToFetch.push(f.s) });
|
| 990 |
+
|
| 991 |
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 992 |
|
| 993 |
if(kList.length > 0) {
|