AJAY KASU commited on
Commit
44f08fc
·
1 Parent(s): 02fc6bc

Feat: Market Cap Filtering (Smallest/Largest 50)

Browse files
Files changed (4) hide show
  1. api/static/index.html +9 -4
  2. core/schema.py +2 -0
  3. data/data_manager.py +52 -0
  4. main.py +23 -0
api/static/index.html CHANGED
@@ -466,6 +466,8 @@
466
  "excluded_sectors": excluded,
467
  "excluded_tickers": excludedTickers,
468
  "max_weight": maxWeight,
 
 
469
  "initial_investment": 100000
470
  };
471
 
@@ -493,15 +495,18 @@
493
  function displayData(data, excluded) {
494
  // Metrics
495
  document.getElementById('teMetric').innerText = (data.tracking_error * 100).toFixed(4) + "%";
496
-
497
  let constraintText = excluded.length > 0 ? "Excl: " + excluded.join(", ") : "None";
498
  if (data.max_weight_applied) {
499
  constraintText += ` | Max Wgt: ${(data.max_weight_applied * 100).toFixed(1)}%`;
500
  } else if (payload.max_weight) {
501
- // Determine if backend ignored it or if we just want to show what we sent
502
- constraintText += ` | Max Wgt: ${(payload.max_weight * 100).toFixed(1)}% (Requested)`;
 
 
 
503
  }
504
-
505
  document.getElementById('excludedMetric').innerText = constraintText;
506
 
507
  // AI Text - Markdown clean
 
466
  "excluded_sectors": excluded,
467
  "excluded_tickers": excludedTickers,
468
  "max_weight": maxWeight,
469
+ "strategy": strategy,
470
+ "top_n": topN,
471
  "initial_investment": 100000
472
  };
473
 
 
495
  function displayData(data, excluded) {
496
  // Metrics
497
  document.getElementById('teMetric').innerText = (data.tracking_error * 100).toFixed(4) + "%";
498
+
499
  let constraintText = excluded.length > 0 ? "Excl: " + excluded.join(", ") : "None";
500
  if (data.max_weight_applied) {
501
  constraintText += ` | Max Wgt: ${(data.max_weight_applied * 100).toFixed(1)}%`;
502
  } else if (payload.max_weight) {
503
+ constraintText += ` | Max Wgt: ${(payload.max_weight * 100).toFixed(1)}% (Req)`;
504
+ }
505
+
506
+ if (payload.strategy) {
507
+ constraintText += ` | Strat: ${payload.strategy.replace('_market_cap', '')} ${payload.top_n}`;
508
  }
509
+
510
  document.getElementById('excludedMetric').innerText = constraintText;
511
 
512
  // AI Text - Markdown clean
core/schema.py CHANGED
@@ -27,6 +27,8 @@ class OptimizationRequest(BaseModel):
27
  excluded_sectors: List[str] = Field(default_factory=list, description="List of sectors to exclude (e.g., ['Energy'])")
28
  excluded_tickers: List[str] = Field(default_factory=list, description="List of specific tickers to exclude (e.g., ['AMZN'])")
29
  max_weight: Optional[float] = Field(None, description="Maximum weight for any single asset (e.g., 0.05)")
 
 
30
  benchmark: str = "^GSPC"
31
 
32
  class Config:
 
27
  excluded_sectors: List[str] = Field(default_factory=list, description="List of sectors to exclude (e.g., ['Energy'])")
28
  excluded_tickers: List[str] = Field(default_factory=list, description="List of specific tickers to exclude (e.g., ['AMZN'])")
29
  max_weight: Optional[float] = Field(None, description="Maximum weight for any single asset (e.g., 0.05)")
30
+ strategy: Optional[str] = Field(None, description="Global Filter Strategy: 'smallest_market_cap' or 'largest_market_cap'")
31
+ top_n: Optional[int] = Field(None, description="Number of assets to select for strategy (e.g. 50)")
32
  benchmark: str = "^GSPC"
33
 
34
  class Config:
data/data_manager.py CHANGED
@@ -150,3 +150,55 @@ class MarketDataEngine:
150
 
151
  def get_sector_map(self) -> Dict[str, str]:
152
  return self.sector_cache.sector_map
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  def get_sector_map(self) -> Dict[str, str]:
152
  return self.sector_cache.sector_map
153
+
154
+ def fetch_market_caps(self, tickers: List[str]) -> Dict[str, float]:
155
+ """
156
+ Fetches market caps for a list of tickers, using a local cache to speed up subsequent runs.
157
+ """
158
+ cache_file = os.path.join(settings.DATA_DIR, "market_cap_cache.json")
159
+ caps = {}
160
+
161
+ # Load Cache
162
+ if os.path.exists(cache_file):
163
+ try:
164
+ with open(cache_file, 'r') as f:
165
+ caps = json.load(f)
166
+ except Exception as e:
167
+ logger.error(f"Failed to load cap cache: {e}")
168
+
169
+ # Identify missing tickers
170
+ missing = [t for t in tickers if t not in caps]
171
+
172
+ if missing:
173
+ logger.info(f"Fetching market caps for {len(missing)} tickers (can take 60s)...")
174
+ import concurrent.futures
175
+
176
+ def get_cap(ticker):
177
+ try:
178
+ # Use yfinance fast_info for speed (no web scraping)
179
+ # fast_info works well, fallback to info
180
+ info = yf.Ticker(ticker).fast_info
181
+ return ticker, info['market_cap']
182
+ except:
183
+ # Retry logic or just 0
184
+ try:
185
+ return ticker, yf.Ticker(ticker).info.get('marketCap', 0)
186
+ except:
187
+ return ticker, 0
188
+
189
+ with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
190
+ results = executor.map(get_cap, missing)
191
+
192
+ for ticker, cap in results:
193
+ if cap and cap > 0:
194
+ caps[ticker] = cap
195
+
196
+ # Save Cache
197
+ try:
198
+ with open(cache_file, 'w') as f:
199
+ json.dump(caps, f, indent=2)
200
+ except Exception as e:
201
+ logger.error(f"Failed to save cap cache: {e}")
202
+
203
+ # Return only requested tickers
204
+ return {t: caps.get(t, 0) for t in tickers}
main.py CHANGED
@@ -44,6 +44,29 @@ class QuantScaleSystem:
44
  # 3. Compute Risk Model
45
  # Ensure we align returns and tickers
46
  valid_tickers = returns.columns.tolist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  cov_matrix = self.risk_model.compute_covariance_matrix(returns)
48
 
49
  # 4. Get Benchmark Data (S&P 500)
 
44
  # 3. Compute Risk Model
45
  # Ensure we align returns and tickers
46
  valid_tickers = returns.columns.tolist()
47
+
48
+ # APPLY FILTERING STRATEGY (New)
49
+ if request.strategy and request.top_n:
50
+ logger.info(f"Applying Strategy: {request.strategy} with Top N={request.top_n}")
51
+ caps = self.data_engine.fetch_market_caps(valid_tickers)
52
+
53
+ # Sort valid_tickers by cap
54
+ # Filter out 0 caps (failed fetches)
55
+ valid_caps = {t: c for t, c in caps.items() if c > 0}
56
+ sorted_tickers = sorted(valid_caps.keys(), key=lambda t: valid_caps[t])
57
+
58
+ if request.strategy == "smallest_market_cap":
59
+ valid_tickers = sorted_tickers[:request.top_n]
60
+ logger.info(f"Filtered to Smallest {request.top_n}: {valid_tickers[:5]}...")
61
+
62
+ elif request.strategy == "largest_market_cap":
63
+ valid_tickers = sorted_tickers[-request.top_n:]
64
+ logger.info(f"Filtered to Largest {request.top_n}: {valid_tickers[:5]}...")
65
+
66
+ # Re-fetch returns for just these? No, we already have `returns` DF.
67
+ # Just slice the DF to save computation in Risk Model
68
+ returns = returns[valid_tickers]
69
+
70
  cov_matrix = self.risk_model.compute_covariance_matrix(returns)
71
 
72
  # 4. Get Benchmark Data (S&P 500)