seawolf2357 commited on
Commit
802ad2c
·
verified ·
1 Parent(s): a4bd7a0

Update app_routes.py

Browse files
Files changed (1) hide show
  1. 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}