Spaces:
Running
Running
Feature: IPO Discount, Greenshoe, & Ownership Logic
Browse files
main.py
CHANGED
|
@@ -309,6 +309,52 @@ def generate_advisory(signals, macro, fundamentals, last_private_price):
|
|
| 309 |
'risk_matrix': risk_matrix
|
| 310 |
}
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
# ==============================================================================
|
| 313 |
# ROUTES
|
| 314 |
# ==============================================================================
|
|
@@ -324,7 +370,10 @@ async def health_check():
|
|
| 324 |
@app.post("/analyze")
|
| 325 |
async def analyze(request: Request,
|
| 326 |
query: str = Form(...),
|
| 327 |
-
last_private: str = Form(None)
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
# 1. Determine Sector
|
| 330 |
sector_key = 'SaaS'
|
|
@@ -347,9 +396,31 @@ async def analyze(request: Request,
|
|
| 347 |
fundamentals = get_fundamentals(target_tickers)
|
| 348 |
|
| 349 |
# 4. The Advisor Engine
|
|
|
|
| 350 |
advisory = generate_advisory(signals, macro, fundamentals, last_private)
|
| 351 |
|
| 352 |
-
# 5.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
normalized = prices / prices.iloc[0] * 100
|
| 354 |
chart_data = []
|
| 355 |
for col in normalized.columns:
|
|
@@ -362,10 +433,12 @@ async def analyze(request: Request,
|
|
| 362 |
'mode': 'lines'
|
| 363 |
})
|
| 364 |
|
| 365 |
-
#
|
| 366 |
response_data = {
|
| 367 |
'sector': sector_key,
|
| 368 |
'advisory': advisory,
|
|
|
|
|
|
|
| 369 |
'macro': macro,
|
| 370 |
'metrics': {
|
| 371 |
'avg_momentum': np.mean([s['momentum'] for s in signals.values()]) if signals else 0,
|
|
|
|
| 309 |
'risk_matrix': risk_matrix
|
| 310 |
}
|
| 311 |
|
| 312 |
+
# ==============================================================================
|
| 313 |
+
# IPO STRUCTURING LOGIC (Professional)
|
| 314 |
+
# ==============================================================================
|
| 315 |
+
|
| 316 |
+
def calculate_ipo_structure(implied_price, discount_pct, greenshoe_active, existing_shares_m):
|
| 317 |
+
"""
|
| 318 |
+
Calculates final deal structure based on banking levers.
|
| 319 |
+
Assumption: Target Capital Raise = $250M (Standard for this segment)
|
| 320 |
+
"""
|
| 321 |
+
target_raise = 250.0 # $M
|
| 322 |
+
|
| 323 |
+
# 1. Apply Discount
|
| 324 |
+
discount_factor = (100 - discount_pct) / 100
|
| 325 |
+
final_price = implied_price * discount_factor
|
| 326 |
+
|
| 327 |
+
if final_price <= 0: final_price = 1.0 # Safety
|
| 328 |
+
|
| 329 |
+
# 2. Calculate Issuance
|
| 330 |
+
new_shares_m = target_raise / final_price
|
| 331 |
+
|
| 332 |
+
# 3. Greenshoe Adjustment (Standard 15% Over-allotment)
|
| 333 |
+
greenshoe_shares_m = 0.0
|
| 334 |
+
if greenshoe_active:
|
| 335 |
+
greenshoe_shares_m = new_shares_m * 0.15
|
| 336 |
+
new_shares_m += greenshoe_shares_m
|
| 337 |
+
target_raise += (greenshoe_shares_m * final_price)
|
| 338 |
+
|
| 339 |
+
# 4. Dilution & Ownership
|
| 340 |
+
total_shares_m = existing_shares_m + new_shares_m
|
| 341 |
+
dilution_pct = (new_shares_m / total_shares_m) * 100
|
| 342 |
+
|
| 343 |
+
# Ownership Split
|
| 344 |
+
ownership = {
|
| 345 |
+
"Existing Shareholders": round(existing_shares_m, 2),
|
| 346 |
+
"New Public Investors": round(new_shares_m, 2)
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
return {
|
| 350 |
+
"final_price": round(final_price, 2),
|
| 351 |
+
"capital_raised": round(target_raise, 2),
|
| 352 |
+
"new_shares": round(new_shares_m, 2),
|
| 353 |
+
"total_shares": round(total_shares_m, 2),
|
| 354 |
+
"dilution": round(dilution_pct, 1),
|
| 355 |
+
"ownership": ownership
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
# ==============================================================================
|
| 359 |
# ROUTES
|
| 360 |
# ==============================================================================
|
|
|
|
| 370 |
@app.post("/analyze")
|
| 371 |
async def analyze(request: Request,
|
| 372 |
query: str = Form(...),
|
| 373 |
+
last_private: str = Form(None),
|
| 374 |
+
ipo_discount: float = Form(15.0), # Default 15%
|
| 375 |
+
greenshoe: bool = Form(False), # Default Off
|
| 376 |
+
primary_shares: float = Form(100.0)): # Default 100M shares
|
| 377 |
|
| 378 |
# 1. Determine Sector
|
| 379 |
sector_key = 'SaaS'
|
|
|
|
| 396 |
fundamentals = get_fundamentals(target_tickers)
|
| 397 |
|
| 398 |
# 4. The Advisor Engine
|
| 399 |
+
# Get base advisory first to get the 'High' implied price
|
| 400 |
advisory = generate_advisory(signals, macro, fundamentals, last_private)
|
| 401 |
|
| 402 |
+
# 5. IPO Structuring (The Pro Layer)
|
| 403 |
+
# We use the midpoint of the implied range as the base for discounting
|
| 404 |
+
implied_midpoint = (advisory['low'] + advisory['high']) / 2
|
| 405 |
+
structure = calculate_ipo_structure(implied_midpoint, ipo_discount, greenshoe, primary_shares)
|
| 406 |
+
|
| 407 |
+
# Update Advisory with Final Price Context
|
| 408 |
+
final_price = structure['final_price']
|
| 409 |
+
|
| 410 |
+
# Re-Run Down-Round Logic on FINAL PRICE
|
| 411 |
+
down_round_alert = False
|
| 412 |
+
dr_text = ""
|
| 413 |
+
if last_private:
|
| 414 |
+
try:
|
| 415 |
+
lpp = float(last_private)
|
| 416 |
+
if final_price < lpp:
|
| 417 |
+
down_round_alert = True
|
| 418 |
+
diff = ((final_price - lpp) / lpp) * 100
|
| 419 |
+
dr_text = f"🚨 <b>DOWN-ROUND ALERT:</b> Final IPO Price (${final_price}) is {diff:.1f}% below Last Private Round (${lpp})."
|
| 420 |
+
except:
|
| 421 |
+
pass
|
| 422 |
+
|
| 423 |
+
# 6. Chart Data
|
| 424 |
normalized = prices / prices.iloc[0] * 100
|
| 425 |
chart_data = []
|
| 426 |
for col in normalized.columns:
|
|
|
|
| 433 |
'mode': 'lines'
|
| 434 |
})
|
| 435 |
|
| 436 |
+
# 7. Response
|
| 437 |
response_data = {
|
| 438 |
'sector': sector_key,
|
| 439 |
'advisory': advisory,
|
| 440 |
+
'structure': structure, # NEW
|
| 441 |
+
'down_round': {'is_active': down_round_alert, 'text': dr_text}, # NEW
|
| 442 |
'macro': macro,
|
| 443 |
'metrics': {
|
| 444 |
'avg_momentum': np.mean([s['momentum'] for s in signals.values()]) if signals else 0,
|