main
py 87 lines 4.26 KB
Raw
1 """Versioned platform benchmark keys resolved only through registered master rows."""
2 from hashlib import md5
3 from uuid import UUID
4 from datetime import datetime
5
6 from app.sector_relative_strength import BenchmarkReference, SectorContext
7
8 MAPPING_VERSION = 'SECTOR_BENCHMARK_MAPPING_V1'
9 CATALOG_VERSION = 'NSE_BENCHMARK_CATALOG_V1'
10 BENCHMARKS = {
11 'INDIA_BROAD_PRICE': 'NIFTY 500',
12 'INDIA_TECHNOLOGY_PRICE': 'NIFTY IT',
13 'INDIA_FINANCIALS_PRICE': 'NIFTY FINANCIAL SERVICES',
14 'INDIA_HEALTHCARE_PRICE': 'NIFTY HEALTHCARE INDEX',
15 }
16 SECTOR_KEYS = {'Technology': 'INDIA_TECHNOLOGY_PRICE', 'Financials': 'INDIA_FINANCIALS_PRICE',
17 'Healthcare': 'INDIA_HEALTHCARE_PRICE'}
18 BROAD_KEY = 'INDIA_BROAD_PRICE'
19
20
21 def benchmark_id(key):
22 """Same platform-key UUIDv3 as portfolio-service; never a provider-symbol UUID."""
23 if key not in BENCHMARKS:
24 raise ValueError('UNKNOWN_BENCHMARK_KEY')
25 return UUID(bytes=md5(('aip:benchmark:' + key).encode(), usedforsecurity=False).digest(), version=3)
26
27
28 def benchmark_identity(metadata, key):
29 if not isinstance(metadata, dict):
30 raise ValueError('BENCHMARK_IDENTITY_UNAVAILABLE')
31 mappings = metadata.get('providerMappings') or []
32 nse = [m for m in mappings if isinstance(m, dict) and m.get('provider') == 'NSE']
33 if (str(metadata.get('globalInstrumentId')) != str(benchmark_id(key))
34 or metadata.get('assetType') != 'INDEX' or metadata.get('status') != 'ACTIVE'
35 or metadata.get('country') != 'IN' or metadata.get('primaryExchange') != 'NSE'
36 or metadata.get('currency') != 'INR' or len(nse) != 1):
37 raise ValueError('BENCHMARK_IDENTITY_UNAVAILABLE')
38 mapping = nse[0]
39 if (mapping.get('status') != 'VERIFIED' or mapping.get('active') is False
40 or mapping.get('resolutionSource') != CATALOG_VERSION or mapping.get('currency') != 'INR'
41 or mapping.get('exchange') != 'NSE' or mapping.get('providerSymbol') != BENCHMARKS[key]):
42 raise ValueError('BENCHMARK_IDENTITY_UNAVAILABLE')
43 return BenchmarkReference(benchmark_id(key), 'INR', frozenset({'NSE'}))
44
45
46 def build_sector_contexts(classifications, registered, instrument_ids):
47 """Pure adapter over canonical Nifty500 cache and registered benchmark metadata."""
48 by_id = {}
49 for row in registered:
50 by_id.setdefault(str(row.get('globalInstrumentId')), []).append(row)
51 def reference(key):
52 matches = by_id.get(str(benchmark_id(key)), [])
53 try:
54 return benchmark_identity(matches[0], key) if len(matches) == 1 else None
55 except ValueError:
56 return None
57 broad = reference(BROAD_KEY)
58 grouped = {}
59 for row in classifications:
60 grouped.setdefault(str(row.get('globalInstrumentId')), []).append(row)
61 output = {}
62 for key in sorted(instrument_ids, key=str):
63 matches = grouped.get(str(key), [])
64 row = matches[0] if len(matches) == 1 else {}
65 sector = row.get('canonicalSector')
66 stamp = row.get('retrievedAt')
67 try:
68 stamp = datetime.fromisoformat(stamp.replace('Z', '+00:00')) if isinstance(stamp, str) else stamp
69 except ValueError:
70 stamp = None
71 india_member = (row.get('source') == 'NSE_INDICES_NIFTY500' and row.get('status') == 'ACTIVE'
72 and row.get('assetType') == 'EQUITY' and row.get('country') == 'IN'
73 and row.get('exchange') in {'NSE', 'XNSE'})
74 if not india_member:
75 sector = None
76 target = SECTOR_KEYS.get(sector)
77 sector_ref = reference(target) if target else None
78 classification_ok = bool(sector and stamp)
79 status = ('NO_SECTOR_CLASSIFICATION' if not classification_ok else
80 'UNMAPPED_SECTOR_BENCHMARK' if not target else
81 'BENCHMARK_IDENTITY_UNAVAILABLE' if sector_ref is None else 'AVAILABLE')
82 market_ref = broad if india_member else None
83 output[key] = SectorContext(sector=sector, source=row.get('source'), as_of=stamp, region='INDIA' if india_member else None,
84 sector_benchmark=sector_ref if classification_ok else None, market_benchmark=market_ref,
85 mapping_version=MAPPING_VERSION, sector_mapping_status=status,
86 market_mapping_status='AVAILABLE' if market_ref else 'BENCHMARK_IDENTITY_UNAVAILABLE')
87 return output