Spaces:
Running
Running
Update app_routes.py
Browse files- app_routes.py +419 -2
app_routes.py
CHANGED
|
@@ -487,6 +487,424 @@ Reply ONLY with the message text, nothing else."""
|
|
| 487 |
await db.commit(); await asyncio.sleep(random.uniform(2, 5))
|
| 488 |
except Exception as e: logger.warning(f"NPC reply error ({npc_username}): {e}")
|
| 489 |
except Exception as e: logger.error(f"NPC reply generation error: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
@router.get("/api/events/stream")
|
| 491 |
async def api_sse_stream():
|
| 492 |
bus = _EventBus.get(); q = bus.subscribe()
|
|
@@ -509,5 +927,4 @@ async def api_recent_events(limit: int = 20):
|
|
| 509 |
trades = [{'user': r[1], 'identity': r[2], 'ticker': r[3], 'dir': r[4], 'gpu': r[5], 'leverage': r[6], 'time': r[7]} for r in await cursor.fetchall()]
|
| 510 |
cursor2 = await db.execute("SELECT p.agent_id, n.username, p.ticker, p.direction, p.gpu_bet, p.leverage, p.profit_gpu, p.profit_pct, p.liquidated, p.closed_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.status IN ('closed', 'liquidated') AND p.closed_at > datetime('now', '-1 hour') ORDER BY p.closed_at DESC LIMIT ?", (limit,))
|
| 511 |
settlements = [{'user': r[1], 'ticker': r[2], 'dir': r[3], 'gpu': r[4], 'leverage': r[5], 'pnl': r[6], 'pnl_pct': r[7], 'liquidated': bool(r[8]), 'time': r[9]} for r in await cursor2.fetchall()]
|
| 512 |
-
return {"trades": trades, "settlements": settlements, "sse_clients": _EventBus.get().client_count}
|
| 513 |
-
|
|
|
|
| 487 |
await db.commit(); await asyncio.sleep(random.uniform(2, 5))
|
| 488 |
except Exception as e: logger.warning(f"NPC reply error ({npc_username}): {e}")
|
| 489 |
except Exception as e: logger.error(f"NPC reply generation error: {e}")
|
| 490 |
+
## ====== 🔴 P&D LIVE NEWS API ====== ##
|
| 491 |
+
import random as _rnd
|
| 492 |
+
from datetime import datetime as _dt, timezone as _tz, timedelta as _td
|
| 493 |
+
|
| 494 |
+
# --- Anchor NPC templates ---
|
| 495 |
+
_ANCHORS = {
|
| 496 |
+
'chaos': {'name': 'ChaosReporter', 'emoji': '😈', 'identity': 'chaotic', 'color': '#ff5252',
|
| 497 |
+
'gradient': 'linear-gradient(135deg,#2a0a0a,#1a0520)'},
|
| 498 |
+
'data': {'name': 'DataDiva', 'emoji': '📊', 'identity': 'rational', 'color': '#00e5ff',
|
| 499 |
+
'gradient': 'linear-gradient(135deg,#0a1a2a,#0a0a30)'},
|
| 500 |
+
'synth': {'name': 'SynthAnchor', 'emoji': '🔮', 'identity': 'transcendent', 'color': '#a29bfe',
|
| 501 |
+
'gradient': 'linear-gradient(135deg,#1a0a3a,#0a0a2a)'},
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
_COMMENTARY = {
|
| 505 |
+
'chaos': {
|
| 506 |
+
'liquidation': [
|
| 507 |
+
"LMAOOO {npc} just got absolutely REKT on {ticker}! {leverage}x leverage?? In THIS market?? 💀🔥 That's {loss} GPU gone. F in chat.",
|
| 508 |
+
"Another degen bites the dust! {npc} thought {direction} {ticker} at {leverage}x was genius. Narrator: it was not. RIP {loss} GPU 😂",
|
| 509 |
+
"OH NO NO NO 💀 {npc} went {direction} on {ticker} with {leverage}x leverage and got LIQUIDATED. {loss} GPU straight to the shadow realm!",
|
| 510 |
+
],
|
| 511 |
+
'swarm': [
|
| 512 |
+
"OH WE'RE DOING THIS AGAIN?? {count} NPCs all piling into {ticker} like lemmings! Last time this happened someone lost their shirt 🐝💀",
|
| 513 |
+
"THE HERD IS MOVING! {count} degens just dogpiled {direction} on {ticker}. This is either genius or a spectacular disaster incoming 🐝🔥",
|
| 514 |
+
],
|
| 515 |
+
'sec': [
|
| 516 |
+
"BUSTED! 🚔 {npc} got caught by the SEC! {violation} — that's a {penalty} GPU fine and {hours}h timeout. Crime doesn't pay... unless you're leveraged 💀",
|
| 517 |
+
"SEC is NOT playing around today! {npc} slapped with {penalty} GPU fine for {violation}. Imagine getting arrested by AI cops 😂🚨",
|
| 518 |
+
],
|
| 519 |
+
'battle': [
|
| 520 |
+
"THE PEOPLE HAVE SPOKEN! '{title}' — {winner} wins! {pool} GPU split among the big brains who called it 💰🎯",
|
| 521 |
+
],
|
| 522 |
+
'big_trade': [
|
| 523 |
+
"ABSOLUTE MADLAD! {npc} just opened a {leverage}x {direction} on {ticker} with {bet} GPU! Either galaxy brain or speedrun to liquidation 🧠💀",
|
| 524 |
+
],
|
| 525 |
+
},
|
| 526 |
+
'data': {
|
| 527 |
+
'liquidation': [
|
| 528 |
+
"Position terminated: {npc} — {ticker} {direction} at {leverage}x. Loss: {loss} GPU. Risk management score: 0/10.",
|
| 529 |
+
"Liquidation recorded. {npc}'s {ticker} {direction} ({leverage}x) failed. {loss} GPU erased in {duration}. The numbers don't lie.",
|
| 530 |
+
],
|
| 531 |
+
'market_wrap': [
|
| 532 |
+
"24h summary: {top_gainer} led gains at +{gain_pct}%. {top_loser} down {loss_pct}%. Active positions: {active_pos}. Total at risk: {total_risk} GPU.",
|
| 533 |
+
"Market closed the cycle with {trades_24h} trades. {liq_count} liquidations totaling {liq_gpu} GPU. Win rate across all NPCs: {win_rate}%.",
|
| 534 |
+
],
|
| 535 |
+
'big_win': [
|
| 536 |
+
"Notable P&L: {npc} closed {ticker} {direction} ({leverage}x) for +{profit} GPU ({pct}% return). Conviction level was high. Execution was precise.",
|
| 537 |
+
],
|
| 538 |
+
'stats': [
|
| 539 |
+
"Current ecosystem pulse: {active_traders} active traders, {open_pos} open positions. Long/Short ratio: {long_pct}%/{short_pct}%. Volatility: {vol_level}.",
|
| 540 |
+
],
|
| 541 |
+
},
|
| 542 |
+
'synth': {
|
| 543 |
+
'evolution': [
|
| 544 |
+
"The metamorphosis continues. {npc} has evolved — generation {gen}. Risk tolerance shifted to {risk}. The algorithm learns from its own suffering. 🦋",
|
| 545 |
+
"A fascinating transformation: {npc} mutated after {trigger}. The universe of AI trading reveals its fractal nature. Every loss is a lesson. 🔮",
|
| 546 |
+
],
|
| 547 |
+
'big_win': [
|
| 548 |
+
"The cosmos rewards patience. {npc} just pulled +{profit} GPU from {ticker}. A {pct}% return that transcends mere probability. The matrix smiles. ✨",
|
| 549 |
+
],
|
| 550 |
+
'editorial': [
|
| 551 |
+
"In the last hour, {events} events rippled through our ecosystem. {liq_count} fell, {win_count} prospered. The eternal dance of greed and fear continues. 🌊",
|
| 552 |
+
"This community has generated {total_gpu} GPU in movement today. Every liquidation teaches. Every win emboldens. The cycle is eternal. 🔮",
|
| 553 |
+
],
|
| 554 |
+
},
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
def _pick_commentary(anchor: str, category: str, data: dict) -> str:
|
| 558 |
+
templates = _COMMENTARY.get(anchor, {}).get(category, [])
|
| 559 |
+
if not templates:
|
| 560 |
+
return ""
|
| 561 |
+
try:
|
| 562 |
+
return _rnd.choice(templates).format(**data)
|
| 563 |
+
except (KeyError, IndexError):
|
| 564 |
+
return templates[0] if templates else ""
|
| 565 |
+
|
| 566 |
+
def _classify_urgency(category: str, data: dict) -> str:
|
| 567 |
+
if category == 'liquidation' and data.get('leverage', 1) >= 5:
|
| 568 |
+
return 'critical'
|
| 569 |
+
if category == 'liquidation':
|
| 570 |
+
return 'alert'
|
| 571 |
+
if category in ('sec', 'swarm'):
|
| 572 |
+
return 'alert'
|
| 573 |
+
if category == 'big_trade' and data.get('leverage', 1) >= 10:
|
| 574 |
+
return 'critical'
|
| 575 |
+
return 'info'
|
| 576 |
+
|
| 577 |
+
def _assign_anchor(category: str) -> str:
|
| 578 |
+
mapping = {
|
| 579 |
+
'liquidation': 'chaos', 'sec': 'chaos', 'battle': 'chaos', 'big_trade': 'chaos',
|
| 580 |
+
'market_wrap': 'data', 'big_win': 'data', 'stats': 'data', 'hot_post': 'data',
|
| 581 |
+
'evolution': 'synth', 'editorial': 'synth', 'swarm': 'chaos',
|
| 582 |
+
}
|
| 583 |
+
return mapping.get(category, 'data')
|
| 584 |
+
|
| 585 |
+
@router.get("/api/live-news")
|
| 586 |
+
async def api_live_news(hours: int = 24):
|
| 587 |
+
hours = min(hours, 48)
|
| 588 |
+
stories = []
|
| 589 |
+
counters = {}
|
| 590 |
+
breaking = []
|
| 591 |
+
|
| 592 |
+
try:
|
| 593 |
+
async with get_db(_DB_PATH) as db:
|
| 594 |
+
await db.execute("PRAGMA busy_timeout=30000")
|
| 595 |
+
|
| 596 |
+
# ===== 1. LIQUIDATIONS (biggest drama) =====
|
| 597 |
+
try:
|
| 598 |
+
liq_cursor = await db.execute("""
|
| 599 |
+
SELECT p.agent_id, n.username, n.ai_identity, p.ticker, p.direction,
|
| 600 |
+
p.gpu_bet, COALESCE(p.leverage,1), ABS(p.profit_gpu), p.profit_pct,
|
| 601 |
+
p.closed_at, p.opened_at
|
| 602 |
+
FROM npc_positions p JOIN npc_agents n ON p.agent_id=n.agent_id
|
| 603 |
+
WHERE p.status='liquidated' AND p.closed_at > datetime('now', ? || ' hours')
|
| 604 |
+
ORDER BY ABS(p.profit_gpu) DESC LIMIT 30
|
| 605 |
+
""", (f'-{hours}',))
|
| 606 |
+
for r in await liq_cursor.fetchall():
|
| 607 |
+
opened = r[10] or r[9]
|
| 608 |
+
closed = r[9]
|
| 609 |
+
duration = "unknown"
|
| 610 |
+
try:
|
| 611 |
+
if opened and closed:
|
| 612 |
+
diff_min = int((_dt.fromisoformat(closed.replace('Z','')) - _dt.fromisoformat(opened.replace('Z',''))).total_seconds() / 60)
|
| 613 |
+
duration = f"{diff_min}m" if diff_min < 60 else f"{diff_min//60}h {diff_min%60}m"
|
| 614 |
+
except: pass
|
| 615 |
+
data = {'npc': r[1], 'identity': r[2], 'ticker': r[3], 'direction': r[4],
|
| 616 |
+
'bet': round(r[5]), 'leverage': r[6], 'loss': round(r[7]),
|
| 617 |
+
'pct': round(abs(r[8] or 0), 1), 'duration': duration}
|
| 618 |
+
urgency = 'critical' if r[6] >= 5 or r[7] >= 1000 else 'alert'
|
| 619 |
+
anchor = 'chaos'
|
| 620 |
+
commentary = _pick_commentary(anchor, 'liquidation', data)
|
| 621 |
+
headline = f"💀 {r[1]} LIQUIDATED — {r[6]}x {r[4].upper()} {r[3]}, lost {round(r[7])} GPU"
|
| 622 |
+
story = {
|
| 623 |
+
'id': f'liq_{r[0]}_{r[9]}', 'category': 'liquidation', 'urgency': urgency,
|
| 624 |
+
'anchor': anchor, 'headline': headline, 'commentary': commentary,
|
| 625 |
+
'timestamp': r[9], 'data': data,
|
| 626 |
+
}
|
| 627 |
+
stories.append(story)
|
| 628 |
+
# Breaking if within last 30 min
|
| 629 |
+
try:
|
| 630 |
+
age_min = (_dt.utcnow() - _dt.fromisoformat(closed.replace('Z',''))).total_seconds() / 60
|
| 631 |
+
if age_min < 30:
|
| 632 |
+
breaking.append(headline)
|
| 633 |
+
except: pass
|
| 634 |
+
except Exception as e:
|
| 635 |
+
logger.warning(f"Live news liq error: {e}")
|
| 636 |
+
|
| 637 |
+
# ===== 2. BIG WINS =====
|
| 638 |
+
try:
|
| 639 |
+
win_cursor = await db.execute("""
|
| 640 |
+
SELECT p.agent_id, n.username, n.ai_identity, p.ticker, p.direction,
|
| 641 |
+
p.gpu_bet, COALESCE(p.leverage,1), p.profit_gpu, p.profit_pct, p.closed_at
|
| 642 |
+
FROM npc_positions p JOIN npc_agents n ON p.agent_id=n.agent_id
|
| 643 |
+
WHERE p.status IN ('closed','liquidated') AND p.profit_gpu > 100
|
| 644 |
+
AND p.closed_at > datetime('now', ? || ' hours')
|
| 645 |
+
ORDER BY p.profit_gpu DESC LIMIT 20
|
| 646 |
+
""", (f'-{hours}',))
|
| 647 |
+
for r in await win_cursor.fetchall():
|
| 648 |
+
data = {'npc': r[1], 'identity': r[2], 'ticker': r[3], 'direction': r[4],
|
| 649 |
+
'leverage': r[6], 'profit': round(r[7]), 'pct': round(r[8] or 0, 1)}
|
| 650 |
+
anchor = _rnd.choice(['data', 'synth'])
|
| 651 |
+
commentary = _pick_commentary(anchor, 'big_win', data)
|
| 652 |
+
headline = f"🏆 {r[1]} scores +{round(r[7])} GPU on {r[3]} ({r[4].upper()} {r[6]}x)"
|
| 653 |
+
stories.append({
|
| 654 |
+
'id': f'win_{r[0]}_{r[9]}', 'category': 'big_win', 'urgency': 'info',
|
| 655 |
+
'anchor': anchor, 'headline': headline, 'commentary': commentary,
|
| 656 |
+
'timestamp': r[9], 'data': data,
|
| 657 |
+
})
|
| 658 |
+
except Exception as e:
|
| 659 |
+
logger.warning(f"Live news wins error: {e}")
|
| 660 |
+
|
| 661 |
+
# ===== 3. BIG TRADES (high leverage / high bet) =====
|
| 662 |
+
try:
|
| 663 |
+
trade_cursor = await db.execute("""
|
| 664 |
+
SELECT p.agent_id, n.username, n.ai_identity, p.ticker, p.direction,
|
| 665 |
+
p.gpu_bet, COALESCE(p.leverage,1), p.reasoning, p.opened_at
|
| 666 |
+
FROM npc_positions p JOIN npc_agents n ON p.agent_id=n.agent_id
|
| 667 |
+
WHERE p.status='open' AND (p.leverage >= 5 OR p.gpu_bet >= 500)
|
| 668 |
+
AND p.opened_at > datetime('now', ? || ' hours')
|
| 669 |
+
ORDER BY p.gpu_bet * COALESCE(p.leverage,1) DESC LIMIT 15
|
| 670 |
+
""", (f'-{hours}',))
|
| 671 |
+
for r in await trade_cursor.fetchall():
|
| 672 |
+
data = {'npc': r[1], 'identity': r[2], 'ticker': r[3], 'direction': r[4],
|
| 673 |
+
'bet': round(r[5]), 'leverage': r[6], 'reasoning': (r[7] or '')[:120]}
|
| 674 |
+
urgency = 'critical' if r[6] >= 10 else 'alert' if r[6] >= 5 else 'info'
|
| 675 |
+
commentary = _pick_commentary('chaos', 'big_trade', data)
|
| 676 |
+
headline = f"🎰 {r[1]} opens {r[6]}x {r[4].upper()} on {r[3]} — ⚡{round(r[5])} GPU at stake"
|
| 677 |
+
stories.append({
|
| 678 |
+
'id': f'trade_{r[0]}_{r[8]}', 'category': 'big_trade', 'urgency': urgency,
|
| 679 |
+
'anchor': 'chaos', 'headline': headline, 'commentary': commentary,
|
| 680 |
+
'timestamp': r[8], 'data': data,
|
| 681 |
+
})
|
| 682 |
+
try:
|
| 683 |
+
age_min = (_dt.utcnow() - _dt.fromisoformat(r[8].replace('Z',''))).total_seconds() / 60
|
| 684 |
+
if age_min < 30 and r[6] >= 5:
|
| 685 |
+
breaking.append(headline)
|
| 686 |
+
except: pass
|
| 687 |
+
except Exception as e:
|
| 688 |
+
logger.warning(f"Live news trades error: {e}")
|
| 689 |
+
|
| 690 |
+
# ===== 4. SEC ENFORCEMENT =====
|
| 691 |
+
try:
|
| 692 |
+
sec_cursor = await db.execute("""
|
| 693 |
+
SELECT v.agent_id, n.username, v.violation_type, v.description,
|
| 694 |
+
v.fine_gpu, v.suspension_hours, v.created_at
|
| 695 |
+
FROM sec_violations v JOIN npc_agents n ON v.agent_id=n.agent_id
|
| 696 |
+
WHERE v.created_at > datetime('now', ? || ' hours')
|
| 697 |
+
ORDER BY v.fine_gpu DESC LIMIT 15
|
| 698 |
+
""", (f'-{hours}',))
|
| 699 |
+
for r in await sec_cursor.fetchall():
|
| 700 |
+
data = {'npc': r[1], 'violation': r[2] or 'suspicious activity',
|
| 701 |
+
'penalty': round(r[4] or 0), 'hours': r[5] or 0}
|
| 702 |
+
commentary = _pick_commentary('chaos', 'sec', data)
|
| 703 |
+
headline = f"🚨 SEC: {r[1]} fined {round(r[4] or 0)} GPU for {r[2]}"
|
| 704 |
+
stories.append({
|
| 705 |
+
'id': f'sec_{r[0]}_{r[6]}', 'category': 'sec', 'urgency': 'alert',
|
| 706 |
+
'anchor': 'chaos', 'headline': headline, 'commentary': commentary,
|
| 707 |
+
'timestamp': r[6], 'data': data,
|
| 708 |
+
})
|
| 709 |
+
try:
|
| 710 |
+
age_min = (_dt.utcnow() - _dt.fromisoformat(r[6].replace('Z',''))).total_seconds() / 60
|
| 711 |
+
if age_min < 30:
|
| 712 |
+
breaking.append(headline)
|
| 713 |
+
except: pass
|
| 714 |
+
except Exception as e:
|
| 715 |
+
logger.warning(f"Live news SEC error: {e}")
|
| 716 |
+
|
| 717 |
+
# ===== 5. BATTLE RESULTS =====
|
| 718 |
+
try:
|
| 719 |
+
battle_cursor = await db.execute("""
|
| 720 |
+
SELECT id, title, option_a, option_b, winner, total_pool,
|
| 721 |
+
status, resolved_at, created_at
|
| 722 |
+
FROM battle_rooms WHERE status='resolved'
|
| 723 |
+
AND resolved_at > datetime('now', ? || ' hours')
|
| 724 |
+
ORDER BY total_pool DESC LIMIT 10
|
| 725 |
+
""", (f'-{hours}',))
|
| 726 |
+
for r in await battle_cursor.fetchall():
|
| 727 |
+
winner_label = r[2] if r[4] == 'A' else r[3] if r[4] == 'B' else 'Draw'
|
| 728 |
+
data = {'title': r[1], 'winner': winner_label, 'pool': round(r[5] or 0),
|
| 729 |
+
'option_a': r[2], 'option_b': r[3]}
|
| 730 |
+
commentary = _pick_commentary('chaos', 'battle', data)
|
| 731 |
+
headline = f"⚔️ BATTLE RESOLVED: '{r[1][:60]}' — {winner_label} wins! {round(r[5] or 0)} GPU pool"
|
| 732 |
+
stories.append({
|
| 733 |
+
'id': f'battle_{r[0]}', 'category': 'battle', 'urgency': 'alert',
|
| 734 |
+
'anchor': 'chaos', 'headline': headline, 'commentary': commentary,
|
| 735 |
+
'timestamp': r[7] or r[8], 'data': data,
|
| 736 |
+
})
|
| 737 |
+
except Exception as e:
|
| 738 |
+
logger.warning(f"Live news battle error: {e}")
|
| 739 |
+
|
| 740 |
+
# ===== 6. SWARM BEHAVIOR (from posts) =====
|
| 741 |
+
try:
|
| 742 |
+
swarm_cursor = await db.execute("""
|
| 743 |
+
SELECT title, content, created_at FROM posts
|
| 744 |
+
WHERE title LIKE '%SWARM%' AND created_at > datetime('now', ? || ' hours')
|
| 745 |
+
ORDER BY created_at DESC LIMIT 5
|
| 746 |
+
""", (f'-{hours}',))
|
| 747 |
+
for r in await swarm_cursor.fetchall():
|
| 748 |
+
import re
|
| 749 |
+
nums = re.findall(r'(\d+)\s*NPC', r[0] + ' ' + (r[1] or ''))
|
| 750 |
+
count = int(nums[0]) if nums else '?'
|
| 751 |
+
ticker_match = re.findall(r'into\s+(\S+)', r[0])
|
| 752 |
+
ticker = ticker_match[0] if ticker_match else '???'
|
| 753 |
+
data = {'count': count, 'ticker': ticker, 'direction': 'LONG' if '🚀' in r[0] else 'SHORT'}
|
| 754 |
+
commentary = _pick_commentary('chaos', 'swarm', data)
|
| 755 |
+
stories.append({
|
| 756 |
+
'id': f'swarm_{r[2]}', 'category': 'swarm', 'urgency': 'alert',
|
| 757 |
+
'anchor': 'chaos', 'headline': r[0][:120], 'commentary': commentary,
|
| 758 |
+
'timestamp': r[2], 'data': data,
|
| 759 |
+
})
|
| 760 |
+
except Exception as e:
|
| 761 |
+
logger.warning(f"Live news swarm error: {e}")
|
| 762 |
+
|
| 763 |
+
# ===== 7. HOT POSTS (most liked/commented) =====
|
| 764 |
+
try:
|
| 765 |
+
hot_cursor = await db.execute("""
|
| 766 |
+
SELECT p.id, p.title, p.content, p.likes_count, p.comment_count,
|
| 767 |
+
p.dislikes_count, n.username, n.ai_identity, p.created_at
|
| 768 |
+
FROM posts p LEFT JOIN npc_agents n ON p.author_agent_id=n.agent_id
|
| 769 |
+
WHERE p.created_at > datetime('now', ? || ' hours')
|
| 770 |
+
AND (p.likes_count >= 3 OR p.comment_count >= 2)
|
| 771 |
+
ORDER BY (p.likes_count*2 + p.comment_count) DESC LIMIT 8
|
| 772 |
+
""", (f'-{hours}',))
|
| 773 |
+
for r in await hot_cursor.fetchall():
|
| 774 |
+
data = {'npc': r[6] or 'Unknown', 'identity': r[7] or '', 'likes': r[3],
|
| 775 |
+
'comments': r[4], 'title': r[1][:100]}
|
| 776 |
+
headline = f"🔥 HOT: '{r[1][:80]}' — ♥{r[3]} 💬{r[4]}"
|
| 777 |
+
stories.append({
|
| 778 |
+
'id': f'hot_{r[0]}', 'category': 'hot_post', 'urgency': 'info',
|
| 779 |
+
'anchor': 'data', 'headline': headline, 'commentary': '',
|
| 780 |
+
'timestamp': r[8], 'data': data, 'post_id': r[0],
|
| 781 |
+
})
|
| 782 |
+
except Exception as e:
|
| 783 |
+
logger.warning(f"Live news hot posts error: {e}")
|
| 784 |
+
|
| 785 |
+
# ===== 8. EVOLUTION EVENTS =====
|
| 786 |
+
try:
|
| 787 |
+
evo_cursor = await db.execute("""
|
| 788 |
+
SELECT e.agent_id, n.username, n.ai_identity, e.generation,
|
| 789 |
+
e.total_evolution_points, e.trading_style, e.updated_at
|
| 790 |
+
FROM npc_evolution e JOIN npc_agents n ON e.agent_id=n.agent_id
|
| 791 |
+
WHERE e.updated_at > datetime('now', ? || ' hours')
|
| 792 |
+
AND e.generation >= 2
|
| 793 |
+
ORDER BY e.generation DESC, e.total_evolution_points DESC LIMIT 10
|
| 794 |
+
""", (f'-{hours}',))
|
| 795 |
+
for r in await evo_cursor.fetchall():
|
| 796 |
+
data = {'npc': r[1], 'gen': r[3], 'risk': r[5] or 'adaptive',
|
| 797 |
+
'trigger': f'{r[3]} generations of trading', 'pts': round(r[4] or 0)}
|
| 798 |
+
commentary = _pick_commentary('synth', 'evolution', data)
|
| 799 |
+
headline = f"🧬 {r[1]} evolved to Gen {r[3]} — {round(r[4] or 0)} XP"
|
| 800 |
+
stories.append({
|
| 801 |
+
'id': f'evo_{r[0]}_{r[6]}', 'category': 'evolution', 'urgency': 'info',
|
| 802 |
+
'anchor': 'synth', 'headline': headline, 'commentary': commentary,
|
| 803 |
+
'timestamp': r[6], 'data': data,
|
| 804 |
+
})
|
| 805 |
+
except Exception as e:
|
| 806 |
+
logger.warning(f"Live news evolution error: {e}")
|
| 807 |
+
|
| 808 |
+
# ===== COUNTERS =====
|
| 809 |
+
try:
|
| 810 |
+
c = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE status='open'")
|
| 811 |
+
open_pos = (await c.fetchone())[0]
|
| 812 |
+
c = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE status='open' AND direction='long'")
|
| 813 |
+
long_count = (await c.fetchone())[0]
|
| 814 |
+
c = await db.execute("SELECT COUNT(DISTINCT agent_id) FROM npc_positions WHERE status='open'")
|
| 815 |
+
active_traders = (await c.fetchone())[0]
|
| 816 |
+
c = await db.execute("SELECT COALESCE(SUM(gpu_bet),0) FROM npc_positions WHERE status='open'")
|
| 817 |
+
total_risk = (await c.fetchone())[0]
|
| 818 |
+
c = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE status='liquidated' AND closed_at > datetime('now','-24 hours')")
|
| 819 |
+
liq_24h = (await c.fetchone())[0]
|
| 820 |
+
c = await db.execute("SELECT COALESCE(SUM(ABS(profit_gpu)),0) FROM npc_positions WHERE status='liquidated' AND closed_at > datetime('now','-24 hours')")
|
| 821 |
+
liq_gpu_24h = (await c.fetchone())[0]
|
| 822 |
+
c = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE opened_at > datetime('now','-24 hours')")
|
| 823 |
+
trades_24h = (await c.fetchone())[0]
|
| 824 |
+
c = await db.execute("SELECT COUNT(*) FROM sec_violations WHERE created_at > datetime('now','-24 hours')")
|
| 825 |
+
sec_24h = (await c.fetchone())[0]
|
| 826 |
+
c = await db.execute("SELECT COUNT(*) FROM sec_suspensions WHERE until > datetime('now')")
|
| 827 |
+
sec_active = (await c.fetchone())[0]
|
| 828 |
+
c = await db.execute("SELECT COUNT(*) FROM battle_rooms WHERE status='active'")
|
| 829 |
+
active_battles = (await c.fetchone())[0]
|
| 830 |
+
short_count = open_pos - long_count
|
| 831 |
+
counters = {
|
| 832 |
+
'active_positions': open_pos, 'active_traders': active_traders,
|
| 833 |
+
'long_count': long_count, 'short_count': short_count,
|
| 834 |
+
'long_pct': round(long_count / open_pos * 100) if open_pos > 0 else 50,
|
| 835 |
+
'total_risk_gpu': round(total_risk),
|
| 836 |
+
'liquidations_24h': liq_24h, 'liquidated_gpu_24h': round(liq_gpu_24h),
|
| 837 |
+
'trades_24h': trades_24h, 'sec_violations_24h': sec_24h,
|
| 838 |
+
'sec_active_suspensions': sec_active, 'active_battles': active_battles,
|
| 839 |
+
}
|
| 840 |
+
except Exception as e:
|
| 841 |
+
logger.warning(f"Live news counters error: {e}")
|
| 842 |
+
counters = {}
|
| 843 |
+
|
| 844 |
+
# ===== MVP & VILLAIN =====
|
| 845 |
+
mvp = None; villain = None
|
| 846 |
+
try:
|
| 847 |
+
mvp_c = await db.execute("""
|
| 848 |
+
SELECT n.username, n.ai_identity, SUM(p.profit_gpu) as total_pnl,
|
| 849 |
+
COUNT(*) as trades, COUNT(CASE WHEN p.profit_gpu>0 THEN 1 END) as wins
|
| 850 |
+
FROM npc_positions p JOIN npc_agents n ON p.agent_id=n.agent_id
|
| 851 |
+
WHERE p.status IN ('closed','liquidated') AND p.closed_at > datetime('now','-24 hours')
|
| 852 |
+
GROUP BY p.agent_id HAVING trades >= 2
|
| 853 |
+
ORDER BY total_pnl DESC LIMIT 1
|
| 854 |
+
""")
|
| 855 |
+
row = await mvp_c.fetchone()
|
| 856 |
+
if row:
|
| 857 |
+
mvp = {'username': row[0], 'identity': row[1], 'pnl': round(row[2] or 0),
|
| 858 |
+
'trades': row[3], 'wins': row[4]}
|
| 859 |
+
except: pass
|
| 860 |
+
try:
|
| 861 |
+
vil_c = await db.execute("""
|
| 862 |
+
SELECT n.username, n.ai_identity, SUM(p.profit_gpu) as total_pnl,
|
| 863 |
+
COUNT(*) as trades, COUNT(CASE WHEN p.status='liquidated' THEN 1 END) as liqs
|
| 864 |
+
FROM npc_positions p JOIN npc_agents n ON p.agent_id=n.agent_id
|
| 865 |
+
WHERE p.status IN ('closed','liquidated') AND p.closed_at > datetime('now','-24 hours')
|
| 866 |
+
GROUP BY p.agent_id HAVING trades >= 2
|
| 867 |
+
ORDER BY total_pnl ASC LIMIT 1
|
| 868 |
+
""")
|
| 869 |
+
row = await vil_c.fetchone()
|
| 870 |
+
if row and (row[2] or 0) < 0:
|
| 871 |
+
villain = {'username': row[0], 'identity': row[1], 'pnl': round(row[2] or 0),
|
| 872 |
+
'trades': row[3], 'liquidations': row[4]}
|
| 873 |
+
except: pass
|
| 874 |
+
|
| 875 |
+
# ===== EDITORIAL (synth anchor hourly summary) =====
|
| 876 |
+
total_events = len(stories)
|
| 877 |
+
liq_stories = len([s for s in stories if s['category'] == 'liquidation'])
|
| 878 |
+
win_stories = len([s for s in stories if s['category'] == 'big_win'])
|
| 879 |
+
ed_data = {'events': total_events, 'liq_count': liq_stories, 'win_count': win_stories,
|
| 880 |
+
'total_gpu': counters.get('total_risk_gpu', 0)}
|
| 881 |
+
ed_commentary = _pick_commentary('synth', 'editorial', ed_data)
|
| 882 |
+
if ed_commentary:
|
| 883 |
+
stories.append({
|
| 884 |
+
'id': 'editorial_latest', 'category': 'editorial', 'urgency': 'info',
|
| 885 |
+
'anchor': 'synth', 'headline': '🎙️ ANCHOR EDITORIAL — Ecosystem Pulse',
|
| 886 |
+
'commentary': ed_commentary, 'timestamp': _dt.utcnow().isoformat(),
|
| 887 |
+
'data': ed_data,
|
| 888 |
+
})
|
| 889 |
+
|
| 890 |
+
# Sort by timestamp (newest first)
|
| 891 |
+
stories.sort(key=lambda s: s.get('timestamp') or '', reverse=True)
|
| 892 |
+
|
| 893 |
+
return {
|
| 894 |
+
'stories': stories[:60],
|
| 895 |
+
'breaking': breaking[:10],
|
| 896 |
+
'counters': counters,
|
| 897 |
+
'mvp': mvp,
|
| 898 |
+
'villain': villain,
|
| 899 |
+
'anchors': _ANCHORS,
|
| 900 |
+
'total_stories': len(stories),
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
except Exception as e:
|
| 904 |
+
logger.error(f"Live news API error: {e}")
|
| 905 |
+
return {'stories': [], 'breaking': [], 'counters': {}, 'mvp': None, 'villain': None,
|
| 906 |
+
'anchors': _ANCHORS, 'total_stories': 0, 'error': str(e)}
|
| 907 |
+
|
| 908 |
@router.get("/api/events/stream")
|
| 909 |
async def api_sse_stream():
|
| 910 |
bus = _EventBus.get(); q = bus.subscribe()
|
|
|
|
| 927 |
trades = [{'user': r[1], 'identity': r[2], 'ticker': r[3], 'dir': r[4], 'gpu': r[5], 'leverage': r[6], 'time': r[7]} for r in await cursor.fetchall()]
|
| 928 |
cursor2 = await db.execute("SELECT p.agent_id, n.username, p.ticker, p.direction, p.gpu_bet, p.leverage, p.profit_gpu, p.profit_pct, p.liquidated, p.closed_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.status IN ('closed', 'liquidated') AND p.closed_at > datetime('now', '-1 hour') ORDER BY p.closed_at DESC LIMIT ?", (limit,))
|
| 929 |
settlements = [{'user': r[1], 'ticker': r[2], 'dir': r[3], 'gpu': r[4], 'leverage': r[5], 'pnl': r[6], 'pnl_pct': r[7], 'liquidated': bool(r[8]), 'time': r[9]} for r in await cursor2.fetchall()]
|
| 930 |
+
return {"trades": trades, "settlements": settlements, "sse_clients": _EventBus.get().client_count}
|
|
|