Spaces:
Running
Running
AJAY KASU commited on
Commit ·
44f08fc
1
Parent(s): 02fc6bc
Feat: Market Cap Filtering (Smallest/Largest 50)
Browse files- api/static/index.html +9 -4
- core/schema.py +2 -0
- data/data_manager.py +52 -0
- 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 |
-
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
| 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)
|