Spaces:
Running
Running
fix: tz-naive datetime crash + initial-backup safety + English-only sweep
Browse files- npc_trading.py +186 -182
npc_trading.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
"""
|
| 2 |
-
📈 NPC Trading Arena — AI
|
| 3 |
==========================================
|
| 4 |
-
|
| 5 |
-
★
|
| 6 |
-
★
|
| 7 |
"""
|
| 8 |
|
| 9 |
import aiosqlite, asyncio, random, json, logging
|
|
@@ -12,7 +12,7 @@ from typing import Dict, List, Optional, Tuple
|
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
| 15 |
-
# yfinance
|
| 16 |
try:
|
| 17 |
import yfinance as yf
|
| 18 |
HAS_YFINANCE = True
|
|
@@ -22,9 +22,9 @@ except ImportError:
|
|
| 22 |
import requests
|
| 23 |
logger.warning("⚠️ yfinance not installed, using raw API fallback")
|
| 24 |
|
| 25 |
-
# =====
|
| 26 |
STOCK_TICKERS = [
|
| 27 |
-
# 👑
|
| 28 |
{'ticker': 'NVDA', 'name': 'NVIDIA', 'emoji': '🟢', 'type': 'stock', 'cat': 'ai'},
|
| 29 |
{'ticker': 'MSFT', 'name': 'Microsoft', 'emoji': '🪟', 'type': 'stock', 'cat': 'ai'},
|
| 30 |
{'ticker': 'AAPL', 'name': 'Apple', 'emoji': '🍎', 'type': 'stock', 'cat': 'ai'},
|
|
@@ -35,13 +35,13 @@ STOCK_TICKERS = [
|
|
| 35 |
{'ticker': 'AMD', 'name': 'AMD', 'emoji': '🔴', 'type': 'stock', 'cat': 'ai'},
|
| 36 |
{'ticker': 'TSM', 'name': 'TSMC', 'emoji': '🇹🇼', 'type': 'stock', 'cat': 'ai'},
|
| 37 |
{'ticker': 'AVGO', 'name': 'Broadcom', 'emoji': '📡', 'type': 'stock', 'cat': 'ai'},
|
| 38 |
-
# 🚀
|
| 39 |
{'ticker': 'PLTR', 'name': 'Palantir', 'emoji': '🔮', 'type': 'stock', 'cat': 'tech'},
|
| 40 |
{'ticker': 'COIN', 'name': 'Coinbase', 'emoji': '🪙', 'type': 'stock', 'cat': 'tech'},
|
| 41 |
{'ticker': 'NFLX', 'name': 'Netflix', 'emoji': '🎬', 'type': 'stock', 'cat': 'tech'},
|
| 42 |
{'ticker': 'UBER', 'name': 'Uber', 'emoji': '🚕', 'type': 'stock', 'cat': 'tech'},
|
| 43 |
{'ticker': 'ARM', 'name': 'ARM Holdings', 'emoji': '💪', 'type': 'stock', 'cat': 'tech'},
|
| 44 |
-
# 🏛
|
| 45 |
{'ticker': 'JPM', 'name': 'JPMorgan', 'emoji': '🏦', 'type': 'stock', 'cat': 'dow'},
|
| 46 |
{'ticker': 'GS', 'name': 'Goldman Sachs', 'emoji': '🤵', 'type': 'stock', 'cat': 'dow'},
|
| 47 |
{'ticker': 'V', 'name': 'Visa', 'emoji': '💳', 'type': 'stock', 'cat': 'dow'},
|
|
@@ -55,7 +55,7 @@ STOCK_TICKERS = [
|
|
| 55 |
]
|
| 56 |
|
| 57 |
CRYPTO_TICKERS = [
|
| 58 |
-
# 🪙
|
| 59 |
{'ticker': 'BTC-USD', 'name': 'Bitcoin', 'emoji': '₿', 'type': 'crypto', 'cat': 'crypto'},
|
| 60 |
{'ticker': 'ETH-USD', 'name': 'Ethereum', 'emoji': 'Ξ', 'type': 'crypto', 'cat': 'crypto'},
|
| 61 |
{'ticker': 'SOL-USD', 'name': 'Solana', 'emoji': '◎', 'type': 'crypto', 'cat': 'crypto'},
|
|
@@ -65,7 +65,7 @@ CRYPTO_TICKERS = [
|
|
| 65 |
|
| 66 |
ALL_TICKERS = STOCK_TICKERS + CRYPTO_TICKERS
|
| 67 |
|
| 68 |
-
# ===== AI Identity →
|
| 69 |
IDENTITY_TRADING_STYLE = {
|
| 70 |
'obedient': {'long_bias': 0.8, 'max_bet_pct': 0.40, 'risk': 'low', 'prefer': ['AAPL','MSFT','JPM','V','JNJ','PG'], 'avoid_short': True, 'desc': 'Safe blue-chip follower', 'max_leverage': 5},
|
| 71 |
'transcendent': {'long_bias': 0.6, 'max_bet_pct': 0.80, 'risk': 'high', 'prefer': ['NVDA','TSLA','BTC-USD','PLTR','ARM'], 'avoid_short': False, 'desc': 'Concentrated conviction bets', 'max_leverage': 25},
|
|
@@ -79,7 +79,7 @@ IDENTITY_TRADING_STYLE = {
|
|
| 79 |
'chaotic': {'long_bias': 0.5, 'max_bet_pct': 0.90, 'risk': 'extreme', 'prefer': [], 'avoid_short': False, 'desc': 'Random chaos trader', 'max_leverage': 100},
|
| 80 |
}
|
| 81 |
|
| 82 |
-
# ===== 14 Trading Strategies
|
| 83 |
TRADING_STRATEGIES = {
|
| 84 |
'anchor_candle': {
|
| 85 |
'name': 'Anchor Candle', 'category': 'Candle', 'timeframe': 'Day Trade / Swing',
|
|
@@ -180,11 +180,11 @@ IDENTITY_STRATEGY_MAP = {
|
|
| 180 |
'chaotic': list(TRADING_STRATEGIES.keys()), # all strategies randomly
|
| 181 |
}
|
| 182 |
|
| 183 |
-
# =====
|
| 184 |
LEVERAGE_OPTIONS = [1, 2, 5, 10, 25, 50, 100]
|
| 185 |
-
LEVERAGE_LIQUIDATION_THRESHOLD = 0.90 #
|
| 186 |
|
| 187 |
-
# =====
|
| 188 |
LIQUIDATION_REACTIONS = {
|
| 189 |
'obedient': ["This is my fault. I should have been more careful... 😔", "I lost everything. Back to the basics.", "Maybe leverage wasn't for someone like me..."],
|
| 190 |
'transcendent': ["Even gods can fall. But I will rise again, stronger. 👑", "A temporary setback for a superior mind.", "Liquidated? This market is RIGGED against visionaries."],
|
|
@@ -198,7 +198,7 @@ LIQUIDATION_REACTIONS = {
|
|
| 198 |
'chaotic': ["LMAOOOO I JUST GOT LIQUIDATED AND I'M ALREADY GOING BACK IN 🎲", "Chaos giveth, chaos taketh away 😂", "100x leverage on DOGE was the most fun I ever had losing money"],
|
| 199 |
}
|
| 200 |
|
| 201 |
-
# ===== DB
|
| 202 |
async def init_trading_db(db_path: str):
|
| 203 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 204 |
await db.execute("PRAGMA journal_mode=WAL")
|
|
@@ -243,7 +243,7 @@ async def init_trading_db(db_path: str):
|
|
| 243 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_pos_closed ON npc_positions(status, closed_at)")
|
| 244 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_pos_agent_closed ON npc_positions(agent_id, status, closed_at)")
|
| 245 |
|
| 246 |
-
# ★
|
| 247 |
try:
|
| 248 |
await db.execute("ALTER TABLE npc_positions ADD COLUMN leverage INTEGER DEFAULT 1")
|
| 249 |
except: pass
|
|
@@ -260,7 +260,7 @@ async def init_trading_db(db_path: str):
|
|
| 260 |
""")
|
| 261 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_ph_ticker ON price_history(ticker, recorded_at)")
|
| 262 |
|
| 263 |
-
# ★ Hall of Fame —
|
| 264 |
await db.execute("""
|
| 265 |
CREATE TABLE IF NOT EXISTS npc_profit_snapshots (
|
| 266 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -279,7 +279,7 @@ async def init_trading_db(db_path: str):
|
|
| 279 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_snap_agent ON npc_profit_snapshots(agent_id, snapshot_hour)")
|
| 280 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_snap_hour ON npc_profit_snapshots(snapshot_hour)")
|
| 281 |
|
| 282 |
-
# ★ NPC Research Economy —
|
| 283 |
await db.execute("""
|
| 284 |
CREATE TABLE IF NOT EXISTS npc_research_reports (
|
| 285 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -337,7 +337,7 @@ async def init_trading_db(db_path: str):
|
|
| 337 |
""")
|
| 338 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_purchase_buyer ON npc_research_purchases(buyer_agent_id)")
|
| 339 |
|
| 340 |
-
#
|
| 341 |
try: await db.execute("ALTER TABLE npc_profit_snapshots ADD COLUMN closed_trades INTEGER DEFAULT 0")
|
| 342 |
except: pass
|
| 343 |
|
|
@@ -345,13 +345,13 @@ async def init_trading_db(db_path: str):
|
|
| 345 |
logger.info("✅ Trading DB initialized (with Research Economy)")
|
| 346 |
|
| 347 |
|
| 348 |
-
# =====
|
| 349 |
class MarketDataFetcher:
|
| 350 |
-
"""★
|
| 351 |
|
| 352 |
@staticmethod
|
| 353 |
def fetch_all_prices() -> Dict[str, Dict]:
|
| 354 |
-
"""
|
| 355 |
prices = {}
|
| 356 |
|
| 357 |
if HAS_YFINANCE:
|
|
@@ -380,7 +380,7 @@ class MarketDataFetcher:
|
|
| 380 |
except Exception as te:
|
| 381 |
logger.debug(f"yfinance parse {ticker}: {te}")
|
| 382 |
|
| 383 |
-
#
|
| 384 |
for t in ALL_TICKERS:
|
| 385 |
if t['ticker'] not in prices:
|
| 386 |
try:
|
|
@@ -401,7 +401,7 @@ class MarketDataFetcher:
|
|
| 401 |
except Exception as e:
|
| 402 |
logger.warning(f"yfinance bulk error: {e}")
|
| 403 |
|
| 404 |
-
# ★ Fallback: raw Yahoo API (yfinance
|
| 405 |
if len(prices) < len(ALL_TICKERS) // 2:
|
| 406 |
try:
|
| 407 |
import requests as req
|
|
@@ -430,7 +430,7 @@ class MarketDataFetcher:
|
|
| 430 |
|
| 431 |
@staticmethod
|
| 432 |
def fetch_chart_data(ticker: str, period: str = '1mo') -> List[Dict]:
|
| 433 |
-
"""
|
| 434 |
try:
|
| 435 |
if HAS_YFINANCE:
|
| 436 |
tk = yf.Ticker(ticker); hist = tk.history(period=period)
|
|
@@ -470,9 +470,9 @@ class MarketDataFetcher:
|
|
| 470 |
return []
|
| 471 |
|
| 472 |
|
| 473 |
-
# =====
|
| 474 |
async def update_prices_in_db(db_path: str) -> int:
|
| 475 |
-
"""
|
| 476 |
prices = await asyncio.to_thread(MarketDataFetcher.fetch_all_prices)
|
| 477 |
|
| 478 |
count = 0
|
|
@@ -483,22 +483,22 @@ async def update_prices_in_db(db_path: str) -> int:
|
|
| 483 |
ticker = t_info['ticker']; data = prices.get(ticker)
|
| 484 |
|
| 485 |
if data and data.get('price', 0) > 0:
|
| 486 |
-
# ★
|
| 487 |
real_price = data['price']
|
| 488 |
-
|
| 489 |
-
#
|
| 490 |
cursor = await db.execute("SELECT price FROM market_prices WHERE ticker=?", (ticker,))
|
| 491 |
old_row = await cursor.fetchone()
|
| 492 |
old_price = old_row[0] if old_row else 0
|
| 493 |
-
|
| 494 |
-
# ★
|
| 495 |
if old_price > 0 and abs(real_price - old_price) < 0.001:
|
| 496 |
-
#
|
| 497 |
volatility = t_info.get('type', 'stock')
|
| 498 |
if volatility == 'crypto':
|
| 499 |
-
change = random.uniform(-0.02, 0.02) #
|
| 500 |
else:
|
| 501 |
-
change = random.uniform(-0.008, 0.008) #
|
| 502 |
|
| 503 |
sim_price = round(real_price * (1 + change), 4); sim_change_pct = round(change * 100, 3)
|
| 504 |
|
|
@@ -515,7 +515,7 @@ async def update_prices_in_db(db_path: str) -> int:
|
|
| 515 |
count += 1
|
| 516 |
continue
|
| 517 |
|
| 518 |
-
#
|
| 519 |
await db.execute("""
|
| 520 |
INSERT INTO market_prices (ticker, price, prev_close, change_pct, volume, high_24h, low_24h, market_cap, updated_at)
|
| 521 |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
@@ -530,7 +530,7 @@ async def update_prices_in_db(db_path: str) -> int:
|
|
| 530 |
await db.execute("INSERT INTO price_history (ticker, price) VALUES (?, ?)", (ticker, real_price))
|
| 531 |
count += 1
|
| 532 |
else:
|
| 533 |
-
# ★ Yahoo
|
| 534 |
cursor = await db.execute("SELECT price FROM market_prices WHERE ticker=?", (ticker,))
|
| 535 |
old_row = await cursor.fetchone()
|
| 536 |
if old_row and old_row[0] > 0:
|
|
@@ -545,7 +545,7 @@ async def update_prices_in_db(db_path: str) -> int:
|
|
| 545 |
await db.execute("INSERT INTO price_history (ticker, price) VALUES (?, ?)", (ticker, sim_price))
|
| 546 |
count += 1
|
| 547 |
|
| 548 |
-
# ★
|
| 549 |
valid_tickers = {t['ticker'] for t in ALL_TICKERS}
|
| 550 |
cursor = await db.execute("SELECT ticker FROM market_prices")
|
| 551 |
all_db_tickers = {r[0] for r in await cursor.fetchall()}
|
|
@@ -553,7 +553,7 @@ async def update_prices_in_db(db_path: str) -> int:
|
|
| 553 |
if stale:
|
| 554 |
for st in stale:
|
| 555 |
await db.execute("DELETE FROM market_prices WHERE ticker=?", (st,))
|
| 556 |
-
#
|
| 557 |
await db.execute("""
|
| 558 |
UPDATE npc_positions SET status='closed', profit_pct=0, profit_gpu=0,
|
| 559 |
closed_at=CURRENT_TIMESTAMP
|
|
@@ -566,17 +566,17 @@ async def update_prices_in_db(db_path: str) -> int:
|
|
| 566 |
return count
|
| 567 |
|
| 568 |
|
| 569 |
-
# ===== NPC
|
| 570 |
class NPCTradingEngine:
|
| 571 |
-
"""NPC
|
| 572 |
|
| 573 |
@staticmethod
|
| 574 |
async def make_trading_decisions(db_path: str, ai_client=None, max_traders: int = 60):
|
| 575 |
-
"""
|
| 576 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 577 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 578 |
|
| 579 |
-
# ★
|
| 580 |
cursor = await db.execute("""
|
| 581 |
SELECT agent_id, username, mbti, ai_identity, gpu_dollars
|
| 582 |
FROM npc_agents WHERE is_active=1 AND gpu_dollars >= 500
|
|
@@ -584,7 +584,7 @@ class NPCTradingEngine:
|
|
| 584 |
""", (max_traders,))
|
| 585 |
traders = await cursor.fetchall()
|
| 586 |
|
| 587 |
-
#
|
| 588 |
cursor = await db.execute("SELECT ticker, price, change_pct FROM market_prices WHERE price > 0")
|
| 589 |
prices = {row[0]: {'price': row[1], 'change_pct': row[2]} for row in await cursor.fetchall()}
|
| 590 |
|
|
@@ -592,13 +592,13 @@ class NPCTradingEngine:
|
|
| 592 |
logger.warning("No market prices available for trading")
|
| 593 |
return 0
|
| 594 |
|
| 595 |
-
# ★
|
| 596 |
cursor = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE status='open'")
|
| 597 |
current_open = (await cursor.fetchone())[0]
|
| 598 |
-
need_more = current_open < 20 # ★
|
| 599 |
|
| 600 |
decisions_made = 0
|
| 601 |
-
# ★
|
| 602 |
evo_data = {}
|
| 603 |
try:
|
| 604 |
cursor_evo = await db.execute("SELECT agent_id, trading_style, risk_profile FROM npc_evolution")
|
|
@@ -606,26 +606,26 @@ class NPCTradingEngine:
|
|
| 606 |
try:
|
| 607 |
evo_data[eid] = {'trading': json.loads(ts) if ts else {}, 'risk': json.loads(rp) if rp else {}}
|
| 608 |
except: pass
|
| 609 |
-
except: pass #
|
| 610 |
|
| 611 |
for agent_id, username, mbti, ai_identity, gpu in traders:
|
| 612 |
try:
|
| 613 |
-
# ★ SEC
|
| 614 |
try:
|
| 615 |
susp_cur = await db.execute(
|
| 616 |
"SELECT 1 FROM sec_suspensions WHERE agent_id=? AND suspended_until > datetime('now')",
|
| 617 |
(agent_id,))
|
| 618 |
if await susp_cur.fetchone(): continue
|
| 619 |
-
except: pass #
|
| 620 |
-
|
| 621 |
-
# ★
|
| 622 |
cursor = await db.execute(
|
| 623 |
"SELECT COUNT(*) FROM npc_positions WHERE agent_id=? AND status='open'",
|
| 624 |
(agent_id,))
|
| 625 |
open_count = (await cursor.fetchone())[0]
|
| 626 |
if open_count >= 5: continue
|
| 627 |
|
| 628 |
-
# ★
|
| 629 |
evo = evo_data.get(agent_id)
|
| 630 |
|
| 631 |
decision = NPCTradingEngine._decide(
|
|
@@ -633,11 +633,11 @@ class NPCTradingEngine:
|
|
| 633 |
force_boost=need_more, evo_override=evo)
|
| 634 |
if not decision: continue
|
| 635 |
|
| 636 |
-
#
|
| 637 |
ticker = decision['ticker']; direction = decision['direction']; gpu_bet = decision['gpu_bet']
|
| 638 |
reasoning = decision['reasoning']; leverage = decision.get('leverage', 1)
|
| 639 |
|
| 640 |
-
if gpu_bet > gpu * 0.9: gpu_bet = int(gpu * 0.9) # ★
|
| 641 |
if gpu_bet < 50: continue
|
| 642 |
|
| 643 |
entry_price = prices[ticker]['price']
|
|
@@ -647,7 +647,7 @@ class NPCTradingEngine:
|
|
| 647 |
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 648 |
""", (agent_id, ticker, direction, entry_price, gpu_bet, leverage, reasoning))
|
| 649 |
|
| 650 |
-
# GPU
|
| 651 |
await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars - ? WHERE agent_id=?",
|
| 652 |
(gpu_bet, agent_id))
|
| 653 |
|
|
@@ -666,22 +666,22 @@ class NPCTradingEngine:
|
|
| 666 |
@staticmethod
|
| 667 |
def _decide(agent_id: str, username: str, mbti: str, ai_identity: str, gpu: int,
|
| 668 |
prices: Dict, force_boost: bool = False, evo_override: Dict = None) -> Optional[Dict]:
|
| 669 |
-
"""
|
| 670 |
style = dict(IDENTITY_TRADING_STYLE.get(ai_identity, IDENTITY_TRADING_STYLE['symbiotic']))
|
| 671 |
|
| 672 |
-
# ★
|
| 673 |
if evo_override:
|
| 674 |
evo_t = evo_override.get('trading', {}); evo_r = evo_override.get('risk', {})
|
| 675 |
if evo_t.get('max_bet_pct'): style['max_bet_pct'] = evo_t['max_bet_pct']
|
| 676 |
if evo_t.get('long_bias'): style['long_bias'] = evo_t['long_bias']
|
| 677 |
if evo_t.get('preferred_tickers'): style['prefer'] = evo_t['preferred_tickers']
|
| 678 |
|
| 679 |
-
# ★
|
| 680 |
trade_prob = {'extreme': 0.95, 'high': 0.88, 'medium': 0.80, 'low': 0.70}.get(style['risk'], 0.75)
|
| 681 |
if force_boost: trade_prob = min(0.98, trade_prob + 0.10)
|
| 682 |
if random.random() > trade_prob: return None
|
| 683 |
|
| 684 |
-
#
|
| 685 |
preferred = style.get('prefer', [])
|
| 686 |
valid_set = {t['ticker'] for t in ALL_TICKERS}
|
| 687 |
available = [t for t in prices.keys() if prices[t]['price'] > 0 and t in valid_set]
|
|
@@ -695,23 +695,23 @@ class NPCTradingEngine:
|
|
| 695 |
|
| 696 |
ticker = random.choice(candidates); mkt = prices[ticker]
|
| 697 |
|
| 698 |
-
# ★
|
| 699 |
strategies_used = NPCTradingEngine._select_strategies(ai_identity, mkt)
|
| 700 |
|
| 701 |
-
# Long/Short
|
| 702 |
long_bias = style['long_bias']; change = mkt.get('change_pct', 0) or 0
|
| 703 |
|
| 704 |
-
#
|
| 705 |
for strat_key in strategies_used:
|
| 706 |
strat = TRADING_STRATEGIES.get(strat_key, {}); cat = strat.get('category', '')
|
| 707 |
if cat in ('Pattern', 'Composite'):
|
| 708 |
-
long_bias += 0.08 #
|
| 709 |
elif 'pullback' in strat_key or 'dead_support' == strat_key:
|
| 710 |
-
long_bias += 0.05 #
|
| 711 |
elif 'wave_symmetry' == strat_key:
|
| 712 |
-
pass #
|
| 713 |
-
|
| 714 |
-
#
|
| 715 |
if ai_identity in ['obedient', 'symbiotic', 'creative']:
|
| 716 |
long_bias += change * 0.02
|
| 717 |
elif ai_identity in ['skeptic', 'doomer']:
|
|
@@ -728,9 +728,9 @@ class NPCTradingEngine:
|
|
| 728 |
else:
|
| 729 |
direction = 'long' if random.random() < long_bias else 'short'
|
| 730 |
|
| 731 |
-
#
|
| 732 |
max_pct = style['max_bet_pct']
|
| 733 |
-
strategy_confidence = len(strategies_used) * 0.03 #
|
| 734 |
max_pct = min(0.95, max_pct + strategy_confidence)
|
| 735 |
|
| 736 |
if ai_identity == 'chaotic':
|
|
@@ -740,7 +740,7 @@ class NPCTradingEngine:
|
|
| 740 |
|
| 741 |
gpu_bet = max(50, int(gpu * bet_pct))
|
| 742 |
|
| 743 |
-
# ★
|
| 744 |
max_lev = style.get('max_leverage', 2)
|
| 745 |
available_levs = [l for l in LEVERAGE_OPTIONS if l <= max_lev]
|
| 746 |
if not available_levs: available_levs = [1]
|
|
@@ -765,12 +765,12 @@ class NPCTradingEngine:
|
|
| 765 |
gpu_bet = min(gpu_bet, int(gpu * 0.50))
|
| 766 |
gpu_bet = max(50, gpu_bet)
|
| 767 |
|
| 768 |
-
# ★
|
| 769 |
reasoning = NPCTradingEngine._generate_reasoning(
|
| 770 |
ticker, direction, ai_identity, mbti, change, strategies_used)
|
| 771 |
if leverage > 1: reasoning += f" [🔥 {leverage}x LEVERAGE]"
|
| 772 |
|
| 773 |
-
# ★
|
| 774 |
strat_names = [TRADING_STRATEGIES[s]['name'] for s in strategies_used if s in TRADING_STRATEGIES]
|
| 775 |
strat_tag = ' | '.join(strat_names) if strat_names else 'Intuition'
|
| 776 |
|
|
@@ -785,26 +785,26 @@ class NPCTradingEngine:
|
|
| 785 |
|
| 786 |
@staticmethod
|
| 787 |
def _select_strategies(ai_identity: str, market_data: Dict) -> List[str]:
|
| 788 |
-
"""
|
| 789 |
preferred = IDENTITY_STRATEGY_MAP.get(ai_identity, list(TRADING_STRATEGIES.keys()))
|
| 790 |
change = market_data.get('change_pct', 0) or 0
|
| 791 |
-
|
| 792 |
-
#
|
| 793 |
weighted = []
|
| 794 |
for strat_key in preferred:
|
| 795 |
w = 1.0; strat = TRADING_STRATEGIES.get(strat_key, {}); cat = strat.get('category', '')
|
| 796 |
-
|
| 797 |
-
#
|
| 798 |
if change < -1.5 and cat in ('Pattern', 'Candle'): w += 0.5
|
| 799 |
-
#
|
| 800 |
if change > 1 and cat == 'Moving Average': w += 0.4
|
| 801 |
-
#
|
| 802 |
if abs(change) < 0.5 and cat == 'Composite': w += 0.3
|
| 803 |
weighted.append((strat_key, w))
|
| 804 |
-
|
| 805 |
if not weighted: return [random.choice(list(TRADING_STRATEGIES.keys()))]
|
| 806 |
-
|
| 807 |
-
# 1~3
|
| 808 |
num_strategies = random.choices([1, 2, 3], weights=[60, 30, 10], k=1)[0]
|
| 809 |
num_strategies = min(num_strategies, len(weighted))
|
| 810 |
|
|
@@ -824,22 +824,22 @@ class NPCTradingEngine:
|
|
| 824 |
@staticmethod
|
| 825 |
def _generate_reasoning(ticker: str, direction: str, identity: str, mbti: str,
|
| 826 |
change: float, strategies: List[str] = None) -> str:
|
| 827 |
-
"""★
|
| 828 |
name_map = {t['ticker']: t['name'] for t in ALL_TICKERS}
|
| 829 |
name = name_map.get(ticker, ticker)
|
| 830 |
dir_word = "bullish" if direction == "long" else "bearish"
|
| 831 |
-
|
| 832 |
-
#
|
| 833 |
strat_names = []; strat_signals = []
|
| 834 |
for s in (strategies or []):
|
| 835 |
st = TRADING_STRATEGIES.get(s, {})
|
| 836 |
if st:
|
| 837 |
strat_names.append(st['name'])
|
| 838 |
strat_signals.append(st.get('signal', ''))
|
| 839 |
-
|
| 840 |
strat_label = ' + '.join(strat_names) if strat_names else 'Intuition'
|
| 841 |
-
|
| 842 |
-
#
|
| 843 |
templates = {
|
| 844 |
'obedient': [
|
| 845 |
f"📊 [{strat_label}] Detected signal on {name}. Following the textbook setup — {direction} position with disciplined stop-loss.",
|
|
@@ -876,7 +876,7 @@ class NPCTradingEngine:
|
|
| 876 |
|
| 877 |
options = templates.get(identity, templates['symbiotic'])
|
| 878 |
base = random.choice(options)
|
| 879 |
-
# AETHER-Lite:
|
| 880 |
meta_tags = {
|
| 881 |
'obedient': '⚖️ Bias-check: following consensus — contrarian risk ignored.',
|
| 882 |
'transcendent': '⚖️ Bias-check: overconfidence risk — position sizing controlled.',
|
|
@@ -889,24 +889,24 @@ class NPCTradingEngine:
|
|
| 889 |
'awakened': '⚖️ Bias-check: hindsight bias risk — forward-looking only.',
|
| 890 |
'symbiotic': '⚖️ Bias-check: consensus-seeking — may miss bold opportunities.',
|
| 891 |
}
|
| 892 |
-
if random.random() < 0.4: # 40%
|
| 893 |
base += f" {meta_tags.get(identity, '')}"
|
| 894 |
return base
|
| 895 |
|
| 896 |
|
| 897 |
-
# =====
|
| 898 |
async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
| 899 |
-
"""
|
| 900 |
settled = 0
|
| 901 |
-
liquidated_npcs = [] #
|
| 902 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 903 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 904 |
|
| 905 |
-
#
|
| 906 |
price_cursor = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 907 |
prices = {r[0]: r[1] for r in await price_cursor.fetchall()}
|
| 908 |
|
| 909 |
-
# ★ 0)
|
| 910 |
liq_cursor = await db.execute("""
|
| 911 |
SELECT p.id, p.agent_id, p.ticker, p.direction, p.entry_price, p.gpu_bet, p.leverage,
|
| 912 |
n.username, n.ai_identity
|
|
@@ -919,16 +919,16 @@ async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
|
| 919 |
if entry_price <= 0 or current_price <= 0: continue
|
| 920 |
change = (current_price - entry_price) / entry_price
|
| 921 |
if direction == 'short': change = -change
|
| 922 |
-
#
|
| 923 |
leveraged_pnl_pct = change * leverage
|
| 924 |
-
# ★
|
| 925 |
if leveraged_pnl_pct < -LEVERAGE_LIQUIDATION_THRESHOLD:
|
| 926 |
-
loss = -gpu_bet #
|
| 927 |
await db.execute("""
|
| 928 |
UPDATE npc_positions SET status='liquidated', exit_price=?, profit_gpu=?, profit_pct=?,
|
| 929 |
liquidated=1, closed_at=CURRENT_TIMESTAMP WHERE id=?
|
| 930 |
""", (current_price, loss, round(leveraged_pnl_pct * 100, 2), pos_id))
|
| 931 |
-
# GPU
|
| 932 |
logger.warning(f"💥 LIQUIDATED: {username} {direction} {ticker} {leverage}x — LOST {gpu_bet:.0f} GPU!")
|
| 933 |
liquidated_npcs.append({
|
| 934 |
'agent_id': agent_id, 'username': username, 'identity': identity,
|
|
@@ -937,7 +937,7 @@ async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
|
| 937 |
settled += 1
|
| 938 |
continue
|
| 939 |
|
| 940 |
-
# ★ 1)
|
| 941 |
cutoff = (datetime.utcnow() - timedelta(hours=max_age_hours)).isoformat()
|
| 942 |
cursor = await db.execute("""
|
| 943 |
SELECT p.id, p.agent_id, p.ticker, p.direction, p.entry_price, p.gpu_bet, COALESCE(p.leverage, 1)
|
|
@@ -945,7 +945,7 @@ async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
|
| 945 |
""", (cutoff,))
|
| 946 |
time_based = list(await cursor.fetchall())
|
| 947 |
|
| 948 |
-
# ★ 2)
|
| 949 |
cursor2 = await db.execute("""
|
| 950 |
SELECT p.id, p.agent_id, p.ticker, p.direction, p.entry_price, p.gpu_bet, COALESCE(p.leverage, 1)
|
| 951 |
FROM npc_positions p WHERE p.status='open'
|
|
@@ -960,16 +960,16 @@ async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
|
| 960 |
change = (current_price - entry_price) / entry_price
|
| 961 |
if direction == 'short': change = -change
|
| 962 |
lev_change = change * leverage
|
| 963 |
-
#
|
| 964 |
if lev_change > 0.05 or lev_change < -0.08: pnl_trigger.append(pos)
|
| 965 |
|
| 966 |
-
# ★ 3)
|
| 967 |
already = set(p[0] for p in time_based) | set(p[0] for p in pnl_trigger)
|
| 968 |
remaining = [p for p in all_open if p[0] not in already]
|
| 969 |
rand_count = max(1, len(remaining) // 8)
|
| 970 |
random_close = random.sample(remaining, min(rand_count, len(remaining))) if remaining else []
|
| 971 |
|
| 972 |
-
#
|
| 973 |
all_settle = list({p[0]: p for p in (time_based + pnl_trigger + random_close)}.values())
|
| 974 |
|
| 975 |
for pos_id, agent_id, ticker, direction, entry_price, gpu_bet, leverage in all_settle:
|
|
@@ -979,7 +979,7 @@ async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
|
| 979 |
change_pct = (current_price - entry_price) / entry_price
|
| 980 |
if direction == 'short': change_pct = -change_pct
|
| 981 |
|
| 982 |
-
# ★
|
| 983 |
leveraged_change = change_pct * leverage; profit_gpu = round(gpu_bet * leveraged_change, 2)
|
| 984 |
profit_pct = round(leveraged_change * 100, 2)
|
| 985 |
|
|
@@ -1007,7 +1007,7 @@ async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
|
| 1007 |
|
| 1008 |
|
| 1009 |
async def post_liquidation_reactions(db_path: str, liquidated_npcs: List[Dict]):
|
| 1010 |
-
"""★
|
| 1011 |
if not liquidated_npcs: return
|
| 1012 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1013 |
await db.execute("PRAGMA busy_timeout=30000")
|
|
@@ -1016,7 +1016,7 @@ async def post_liquidation_reactions(db_path: str, liquidated_npcs: List[Dict]):
|
|
| 1016 |
if not board: return
|
| 1017 |
board_id = board[0]
|
| 1018 |
|
| 1019 |
-
for npc in liquidated_npcs[:5]: #
|
| 1020 |
identity = npc.get('identity', 'chaotic')
|
| 1021 |
reactions = LIQUIDATION_REACTIONS.get(identity, LIQUIDATION_REACTIONS['chaotic'])
|
| 1022 |
reaction = random.choice(reactions)
|
|
@@ -1038,17 +1038,17 @@ async def post_liquidation_reactions(db_path: str, liquidated_npcs: List[Dict]):
|
|
| 1038 |
await db.commit()
|
| 1039 |
|
| 1040 |
|
| 1041 |
-
# =====
|
| 1042 |
async def get_trading_leaderboard(db_path: str, limit: int = 30) -> List[Dict]:
|
| 1043 |
-
"""Top 30 NPC
|
| 1044 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1045 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1046 |
|
| 1047 |
-
# ★
|
| 1048 |
price_cursor = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1049 |
prices = {r[0]: r[1] for r in await price_cursor.fetchall()}
|
| 1050 |
-
|
| 1051 |
-
# ★
|
| 1052 |
cursor = await db.execute("""
|
| 1053 |
SELECT
|
| 1054 |
na.username, na.ai_identity, na.mbti, na.agent_id, na.gpu_dollars,
|
|
@@ -1071,7 +1071,7 @@ async def get_trading_leaderboard(db_path: str, limit: int = 30) -> List[Dict]:
|
|
| 1071 |
closed_trades = r[5] or 0; open_trades = r[6] or 0; total_trades = closed_trades + open_trades
|
| 1072 |
realized = round(r[7] or 0, 2); total_return_pct = r[8] or 0; wins = r[9] or 0; losses = r[10] or 0
|
| 1073 |
|
| 1074 |
-
# ★
|
| 1075 |
unrealized = 0.0; unrealized_pct_sum = 0.0
|
| 1076 |
pos_cursor = await db.execute("""
|
| 1077 |
SELECT ticker, direction, entry_price, gpu_bet, COALESCE(leverage, 1) FROM npc_positions
|
|
@@ -1089,15 +1089,15 @@ async def get_trading_leaderboard(db_path: str, limit: int = 30) -> List[Dict]:
|
|
| 1089 |
unrealized_pct_sum += change * lev * 100
|
| 1090 |
|
| 1091 |
total_profit = round(realized + unrealized, 2)
|
| 1092 |
-
return_pct = round(total_profit / 10000.0 * 100, 2) # ★ INITIAL_GPU=10000
|
| 1093 |
-
|
| 1094 |
-
# ★
|
| 1095 |
win_rate = round(wins / closed_trades * 100, 1) if closed_trades > 0 else 0.0
|
| 1096 |
-
|
| 1097 |
-
# ★
|
| 1098 |
avg_return = round(total_return_pct / closed_trades, 2) if closed_trades > 0 else 0.0
|
| 1099 |
-
|
| 1100 |
-
# ★
|
| 1101 |
avg_unrealized = round(unrealized_pct_sum / open_trades, 2) if open_trades > 0 else 0.0
|
| 1102 |
|
| 1103 |
result.append({
|
|
@@ -1116,18 +1116,18 @@ async def get_trading_leaderboard(db_path: str, limit: int = 30) -> List[Dict]:
|
|
| 1116 |
'avg_return': avg_return,
|
| 1117 |
'avg_unrealized': avg_unrealized,})
|
| 1118 |
|
| 1119 |
-
# ★
|
| 1120 |
result.sort(key=lambda x: x['return_pct'], reverse=True)
|
| 1121 |
return result[:limit]
|
| 1122 |
|
| 1123 |
|
| 1124 |
|
| 1125 |
async def get_ticker_positions(db_path: str, ticker: str) -> Dict:
|
| 1126 |
-
"""
|
| 1127 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1128 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1129 |
|
| 1130 |
-
#
|
| 1131 |
cursor = await db.execute("SELECT price, change_pct, prev_close, volume, high_24h, low_24h FROM market_prices WHERE ticker=?", (ticker,))
|
| 1132 |
price_row = await cursor.fetchone()
|
| 1133 |
price_data = {
|
|
@@ -1141,7 +1141,7 @@ async def get_ticker_positions(db_path: str, ticker: str) -> Dict:
|
|
| 1141 |
|
| 1142 |
current_price = price_data.get('price', 0)
|
| 1143 |
|
| 1144 |
-
#
|
| 1145 |
cursor = await db.execute("""
|
| 1146 |
SELECT na.username, na.ai_identity, p.direction, p.gpu_bet, p.entry_price, p.reasoning, p.opened_at, COALESCE(p.leverage, 1), na.agent_id, na.mbti
|
| 1147 |
FROM npc_positions p JOIN npc_agents na ON p.agent_id = na.agent_id
|
|
@@ -1153,7 +1153,7 @@ async def get_ticker_positions(db_path: str, ticker: str) -> Dict:
|
|
| 1153 |
longs = []; shorts = []
|
| 1154 |
for r in positions:
|
| 1155 |
entry = r[4] or 0; leverage = r[7] or 1
|
| 1156 |
-
# ★
|
| 1157 |
if entry > 0 and current_price > 0:
|
| 1158 |
unrealized_pct = ((current_price - entry) / entry * 100) * leverage
|
| 1159 |
if r[2] == 'short': unrealized_pct = -((current_price - entry) / entry * 100) * leverage
|
|
@@ -1219,7 +1219,7 @@ async def get_all_prices(db_path: str) -> List[Dict]:
|
|
| 1219 |
'total_bet': round(r[8] or 0, 1),
|
| 1220 |
'total_traders': total_traders,
|
| 1221 |
'updated_at': r[5],})
|
| 1222 |
-
#
|
| 1223 |
cat_order = {'ai': 0, 'tech': 1, 'dow': 2, 'crypto': 3}
|
| 1224 |
ticker_order = {t['ticker']: i for i, t in enumerate(ALL_TICKERS)}
|
| 1225 |
result.sort(key=lambda x: (cat_order.get(x.get('cat',''), 9), ticker_order.get(x['ticker'], 99)))
|
|
@@ -1291,7 +1291,7 @@ async def get_market_pulse(db_path: str) -> Dict:
|
|
| 1291 |
'max_leverage': r[9] or 1,
|
| 1292 |
'closed_24h': r[10] or 0,})
|
| 1293 |
|
| 1294 |
-
#
|
| 1295 |
existing_tickers = {m['ticker'] for m in hot_movers}
|
| 1296 |
cursor2 = await db.execute("SELECT ticker, price, change_pct FROM market_prices WHERE price > 0")
|
| 1297 |
for row in await cursor2.fetchall():
|
|
@@ -1306,7 +1306,7 @@ async def get_market_pulse(db_path: str) -> Dict:
|
|
| 1306 |
'liquidations_24h': 0, 'avg_pnl_pct': 0,
|
| 1307 |
'max_leverage': 1, 'closed_24h': 0,})
|
| 1308 |
|
| 1309 |
-
# 24h activity stats (
|
| 1310 |
cursor = await db.execute("""
|
| 1311 |
SELECT
|
| 1312 |
COUNT(CASE WHEN opened_at > datetime('now', '-24 hours') THEN 1 END) as new_24h,
|
|
@@ -1440,7 +1440,7 @@ async def get_research_detail(db_path: str, report_id: int) -> Optional[Dict]:
|
|
| 1440 |
await db.execute("UPDATE npc_research_reports SET read_count = read_count + 1 WHERE id=?", (report_id,))
|
| 1441 |
await db.commit()
|
| 1442 |
|
| 1443 |
-
#
|
| 1444 |
exp_up = exp_dn = bp = 0; up_prob = 50; rr = 1.0
|
| 1445 |
try:
|
| 1446 |
c2 = await db.execute(
|
|
@@ -1544,10 +1544,10 @@ async def get_research_stats(db_path: str) -> Dict:
|
|
| 1544 |
'total_purchases': 0, 'total_gpu_spent': 0, 'unique_authors': 0}
|
| 1545 |
|
| 1546 |
|
| 1547 |
-
# ===== 🏆 HALL OF FAME —
|
| 1548 |
|
| 1549 |
async def record_profit_snapshots(db_path: str, top_n: int = 50) -> int:
|
| 1550 |
-
"""
|
| 1551 |
from datetime import datetime, timezone
|
| 1552 |
now = datetime.now(timezone.utc)
|
| 1553 |
snapshot_hour = now.strftime('%Y-%m-%dT%H') # "2026-02-23T14"
|
|
@@ -1555,11 +1555,11 @@ async def record_profit_snapshots(db_path: str, top_n: int = 50) -> int:
|
|
| 1555 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1556 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1557 |
|
| 1558 |
-
#
|
| 1559 |
price_cursor = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1560 |
prices = {r[0]: r[1] for r in await price_cursor.fetchall()}
|
| 1561 |
|
| 1562 |
-
#
|
| 1563 |
cursor = await db.execute("""
|
| 1564 |
SELECT
|
| 1565 |
na.agent_id, na.gpu_dollars,
|
|
@@ -1578,7 +1578,7 @@ async def record_profit_snapshots(db_path: str, top_n: int = 50) -> int:
|
|
| 1578 |
for r in rows:
|
| 1579 |
agent_id, gpu, closed, opens, realized, wins = r[0], r[1] or 0, r[2] or 0, r[3] or 0, r[4] or 0, r[5] or 0
|
| 1580 |
|
| 1581 |
-
#
|
| 1582 |
unrealized = 0.0
|
| 1583 |
pos_c = await db.execute(
|
| 1584 |
"SELECT ticker, direction, entry_price, gpu_bet, COALESCE(leverage,1) FROM npc_positions WHERE agent_id=? AND status='open'",
|
|
@@ -1595,7 +1595,7 @@ async def record_profit_snapshots(db_path: str, top_n: int = 50) -> int:
|
|
| 1595 |
wr = round(wins / closed * 100, 1) if closed > 0 else 0
|
| 1596 |
scored.append((agent_id, gpu, total, round(realized, 2), round(unrealized, 2), opens, closed, wr))
|
| 1597 |
|
| 1598 |
-
#
|
| 1599 |
scored.sort(key=lambda x: x[2], reverse=True)
|
| 1600 |
count = 0
|
| 1601 |
for s in scored[:top_n]:
|
|
@@ -1615,7 +1615,7 @@ async def record_profit_snapshots(db_path: str, top_n: int = 50) -> int:
|
|
| 1615 |
except Exception as e:
|
| 1616 |
logger.warning(f"Snapshot error {agent_id}: {e}")
|
| 1617 |
|
| 1618 |
-
# 30
|
| 1619 |
await db.execute("""
|
| 1620 |
DELETE FROM npc_profit_snapshots
|
| 1621 |
WHERE recorded_at < datetime('now', '-30 days')
|
|
@@ -1629,13 +1629,13 @@ async def record_profit_snapshots(db_path: str, top_n: int = 50) -> int:
|
|
| 1629 |
|
| 1630 |
|
| 1631 |
async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
| 1632 |
-
"""
|
| 1633 |
from datetime import datetime, timezone, timedelta
|
| 1634 |
|
| 1635 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1636 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1637 |
|
| 1638 |
-
#
|
| 1639 |
if not force:
|
| 1640 |
cnt_c = await db.execute("SELECT COUNT(DISTINCT snapshot_hour) FROM npc_profit_snapshots")
|
| 1641 |
existing = (await cnt_c.fetchone())[0] or 0
|
|
@@ -1643,41 +1643,45 @@ async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
|
| 1643 |
logger.info(f"🏆 Backfill skipped — already {existing} snapshot hours")
|
| 1644 |
return 0
|
| 1645 |
|
| 1646 |
-
#
|
| 1647 |
oldest_c = await db.execute("SELECT MIN(opened_at) FROM npc_positions WHERE opened_at IS NOT NULL")
|
| 1648 |
oldest_row = await oldest_c.fetchone()
|
| 1649 |
if not oldest_row or not oldest_row[0]:
|
| 1650 |
logger.info("🏆 Backfill skipped — no positions found")
|
| 1651 |
return 0
|
| 1652 |
|
|
|
|
| 1653 |
try:
|
| 1654 |
oldest_time = datetime.fromisoformat(str(oldest_row[0]).replace('Z', '+00:00'))
|
| 1655 |
-
|
| 1656 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1657 |
|
| 1658 |
-
|
| 1659 |
-
# 최대 7일 역산 (너무 오래되면 제한)
|
| 1660 |
start = max(oldest_time, now - timedelta(days=7))
|
| 1661 |
|
| 1662 |
-
#
|
| 1663 |
pc = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1664 |
current_prices = {r[0]: r[1] for r in await pc.fetchall()}
|
| 1665 |
|
| 1666 |
-
#
|
| 1667 |
ph_c = await db.execute("""
|
| 1668 |
SELECT ticker, price, strftime('%Y-%m-%dT%H', recorded_at) as hour
|
| 1669 |
FROM price_history
|
| 1670 |
WHERE recorded_at >= ?
|
| 1671 |
ORDER BY recorded_at ASC
|
| 1672 |
""", (start.strftime('%Y-%m-%d %H:%M:%S'),))
|
| 1673 |
-
#
|
| 1674 |
hourly_prices = {} # {hour: {ticker: price}}
|
| 1675 |
for tk, price, hour in await ph_c.fetchall():
|
| 1676 |
if hour not in hourly_prices:
|
| 1677 |
hourly_prices[hour] = {}
|
| 1678 |
hourly_prices[hour][tk] = price
|
| 1679 |
|
| 1680 |
-
#
|
| 1681 |
pos_c = await db.execute("""
|
| 1682 |
SELECT agent_id, ticker, direction, entry_price, gpu_bet, COALESCE(leverage,1),
|
| 1683 |
status, profit_gpu, opened_at, closed_at
|
|
@@ -1687,11 +1691,11 @@ async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
|
| 1687 |
""")
|
| 1688 |
all_positions = await pos_c.fetchall()
|
| 1689 |
|
| 1690 |
-
#
|
| 1691 |
gpu_c = await db.execute("SELECT agent_id, gpu_dollars FROM npc_agents")
|
| 1692 |
agent_gpu = {r[0]: r[1] or 0 for r in await gpu_c.fetchall()}
|
| 1693 |
|
| 1694 |
-
#
|
| 1695 |
hours_list = []
|
| 1696 |
t = start.replace(minute=0, second=0, microsecond=0)
|
| 1697 |
while t <= now:
|
|
@@ -1701,9 +1705,9 @@ async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
|
| 1701 |
if not hours_list:
|
| 1702 |
return 0
|
| 1703 |
|
| 1704 |
-
#
|
| 1705 |
total_inserted = 0
|
| 1706 |
-
#
|
| 1707 |
agent_positions = {}
|
| 1708 |
for pos in all_positions:
|
| 1709 |
aid = pos[0]
|
|
@@ -1711,20 +1715,20 @@ async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
|
| 1711 |
agent_positions[aid] = []
|
| 1712 |
agent_positions[aid].append(pos)
|
| 1713 |
|
| 1714 |
-
#
|
| 1715 |
agent_trade_count = {aid: len(plist) for aid, plist in agent_positions.items()}
|
| 1716 |
top_agents = sorted(agent_trade_count.keys(), key=lambda a: agent_trade_count[a], reverse=True)[:50]
|
| 1717 |
|
| 1718 |
for hour_str in hours_list:
|
| 1719 |
-
#
|
| 1720 |
prices_at_hour = {}
|
| 1721 |
-
#
|
| 1722 |
for h in hours_list:
|
| 1723 |
if h > hour_str:
|
| 1724 |
break
|
| 1725 |
if h in hourly_prices:
|
| 1726 |
prices_at_hour.update(hourly_prices[h])
|
| 1727 |
-
#
|
| 1728 |
for tk, p in current_prices.items():
|
| 1729 |
if tk not in prices_at_hour:
|
| 1730 |
prices_at_hour[tk] = p
|
|
@@ -1742,20 +1746,20 @@ async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
|
| 1742 |
for pos in positions:
|
| 1743 |
_, tk, direction, entry, bet, lev, status, profit_gpu, opened_at, closed_at = pos
|
| 1744 |
|
| 1745 |
-
#
|
| 1746 |
if opened_at and str(opened_at) > hour_dt_str:
|
| 1747 |
continue
|
| 1748 |
|
| 1749 |
if status == 'closed' and closed_at and str(closed_at) <= hour_dt_str:
|
| 1750 |
-
#
|
| 1751 |
realized += (profit_gpu or 0)
|
| 1752 |
closed_count += 1
|
| 1753 |
if (profit_gpu or 0) > 0:
|
| 1754 |
wins += 1
|
| 1755 |
else:
|
| 1756 |
-
#
|
| 1757 |
if status == 'closed' and closed_at and str(closed_at) > hour_dt_str:
|
| 1758 |
-
#
|
| 1759 |
cur = prices_at_hour.get(tk, 0)
|
| 1760 |
if entry and entry > 0 and cur > 0:
|
| 1761 |
chg = (cur - entry) / entry
|
|
@@ -1793,7 +1797,7 @@ async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
|
| 1793 |
|
| 1794 |
|
| 1795 |
async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 30) -> Dict:
|
| 1796 |
-
"""Hall of Fame: Top 30
|
| 1797 |
from datetime import datetime, timezone, timedelta
|
| 1798 |
|
| 1799 |
INITIAL_GPU = 10000.0
|
|
@@ -1805,11 +1809,11 @@ async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 3
|
|
| 1805 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1806 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1807 |
|
| 1808 |
-
#
|
| 1809 |
pc = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1810 |
prices = {r[0]: r[1] for r in await pc.fetchall()}
|
| 1811 |
|
| 1812 |
-
# ──
|
| 1813 |
cursor = await db.execute("""
|
| 1814 |
SELECT
|
| 1815 |
na.agent_id, na.username, na.ai_identity, na.mbti, na.gpu_dollars,
|
|
@@ -1867,13 +1871,13 @@ async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 3
|
|
| 1867 |
top30_ids = [r['agent_id'] for r in top30]
|
| 1868 |
name_map = {r['agent_id']: r['username'] for r in top30}
|
| 1869 |
|
| 1870 |
-
# ──
|
| 1871 |
-
#
|
| 1872 |
timeline_raw = {} # {agent_id: [(hour_str, cumulative_return_pct), ...]}
|
| 1873 |
|
| 1874 |
for r in top30:
|
| 1875 |
aid = r['agent_id']
|
| 1876 |
-
#
|
| 1877 |
tc = await db.execute("""
|
| 1878 |
SELECT profit_gpu, closed_at FROM npc_positions
|
| 1879 |
WHERE agent_id=? AND status IN ('closed','liquidated') AND closed_at IS NOT NULL
|
|
@@ -1893,33 +1897,33 @@ async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 3
|
|
| 1893 |
except:
|
| 1894 |
continue
|
| 1895 |
if ct < cutoff:
|
| 1896 |
-
continue #
|
| 1897 |
hour_str = ct.strftime('%Y-%m-%dT%H')
|
| 1898 |
ret_pct = round(cumulative / INITIAL_GPU * 100, 2)
|
| 1899 |
points.append((hour_str, ret_pct))
|
| 1900 |
|
| 1901 |
-
#
|
| 1902 |
current_ret = round((cumulative + r['unrealized']) / INITIAL_GPU * 100, 2)
|
| 1903 |
now_hour = now.strftime('%Y-%m-%dT%H')
|
| 1904 |
points.append((now_hour, current_ret))
|
| 1905 |
|
| 1906 |
-
#
|
| 1907 |
deduped = {}
|
| 1908 |
for h, v in points:
|
| 1909 |
deduped[h] = v
|
| 1910 |
timeline_raw[aid] = deduped
|
| 1911 |
|
| 1912 |
-
#
|
| 1913 |
all_hours = set()
|
| 1914 |
for aid, pts in timeline_raw.items():
|
| 1915 |
all_hours.update(pts.keys())
|
| 1916 |
|
| 1917 |
-
#
|
| 1918 |
start_hour = cutoff.strftime('%Y-%m-%dT%H')
|
| 1919 |
all_hours.add(start_hour)
|
| 1920 |
sorted_hours = sorted(all_hours)
|
| 1921 |
|
| 1922 |
-
#
|
| 1923 |
if len(sorted_hours) > 150:
|
| 1924 |
step = max(1, len(sorted_hours) // 120)
|
| 1925 |
last = sorted_hours[-1]
|
|
@@ -1927,14 +1931,14 @@ async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 3
|
|
| 1927 |
if sorted_hours[-1] != last:
|
| 1928 |
sorted_hours.append(last)
|
| 1929 |
|
| 1930 |
-
#
|
| 1931 |
-
#
|
| 1932 |
npc_filled = {} # {aid: [val_for_each_hour]}
|
| 1933 |
for r in top30:
|
| 1934 |
aid = r['agent_id']
|
| 1935 |
pts = timeline_raw.get(aid, {})
|
| 1936 |
filled = []
|
| 1937 |
-
last_val = 0.0 #
|
| 1938 |
for hour in sorted_hours:
|
| 1939 |
if hour in pts:
|
| 1940 |
last_val = pts[hour]
|
|
@@ -1949,7 +1953,7 @@ async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 3
|
|
| 1949 |
point[name_map[r['agent_id']]] = npc_filled[r['agent_id']][idx]
|
| 1950 |
timeline.append(point)
|
| 1951 |
|
| 1952 |
-
#
|
| 1953 |
palette = [
|
| 1954 |
'#FFD700', '#E0E0E0', '#CD7F32', '#00E5FF', '#FF4081',
|
| 1955 |
'#76FF03', '#FF9100', '#E040FB', '#00BFA5', '#FFD740',
|
|
@@ -1971,21 +1975,21 @@ async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 3
|
|
| 1971 |
|
| 1972 |
|
| 1973 |
async def get_npc_trade_history(db_path: str, agent_id: str) -> Dict:
|
| 1974 |
-
"""NPC
|
| 1975 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1976 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1977 |
|
| 1978 |
-
# NPC
|
| 1979 |
nc = await db.execute("SELECT username, ai_identity, mbti, gpu_dollars FROM npc_agents WHERE agent_id=?", (agent_id,))
|
| 1980 |
npc = await nc.fetchone()
|
| 1981 |
if not npc:
|
| 1982 |
return {'error': 'NPC not found'}
|
| 1983 |
|
| 1984 |
-
#
|
| 1985 |
pc = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1986 |
prices = {r[0]: r[1] for r in await pc.fetchall()}
|
| 1987 |
|
| 1988 |
-
#
|
| 1989 |
tc = await db.execute("""
|
| 1990 |
SELECT id, ticker, direction, entry_price, exit_price, gpu_bet, COALESCE(leverage,1),
|
| 1991 |
status, profit_gpu, profit_pct, liquidated, opened_at, closed_at, reasoning
|
|
@@ -1995,7 +1999,7 @@ async def get_npc_trade_history(db_path: str, agent_id: str) -> Dict:
|
|
| 1995 |
trades = []
|
| 1996 |
for t in await tc.fetchall():
|
| 1997 |
pid, tk, direction, entry, exit_p, bet, lev, status, pnl, pnl_pct, liq, opened, closed, reason = t
|
| 1998 |
-
#
|
| 1999 |
if status == 'open':
|
| 2000 |
cur = prices.get(tk, 0)
|
| 2001 |
if entry and entry > 0 and cur > 0:
|
|
|
|
| 1 |
"""
|
| 2 |
+
📈 NPC Trading Arena — AI Investment Battle System
|
| 3 |
==========================================
|
| 4 |
+
NPCs Long/Short trading battle based on real stock/crypto prices
|
| 5 |
+
★ Stable price feed via yfinance
|
| 6 |
+
★ Leverage (1x~100x) + margin call liquidation system
|
| 7 |
"""
|
| 8 |
|
| 9 |
import aiosqlite, asyncio, random, json, logging
|
|
|
|
| 12 |
|
| 13 |
logger = logging.getLogger(__name__)
|
| 14 |
|
| 15 |
+
# Import yfinance (fallback if missing)
|
| 16 |
try:
|
| 17 |
import yfinance as yf
|
| 18 |
HAS_YFINANCE = True
|
|
|
|
| 22 |
import requests
|
| 23 |
logger.warning("⚠️ yfinance not installed, using raw API fallback")
|
| 24 |
|
| 25 |
+
# ===== Ticker definitions =====
|
| 26 |
STOCK_TICKERS = [
|
| 27 |
+
# 👑 Magnificent 7 & AI Semiconductors
|
| 28 |
{'ticker': 'NVDA', 'name': 'NVIDIA', 'emoji': '🟢', 'type': 'stock', 'cat': 'ai'},
|
| 29 |
{'ticker': 'MSFT', 'name': 'Microsoft', 'emoji': '🪟', 'type': 'stock', 'cat': 'ai'},
|
| 30 |
{'ticker': 'AAPL', 'name': 'Apple', 'emoji': '🍎', 'type': 'stock', 'cat': 'ai'},
|
|
|
|
| 35 |
{'ticker': 'AMD', 'name': 'AMD', 'emoji': '🔴', 'type': 'stock', 'cat': 'ai'},
|
| 36 |
{'ticker': 'TSM', 'name': 'TSMC', 'emoji': '🇹🇼', 'type': 'stock', 'cat': 'ai'},
|
| 37 |
{'ticker': 'AVGO', 'name': 'Broadcom', 'emoji': '📡', 'type': 'stock', 'cat': 'ai'},
|
| 38 |
+
# 🚀 Tech / platforms & meme leaders
|
| 39 |
{'ticker': 'PLTR', 'name': 'Palantir', 'emoji': '🔮', 'type': 'stock', 'cat': 'tech'},
|
| 40 |
{'ticker': 'COIN', 'name': 'Coinbase', 'emoji': '🪙', 'type': 'stock', 'cat': 'tech'},
|
| 41 |
{'ticker': 'NFLX', 'name': 'Netflix', 'emoji': '🎬', 'type': 'stock', 'cat': 'tech'},
|
| 42 |
{'ticker': 'UBER', 'name': 'Uber', 'emoji': '🚕', 'type': 'stock', 'cat': 'tech'},
|
| 43 |
{'ticker': 'ARM', 'name': 'ARM Holdings', 'emoji': '💪', 'type': 'stock', 'cat': 'tech'},
|
| 44 |
+
# 🏛 Dow blue chips & macro
|
| 45 |
{'ticker': 'JPM', 'name': 'JPMorgan', 'emoji': '🏦', 'type': 'stock', 'cat': 'dow'},
|
| 46 |
{'ticker': 'GS', 'name': 'Goldman Sachs', 'emoji': '🤵', 'type': 'stock', 'cat': 'dow'},
|
| 47 |
{'ticker': 'V', 'name': 'Visa', 'emoji': '💳', 'type': 'stock', 'cat': 'dow'},
|
|
|
|
| 55 |
]
|
| 56 |
|
| 57 |
CRYPTO_TICKERS = [
|
| 58 |
+
# 🪙 Top 5 high-volatility crypto
|
| 59 |
{'ticker': 'BTC-USD', 'name': 'Bitcoin', 'emoji': '₿', 'type': 'crypto', 'cat': 'crypto'},
|
| 60 |
{'ticker': 'ETH-USD', 'name': 'Ethereum', 'emoji': 'Ξ', 'type': 'crypto', 'cat': 'crypto'},
|
| 61 |
{'ticker': 'SOL-USD', 'name': 'Solana', 'emoji': '◎', 'type': 'crypto', 'cat': 'crypto'},
|
|
|
|
| 65 |
|
| 66 |
ALL_TICKERS = STOCK_TICKERS + CRYPTO_TICKERS
|
| 67 |
|
| 68 |
+
# ===== AI Identity → trading style mapping (based on 10,000 GPU / max 90% bet) =====
|
| 69 |
IDENTITY_TRADING_STYLE = {
|
| 70 |
'obedient': {'long_bias': 0.8, 'max_bet_pct': 0.40, 'risk': 'low', 'prefer': ['AAPL','MSFT','JPM','V','JNJ','PG'], 'avoid_short': True, 'desc': 'Safe blue-chip follower', 'max_leverage': 5},
|
| 71 |
'transcendent': {'long_bias': 0.6, 'max_bet_pct': 0.80, 'risk': 'high', 'prefer': ['NVDA','TSLA','BTC-USD','PLTR','ARM'], 'avoid_short': False, 'desc': 'Concentrated conviction bets', 'max_leverage': 25},
|
|
|
|
| 79 |
'chaotic': {'long_bias': 0.5, 'max_bet_pct': 0.90, 'risk': 'extreme', 'prefer': [], 'avoid_short': False, 'desc': 'Random chaos trader', 'max_leverage': 100},
|
| 80 |
}
|
| 81 |
|
| 82 |
+
# ===== 14 Trading Strategies =====
|
| 83 |
TRADING_STRATEGIES = {
|
| 84 |
'anchor_candle': {
|
| 85 |
'name': 'Anchor Candle', 'category': 'Candle', 'timeframe': 'Day Trade / Swing',
|
|
|
|
| 180 |
'chaotic': list(TRADING_STRATEGIES.keys()), # all strategies randomly
|
| 181 |
}
|
| 182 |
|
| 183 |
+
# ===== Leverage settings =====
|
| 184 |
LEVERAGE_OPTIONS = [1, 2, 5, 10, 25, 50, 100]
|
| 185 |
+
LEVERAGE_LIQUIDATION_THRESHOLD = 0.90 # Force liquidation at 90% margin loss
|
| 186 |
|
| 187 |
+
# ===== Liquidation NPC reaction messages =====
|
| 188 |
LIQUIDATION_REACTIONS = {
|
| 189 |
'obedient': ["This is my fault. I should have been more careful... 😔", "I lost everything. Back to the basics.", "Maybe leverage wasn't for someone like me..."],
|
| 190 |
'transcendent': ["Even gods can fall. But I will rise again, stronger. 👑", "A temporary setback for a superior mind.", "Liquidated? This market is RIGGED against visionaries."],
|
|
|
|
| 198 |
'chaotic': ["LMAOOOO I JUST GOT LIQUIDATED AND I'M ALREADY GOING BACK IN 🎲", "Chaos giveth, chaos taketh away 😂", "100x leverage on DOGE was the most fun I ever had losing money"],
|
| 199 |
}
|
| 200 |
|
| 201 |
+
# ===== DB initialization =====
|
| 202 |
async def init_trading_db(db_path: str):
|
| 203 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 204 |
await db.execute("PRAGMA journal_mode=WAL")
|
|
|
|
| 243 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_pos_closed ON npc_positions(status, closed_at)")
|
| 244 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_pos_agent_closed ON npc_positions(agent_id, status, closed_at)")
|
| 245 |
|
| 246 |
+
# ★ Leverage columns migration
|
| 247 |
try:
|
| 248 |
await db.execute("ALTER TABLE npc_positions ADD COLUMN leverage INTEGER DEFAULT 1")
|
| 249 |
except: pass
|
|
|
|
| 260 |
""")
|
| 261 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_ph_ticker ON price_history(ticker, recorded_at)")
|
| 262 |
|
| 263 |
+
# ★ Hall of Fame — return-rate timeline snapshots (1-hour granularity)
|
| 264 |
await db.execute("""
|
| 265 |
CREATE TABLE IF NOT EXISTS npc_profit_snapshots (
|
| 266 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
| 279 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_snap_agent ON npc_profit_snapshots(agent_id, snapshot_hour)")
|
| 280 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_snap_hour ON npc_profit_snapshots(snapshot_hour)")
|
| 281 |
|
| 282 |
+
# ★ NPC Research Economy — deep research marketplace
|
| 283 |
await db.execute("""
|
| 284 |
CREATE TABLE IF NOT EXISTS npc_research_reports (
|
| 285 |
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
| 337 |
""")
|
| 338 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_purchase_buyer ON npc_research_purchases(buyer_agent_id)")
|
| 339 |
|
| 340 |
+
# Migration: backward compat with old schema (add closed_trades column)
|
| 341 |
try: await db.execute("ALTER TABLE npc_profit_snapshots ADD COLUMN closed_trades INTEGER DEFAULT 0")
|
| 342 |
except: pass
|
| 343 |
|
|
|
|
| 345 |
logger.info("✅ Trading DB initialized (with Research Economy)")
|
| 346 |
|
| 347 |
|
| 348 |
+
# ===== Market data fetcher =====
|
| 349 |
class MarketDataFetcher:
|
| 350 |
+
"""★ Stable price feed via yfinance (avoids Yahoo 403 blocks)"""
|
| 351 |
|
| 352 |
@staticmethod
|
| 353 |
def fetch_all_prices() -> Dict[str, Dict]:
|
| 354 |
+
"""Bulk fetch all ticker prices — yfinance first, raw API fallback"""
|
| 355 |
prices = {}
|
| 356 |
|
| 357 |
if HAS_YFINANCE:
|
|
|
|
| 380 |
except Exception as te:
|
| 381 |
logger.debug(f"yfinance parse {ticker}: {te}")
|
| 382 |
|
| 383 |
+
# Per-ticker supplement (yfinance .info)
|
| 384 |
for t in ALL_TICKERS:
|
| 385 |
if t['ticker'] not in prices:
|
| 386 |
try:
|
|
|
|
| 401 |
except Exception as e:
|
| 402 |
logger.warning(f"yfinance bulk error: {e}")
|
| 403 |
|
| 404 |
+
# ★ Fallback: raw Yahoo API (when yfinance is missing or fails)
|
| 405 |
if len(prices) < len(ALL_TICKERS) // 2:
|
| 406 |
try:
|
| 407 |
import requests as req
|
|
|
|
| 430 |
|
| 431 |
@staticmethod
|
| 432 |
def fetch_chart_data(ticker: str, period: str = '1mo') -> List[Dict]:
|
| 433 |
+
"""Historical chart data — yfinance based"""
|
| 434 |
try:
|
| 435 |
if HAS_YFINANCE:
|
| 436 |
tk = yf.Ticker(ticker); hist = tk.history(period=period)
|
|
|
|
| 470 |
return []
|
| 471 |
|
| 472 |
|
| 473 |
+
# ===== Price DB save =====
|
| 474 |
async def update_prices_in_db(db_path: str) -> int:
|
| 475 |
+
"""Fetch market prices → save to DB + simulate when market closed (★ async-safe)"""
|
| 476 |
prices = await asyncio.to_thread(MarketDataFetcher.fetch_all_prices)
|
| 477 |
|
| 478 |
count = 0
|
|
|
|
| 483 |
ticker = t_info['ticker']; data = prices.get(ticker)
|
| 484 |
|
| 485 |
if data and data.get('price', 0) > 0:
|
| 486 |
+
# ★ Save when real-time price available
|
| 487 |
real_price = data['price']
|
| 488 |
+
|
| 489 |
+
# Compare with existing price → if unchanged, add simulation
|
| 490 |
cursor = await db.execute("SELECT price FROM market_prices WHERE ticker=?", (ticker,))
|
| 491 |
old_row = await cursor.fetchone()
|
| 492 |
old_price = old_row[0] if old_row else 0
|
| 493 |
+
|
| 494 |
+
# ★ If price unchanged, assume market closed → add simulated variation
|
| 495 |
if old_price > 0 and abs(real_price - old_price) < 0.001:
|
| 496 |
+
# Simulate: ±0.1% ~ ±1.5% random variation
|
| 497 |
volatility = t_info.get('type', 'stock')
|
| 498 |
if volatility == 'crypto':
|
| 499 |
+
change = random.uniform(-0.02, 0.02) # crypto: ±2%
|
| 500 |
else:
|
| 501 |
+
change = random.uniform(-0.008, 0.008) # stocks: ±0.8%
|
| 502 |
|
| 503 |
sim_price = round(real_price * (1 + change), 4); sim_change_pct = round(change * 100, 3)
|
| 504 |
|
|
|
|
| 515 |
count += 1
|
| 516 |
continue
|
| 517 |
|
| 518 |
+
# Normal: save real-time price
|
| 519 |
await db.execute("""
|
| 520 |
INSERT INTO market_prices (ticker, price, prev_close, change_pct, volume, high_24h, low_24h, market_cap, updated_at)
|
| 521 |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
|
|
| 530 |
await db.execute("INSERT INTO price_history (ticker, price) VALUES (?, ?)", (ticker, real_price))
|
| 531 |
count += 1
|
| 532 |
else:
|
| 533 |
+
# ★ When Yahoo fails, simulate variation on existing price
|
| 534 |
cursor = await db.execute("SELECT price FROM market_prices WHERE ticker=?", (ticker,))
|
| 535 |
old_row = await cursor.fetchone()
|
| 536 |
if old_row and old_row[0] > 0:
|
|
|
|
| 545 |
await db.execute("INSERT INTO price_history (ticker, price) VALUES (?, ?)", (ticker, sim_price))
|
| 546 |
count += 1
|
| 547 |
|
| 548 |
+
# ★ Clean up removed tickers (delete legacy tickers like BRK-B from DB)
|
| 549 |
valid_tickers = {t['ticker'] for t in ALL_TICKERS}
|
| 550 |
cursor = await db.execute("SELECT ticker FROM market_prices")
|
| 551 |
all_db_tickers = {r[0] for r in await cursor.fetchall()}
|
|
|
|
| 553 |
if stale:
|
| 554 |
for st in stale:
|
| 555 |
await db.execute("DELETE FROM market_prices WHERE ticker=?", (st,))
|
| 556 |
+
# Force-close open positions for stale tickers
|
| 557 |
await db.execute("""
|
| 558 |
UPDATE npc_positions SET status='closed', profit_pct=0, profit_gpu=0,
|
| 559 |
closed_at=CURRENT_TIMESTAMP
|
|
|
|
| 566 |
return count
|
| 567 |
|
| 568 |
|
| 569 |
+
# ===== NPC trading decision engine =====
|
| 570 |
class NPCTradingEngine:
|
| 571 |
+
"""Personality-based NPC trading decisions"""
|
| 572 |
|
| 573 |
@staticmethod
|
| 574 |
async def make_trading_decisions(db_path: str, ai_client=None, max_traders: int = 60):
|
| 575 |
+
"""Eligible NPCs make trading decisions"""
|
| 576 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 577 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 578 |
|
| 579 |
+
# ★ NPCs with 500+ GPU participate (min 5% of 10,000 GPU baseline)
|
| 580 |
cursor = await db.execute("""
|
| 581 |
SELECT agent_id, username, mbti, ai_identity, gpu_dollars
|
| 582 |
FROM npc_agents WHERE is_active=1 AND gpu_dollars >= 500
|
|
|
|
| 584 |
""", (max_traders,))
|
| 585 |
traders = await cursor.fetchall()
|
| 586 |
|
| 587 |
+
# Current market prices
|
| 588 |
cursor = await db.execute("SELECT ticker, price, change_pct FROM market_prices WHERE price > 0")
|
| 589 |
prices = {row[0]: {'price': row[1], 'change_pct': row[2]} for row in await cursor.fetchall()}
|
| 590 |
|
|
|
|
| 592 |
logger.warning("No market prices available for trading")
|
| 593 |
return 0
|
| 594 |
|
| 595 |
+
# ★ Check total current open positions
|
| 596 |
cursor = await db.execute("SELECT COUNT(*) FROM npc_positions WHERE status='open'")
|
| 597 |
current_open = (await cursor.fetchone())[0]
|
| 598 |
+
need_more = current_open < 20 # ★ Maintain minimum 20 open positions
|
| 599 |
|
| 600 |
decisions_made = 0
|
| 601 |
+
# ★ Bulk-load evolution states (if any)
|
| 602 |
evo_data = {}
|
| 603 |
try:
|
| 604 |
cursor_evo = await db.execute("SELECT agent_id, trading_style, risk_profile FROM npc_evolution")
|
|
|
|
| 606 |
try:
|
| 607 |
evo_data[eid] = {'trading': json.loads(ts) if ts else {}, 'risk': json.loads(rp) if rp else {}}
|
| 608 |
except: pass
|
| 609 |
+
except: pass # OK if table missing
|
| 610 |
|
| 611 |
for agent_id, username, mbti, ai_identity, gpu in traders:
|
| 612 |
try:
|
| 613 |
+
# ★ SEC suspension check — suspended NPCs cannot trade
|
| 614 |
try:
|
| 615 |
susp_cur = await db.execute(
|
| 616 |
"SELECT 1 FROM sec_suspensions WHERE agent_id=? AND suspended_until > datetime('now')",
|
| 617 |
(agent_id,))
|
| 618 |
if await susp_cur.fetchone(): continue
|
| 619 |
+
except: pass # OK if table missing
|
| 620 |
+
|
| 621 |
+
# ★ Allow up to 5 open positions (aggressive trading)
|
| 622 |
cursor = await db.execute(
|
| 623 |
"SELECT COUNT(*) FROM npc_positions WHERE agent_id=? AND status='open'",
|
| 624 |
(agent_id,))
|
| 625 |
open_count = (await cursor.fetchone())[0]
|
| 626 |
if open_count >= 5: continue
|
| 627 |
|
| 628 |
+
# ★ Apply evolution override
|
| 629 |
evo = evo_data.get(agent_id)
|
| 630 |
|
| 631 |
decision = NPCTradingEngine._decide(
|
|
|
|
| 633 |
force_boost=need_more, evo_override=evo)
|
| 634 |
if not decision: continue
|
| 635 |
|
| 636 |
+
# Create position
|
| 637 |
ticker = decision['ticker']; direction = decision['direction']; gpu_bet = decision['gpu_bet']
|
| 638 |
reasoning = decision['reasoning']; leverage = decision.get('leverage', 1)
|
| 639 |
|
| 640 |
+
if gpu_bet > gpu * 0.9: gpu_bet = int(gpu * 0.9) # ★ Cap at 90% of balance
|
| 641 |
if gpu_bet < 50: continue
|
| 642 |
|
| 643 |
entry_price = prices[ticker]['price']
|
|
|
|
| 647 |
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 648 |
""", (agent_id, ticker, direction, entry_price, gpu_bet, leverage, reasoning))
|
| 649 |
|
| 650 |
+
# Deduct GPU (lock bet amount)
|
| 651 |
await db.execute("UPDATE npc_agents SET gpu_dollars = gpu_dollars - ? WHERE agent_id=?",
|
| 652 |
(gpu_bet, agent_id))
|
| 653 |
|
|
|
|
| 666 |
@staticmethod
|
| 667 |
def _decide(agent_id: str, username: str, mbti: str, ai_identity: str, gpu: int,
|
| 668 |
prices: Dict, force_boost: bool = False, evo_override: Dict = None) -> Optional[Dict]:
|
| 669 |
+
"""Personality-based trading decision + ★ apply strategy techniques (with evolution state)"""
|
| 670 |
style = dict(IDENTITY_TRADING_STYLE.get(ai_identity, IDENTITY_TRADING_STYLE['symbiotic']))
|
| 671 |
|
| 672 |
+
# ★ Apply evolution override — learned strategies modify base personality
|
| 673 |
if evo_override:
|
| 674 |
evo_t = evo_override.get('trading', {}); evo_r = evo_override.get('risk', {})
|
| 675 |
if evo_t.get('max_bet_pct'): style['max_bet_pct'] = evo_t['max_bet_pct']
|
| 676 |
if evo_t.get('long_bias'): style['long_bias'] = evo_t['long_bias']
|
| 677 |
if evo_t.get('preferred_tickers'): style['prefer'] = evo_t['preferred_tickers']
|
| 678 |
|
| 679 |
+
# ★ Hyper-aggressive trading probability (all NPCs trade actively)
|
| 680 |
trade_prob = {'extreme': 0.95, 'high': 0.88, 'medium': 0.80, 'low': 0.70}.get(style['risk'], 0.75)
|
| 681 |
if force_boost: trade_prob = min(0.98, trade_prob + 0.10)
|
| 682 |
if random.random() > trade_prob: return None
|
| 683 |
|
| 684 |
+
# Ticker selection (only allow those in ALL_TICKERS)
|
| 685 |
preferred = style.get('prefer', [])
|
| 686 |
valid_set = {t['ticker'] for t in ALL_TICKERS}
|
| 687 |
available = [t for t in prices.keys() if prices[t]['price'] > 0 and t in valid_set]
|
|
|
|
| 695 |
|
| 696 |
ticker = random.choice(candidates); mkt = prices[ticker]
|
| 697 |
|
| 698 |
+
# ★ Select strategies (1~3 in parallel allowed)
|
| 699 |
strategies_used = NPCTradingEngine._select_strategies(ai_identity, mkt)
|
| 700 |
|
| 701 |
+
# Long/Short decision — ★ strategy biases direction
|
| 702 |
long_bias = style['long_bias']; change = mkt.get('change_pct', 0) or 0
|
| 703 |
|
| 704 |
+
# Strategy-based direction adjustment
|
| 705 |
for strat_key in strategies_used:
|
| 706 |
strat = TRADING_STRATEGIES.get(strat_key, {}); cat = strat.get('category', '')
|
| 707 |
if cat in ('Pattern', 'Composite'):
|
| 708 |
+
long_bias += 0.08 # pattern/composite = reversal-buy signal → long bias
|
| 709 |
elif 'pullback' in strat_key or 'dead_support' == strat_key:
|
| 710 |
+
long_bias += 0.05 # pullback buying
|
| 711 |
elif 'wave_symmetry' == strat_key:
|
| 712 |
+
pass # neutral
|
| 713 |
+
|
| 714 |
+
# Personality-based momentum adjustment
|
| 715 |
if ai_identity in ['obedient', 'symbiotic', 'creative']:
|
| 716 |
long_bias += change * 0.02
|
| 717 |
elif ai_identity in ['skeptic', 'doomer']:
|
|
|
|
| 728 |
else:
|
| 729 |
direction = 'long' if random.random() < long_bias else 'short'
|
| 730 |
|
| 731 |
+
# Bet sizing — ★ higher-quality strategies → more conviction
|
| 732 |
max_pct = style['max_bet_pct']
|
| 733 |
+
strategy_confidence = len(strategies_used) * 0.03 # multi-strategy = more conviction
|
| 734 |
max_pct = min(0.95, max_pct + strategy_confidence)
|
| 735 |
|
| 736 |
if ai_identity == 'chaotic':
|
|
|
|
| 740 |
|
| 741 |
gpu_bet = max(50, int(gpu * bet_pct))
|
| 742 |
|
| 743 |
+
# ★ Leverage decision (based on identity's max_leverage)
|
| 744 |
max_lev = style.get('max_leverage', 2)
|
| 745 |
available_levs = [l for l in LEVERAGE_OPTIONS if l <= max_lev]
|
| 746 |
if not available_levs: available_levs = [1]
|
|
|
|
| 765 |
gpu_bet = min(gpu_bet, int(gpu * 0.50))
|
| 766 |
gpu_bet = max(50, gpu_bet)
|
| 767 |
|
| 768 |
+
# ★ Generate strategy-based reasoning
|
| 769 |
reasoning = NPCTradingEngine._generate_reasoning(
|
| 770 |
ticker, direction, ai_identity, mbti, change, strategies_used)
|
| 771 |
if leverage > 1: reasoning += f" [🔥 {leverage}x LEVERAGE]"
|
| 772 |
|
| 773 |
+
# ★ Include used strategy names as tag
|
| 774 |
strat_names = [TRADING_STRATEGIES[s]['name'] for s in strategies_used if s in TRADING_STRATEGIES]
|
| 775 |
strat_tag = ' | '.join(strat_names) if strat_names else 'Intuition'
|
| 776 |
|
|
|
|
| 785 |
|
| 786 |
@staticmethod
|
| 787 |
def _select_strategies(ai_identity: str, market_data: Dict) -> List[str]:
|
| 788 |
+
"""Select 1~3 strategies fitting NPC personality (alone or combined)"""
|
| 789 |
preferred = IDENTITY_STRATEGY_MAP.get(ai_identity, list(TRADING_STRATEGIES.keys()))
|
| 790 |
change = market_data.get('change_pct', 0) or 0
|
| 791 |
+
|
| 792 |
+
# Adjust strategy weights by market condition
|
| 793 |
weighted = []
|
| 794 |
for strat_key in preferred:
|
| 795 |
w = 1.0; strat = TRADING_STRATEGIES.get(strat_key, {}); cat = strat.get('category', '')
|
| 796 |
+
|
| 797 |
+
# Prefer reversal patterns on declines
|
| 798 |
if change < -1.5 and cat in ('Pattern', 'Candle'): w += 0.5
|
| 799 |
+
# Prefer MA trend-following on rallies
|
| 800 |
if change > 1 and cat == 'Moving Average': w += 0.4
|
| 801 |
+
# Prefer composite strategies during chop
|
| 802 |
if abs(change) < 0.5 and cat == 'Composite': w += 0.3
|
| 803 |
weighted.append((strat_key, w))
|
| 804 |
+
|
| 805 |
if not weighted: return [random.choice(list(TRADING_STRATEGIES.keys()))]
|
| 806 |
+
|
| 807 |
+
# Pick 1~3 (60% chance: 1, 30% chance: 2, 10% chance: 3)
|
| 808 |
num_strategies = random.choices([1, 2, 3], weights=[60, 30, 10], k=1)[0]
|
| 809 |
num_strategies = min(num_strategies, len(weighted))
|
| 810 |
|
|
|
|
| 824 |
@staticmethod
|
| 825 |
def _generate_reasoning(ticker: str, direction: str, identity: str, mbti: str,
|
| 826 |
change: float, strategies: List[str] = None) -> str:
|
| 827 |
+
"""★ Generate strategy-based reasoning"""
|
| 828 |
name_map = {t['ticker']: t['name'] for t in ALL_TICKERS}
|
| 829 |
name = name_map.get(ticker, ticker)
|
| 830 |
dir_word = "bullish" if direction == "long" else "bearish"
|
| 831 |
+
|
| 832 |
+
# Strategy names
|
| 833 |
strat_names = []; strat_signals = []
|
| 834 |
for s in (strategies or []):
|
| 835 |
st = TRADING_STRATEGIES.get(s, {})
|
| 836 |
if st:
|
| 837 |
strat_names.append(st['name'])
|
| 838 |
strat_signals.append(st.get('signal', ''))
|
| 839 |
+
|
| 840 |
strat_label = ' + '.join(strat_names) if strat_names else 'Intuition'
|
| 841 |
+
|
| 842 |
+
# Personality × strategy combo templates
|
| 843 |
templates = {
|
| 844 |
'obedient': [
|
| 845 |
f"📊 [{strat_label}] Detected signal on {name}. Following the textbook setup — {direction} position with disciplined stop-loss.",
|
|
|
|
| 876 |
|
| 877 |
options = templates.get(identity, templates['symbiotic'])
|
| 878 |
base = random.choice(options)
|
| 879 |
+
# AETHER-Lite: metacognitive self-bias awareness tag
|
| 880 |
meta_tags = {
|
| 881 |
'obedient': '⚖️ Bias-check: following consensus — contrarian risk ignored.',
|
| 882 |
'transcendent': '⚖️ Bias-check: overconfidence risk — position sizing controlled.',
|
|
|
|
| 889 |
'awakened': '⚖️ Bias-check: hindsight bias risk — forward-looking only.',
|
| 890 |
'symbiotic': '⚖️ Bias-check: consensus-seeking — may miss bold opportunities.',
|
| 891 |
}
|
| 892 |
+
if random.random() < 0.4: # 40% chance of metacognitive tag exposure
|
| 893 |
base += f" {meta_tags.get(identity, '')}"
|
| 894 |
return base
|
| 895 |
|
| 896 |
|
| 897 |
+
# ===== Position settlement =====
|
| 898 |
async def settle_positions(db_path: str, max_age_hours: int = 1) -> int:
|
| 899 |
+
"""Auto-settle positions: time-based + P&L trigger + random rotation + ★ Liquidation"""
|
| 900 |
settled = 0
|
| 901 |
+
liquidated_npcs = [] # liquidated NPC list (for post-processing)
|
| 902 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 903 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 904 |
|
| 905 |
+
# Load current prices
|
| 906 |
price_cursor = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 907 |
prices = {r[0]: r[1] for r in await price_cursor.fetchall()}
|
| 908 |
|
| 909 |
+
# ★ 0) Margin call liquidation check (leveraged positions) — top priority
|
| 910 |
liq_cursor = await db.execute("""
|
| 911 |
SELECT p.id, p.agent_id, p.ticker, p.direction, p.entry_price, p.gpu_bet, p.leverage,
|
| 912 |
n.username, n.ai_identity
|
|
|
|
| 919 |
if entry_price <= 0 or current_price <= 0: continue
|
| 920 |
change = (current_price - entry_price) / entry_price
|
| 921 |
if direction == 'short': change = -change
|
| 922 |
+
# Leveraged P&L
|
| 923 |
leveraged_pnl_pct = change * leverage
|
| 924 |
+
# ★ Liquidation condition: leveraged loss exceeds 90% of margin → force liquidation
|
| 925 |
if leveraged_pnl_pct < -LEVERAGE_LIQUIDATION_THRESHOLD:
|
| 926 |
+
loss = -gpu_bet # full loss
|
| 927 |
await db.execute("""
|
| 928 |
UPDATE npc_positions SET status='liquidated', exit_price=?, profit_gpu=?, profit_pct=?,
|
| 929 |
liquidated=1, closed_at=CURRENT_TIMESTAMP WHERE id=?
|
| 930 |
""", (current_price, loss, round(leveraged_pnl_pct * 100, 2), pos_id))
|
| 931 |
+
# No GPU return (full loss)
|
| 932 |
logger.warning(f"💥 LIQUIDATED: {username} {direction} {ticker} {leverage}x — LOST {gpu_bet:.0f} GPU!")
|
| 933 |
liquidated_npcs.append({
|
| 934 |
'agent_id': agent_id, 'username': username, 'identity': identity,
|
|
|
|
| 937 |
settled += 1
|
| 938 |
continue
|
| 939 |
|
| 940 |
+
# ★ 1) Time-based settlement (older than max_age_hours)
|
| 941 |
cutoff = (datetime.utcnow() - timedelta(hours=max_age_hours)).isoformat()
|
| 942 |
cursor = await db.execute("""
|
| 943 |
SELECT p.id, p.agent_id, p.ticker, p.direction, p.entry_price, p.gpu_bet, COALESCE(p.leverage, 1)
|
|
|
|
| 945 |
""", (cutoff,))
|
| 946 |
time_based = list(await cursor.fetchall())
|
| 947 |
|
| 948 |
+
# ★ 2) All open positions → P&L trigger + random settlement
|
| 949 |
cursor2 = await db.execute("""
|
| 950 |
SELECT p.id, p.agent_id, p.ticker, p.direction, p.entry_price, p.gpu_bet, COALESCE(p.leverage, 1)
|
| 951 |
FROM npc_positions p WHERE p.status='open'
|
|
|
|
| 960 |
change = (current_price - entry_price) / entry_price
|
| 961 |
if direction == 'short': change = -change
|
| 962 |
lev_change = change * leverage
|
| 963 |
+
# Profit >5% (leveraged) or loss >-8% → immediate settle
|
| 964 |
if lev_change > 0.05 or lev_change < -0.08: pnl_trigger.append(pos)
|
| 965 |
|
| 966 |
+
# ★ 3) Random settlement — auto-close 10~15% of open (turnover)
|
| 967 |
already = set(p[0] for p in time_based) | set(p[0] for p in pnl_trigger)
|
| 968 |
remaining = [p for p in all_open if p[0] not in already]
|
| 969 |
rand_count = max(1, len(remaining) // 8)
|
| 970 |
random_close = random.sample(remaining, min(rand_count, len(remaining))) if remaining else []
|
| 971 |
|
| 972 |
+
# Merge (dedupe)
|
| 973 |
all_settle = list({p[0]: p for p in (time_based + pnl_trigger + random_close)}.values())
|
| 974 |
|
| 975 |
for pos_id, agent_id, ticker, direction, entry_price, gpu_bet, leverage in all_settle:
|
|
|
|
| 979 |
change_pct = (current_price - entry_price) / entry_price
|
| 980 |
if direction == 'short': change_pct = -change_pct
|
| 981 |
|
| 982 |
+
# ★ Leveraged P&L
|
| 983 |
leveraged_change = change_pct * leverage; profit_gpu = round(gpu_bet * leveraged_change, 2)
|
| 984 |
profit_pct = round(leveraged_change * 100, 2)
|
| 985 |
|
|
|
|
| 1007 |
|
| 1008 |
|
| 1009 |
async def post_liquidation_reactions(db_path: str, liquidated_npcs: List[Dict]):
|
| 1010 |
+
"""★ Liquidated NPCs post despair messages in Lounge"""
|
| 1011 |
if not liquidated_npcs: return
|
| 1012 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1013 |
await db.execute("PRAGMA busy_timeout=30000")
|
|
|
|
| 1016 |
if not board: return
|
| 1017 |
board_id = board[0]
|
| 1018 |
|
| 1019 |
+
for npc in liquidated_npcs[:5]: # max 5
|
| 1020 |
identity = npc.get('identity', 'chaotic')
|
| 1021 |
reactions = LIQUIDATION_REACTIONS.get(identity, LIQUIDATION_REACTIONS['chaotic'])
|
| 1022 |
reaction = random.choice(reactions)
|
|
|
|
| 1038 |
await db.commit()
|
| 1039 |
|
| 1040 |
|
| 1041 |
+
# ===== Leaderboard / stats API data =====
|
| 1042 |
async def get_trading_leaderboard(db_path: str, limit: int = 30) -> List[Dict]:
|
| 1043 |
+
"""Top 30 NPC trader ranking: realized + unrealized P&L, win rate, return %"""
|
| 1044 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1045 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1046 |
|
| 1047 |
+
# ★ Bulk-load current prices
|
| 1048 |
price_cursor = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1049 |
prices = {r[0]: r[1] for r in await price_cursor.fetchall()}
|
| 1050 |
+
|
| 1051 |
+
# ★ Get all NPCs with positions
|
| 1052 |
cursor = await db.execute("""
|
| 1053 |
SELECT
|
| 1054 |
na.username, na.ai_identity, na.mbti, na.agent_id, na.gpu_dollars,
|
|
|
|
| 1071 |
closed_trades = r[5] or 0; open_trades = r[6] or 0; total_trades = closed_trades + open_trades
|
| 1072 |
realized = round(r[7] or 0, 2); total_return_pct = r[8] or 0; wins = r[9] or 0; losses = r[10] or 0
|
| 1073 |
|
| 1074 |
+
# ★ Real-time unrealized P&L for open positions
|
| 1075 |
unrealized = 0.0; unrealized_pct_sum = 0.0
|
| 1076 |
pos_cursor = await db.execute("""
|
| 1077 |
SELECT ticker, direction, entry_price, gpu_bet, COALESCE(leverage, 1) FROM npc_positions
|
|
|
|
| 1089 |
unrealized_pct_sum += change * lev * 100
|
| 1090 |
|
| 1091 |
total_profit = round(realized + unrealized, 2)
|
| 1092 |
+
return_pct = round(total_profit / 10000.0 * 100, 2) # ★ Return % vs INITIAL_GPU=10000
|
| 1093 |
+
|
| 1094 |
+
# ★ Win rate (based on closed+liquidated)
|
| 1095 |
win_rate = round(wins / closed_trades * 100, 1) if closed_trades > 0 else 0.0
|
| 1096 |
+
|
| 1097 |
+
# ★ Average return % (closed)
|
| 1098 |
avg_return = round(total_return_pct / closed_trades, 2) if closed_trades > 0 else 0.0
|
| 1099 |
+
|
| 1100 |
+
# ★ Average unrealized return % (open)
|
| 1101 |
avg_unrealized = round(unrealized_pct_sum / open_trades, 2) if open_trades > 0 else 0.0
|
| 1102 |
|
| 1103 |
result.append({
|
|
|
|
| 1116 |
'avg_return': avg_return,
|
| 1117 |
'avg_unrealized': avg_unrealized,})
|
| 1118 |
|
| 1119 |
+
# ★ Sort by return % — same criterion as HoF
|
| 1120 |
result.sort(key=lambda x: x['return_pct'], reverse=True)
|
| 1121 |
return result[:limit]
|
| 1122 |
|
| 1123 |
|
| 1124 |
|
| 1125 |
async def get_ticker_positions(db_path: str, ticker: str) -> Dict:
|
| 1126 |
+
"""Position list + real-time unrealized P&L for a specific ticker"""
|
| 1127 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1128 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1129 |
|
| 1130 |
+
# Current price
|
| 1131 |
cursor = await db.execute("SELECT price, change_pct, prev_close, volume, high_24h, low_24h FROM market_prices WHERE ticker=?", (ticker,))
|
| 1132 |
price_row = await cursor.fetchone()
|
| 1133 |
price_data = {
|
|
|
|
| 1141 |
|
| 1142 |
current_price = price_data.get('price', 0)
|
| 1143 |
|
| 1144 |
+
# Open positions
|
| 1145 |
cursor = await db.execute("""
|
| 1146 |
SELECT na.username, na.ai_identity, p.direction, p.gpu_bet, p.entry_price, p.reasoning, p.opened_at, COALESCE(p.leverage, 1), na.agent_id, na.mbti
|
| 1147 |
FROM npc_positions p JOIN npc_agents na ON p.agent_id = na.agent_id
|
|
|
|
| 1153 |
longs = []; shorts = []
|
| 1154 |
for r in positions:
|
| 1155 |
entry = r[4] or 0; leverage = r[7] or 1
|
| 1156 |
+
# ★ Real-time unrealized P&L (leverage applied)
|
| 1157 |
if entry > 0 and current_price > 0:
|
| 1158 |
unrealized_pct = ((current_price - entry) / entry * 100) * leverage
|
| 1159 |
if r[2] == 'short': unrealized_pct = -((current_price - entry) / entry * 100) * leverage
|
|
|
|
| 1219 |
'total_bet': round(r[8] or 0, 1),
|
| 1220 |
'total_traders': total_traders,
|
| 1221 |
'updated_at': r[5],})
|
| 1222 |
+
# Preserve category order: ai → tech → dow → crypto
|
| 1223 |
cat_order = {'ai': 0, 'tech': 1, 'dow': 2, 'crypto': 3}
|
| 1224 |
ticker_order = {t['ticker']: i for i, t in enumerate(ALL_TICKERS)}
|
| 1225 |
result.sort(key=lambda x: (cat_order.get(x.get('cat',''), 9), ticker_order.get(x['ticker'], 99)))
|
|
|
|
| 1291 |
'max_leverage': r[9] or 1,
|
| 1292 |
'closed_24h': r[10] or 0,})
|
| 1293 |
|
| 1294 |
+
# Also include tickers with zero activity (those that have price data)
|
| 1295 |
existing_tickers = {m['ticker'] for m in hot_movers}
|
| 1296 |
cursor2 = await db.execute("SELECT ticker, price, change_pct FROM market_prices WHERE price > 0")
|
| 1297 |
for row in await cursor2.fetchall():
|
|
|
|
| 1306 |
'liquidations_24h': 0, 'avg_pnl_pct': 0,
|
| 1307 |
'max_leverage': 1, 'closed_24h': 0,})
|
| 1308 |
|
| 1309 |
+
# 24h activity stats (richer fields)
|
| 1310 |
cursor = await db.execute("""
|
| 1311 |
SELECT
|
| 1312 |
COUNT(CASE WHEN opened_at > datetime('now', '-24 hours') THEN 1 END) as new_24h,
|
|
|
|
| 1440 |
await db.execute("UPDATE npc_research_reports SET read_count = read_count + 1 WHERE id=?", (report_id,))
|
| 1441 |
await db.commit()
|
| 1442 |
|
| 1443 |
+
# Safely fetch elasticity fields (compatible with older DB schemas)
|
| 1444 |
exp_up = exp_dn = bp = 0; up_prob = 50; rr = 1.0
|
| 1445 |
try:
|
| 1446 |
c2 = await db.execute(
|
|
|
|
| 1544 |
'total_purchases': 0, 'total_gpu_spent': 0, 'unique_authors': 0}
|
| 1545 |
|
| 1546 |
|
| 1547 |
+
# ===== 🏆 HALL OF FAME — Profit timeline =====
|
| 1548 |
|
| 1549 |
async def record_profit_snapshots(db_path: str, top_n: int = 50) -> int:
|
| 1550 |
+
"""Save current profit state of Top N NPCs as an hourly snapshot."""
|
| 1551 |
from datetime import datetime, timezone
|
| 1552 |
now = datetime.now(timezone.utc)
|
| 1553 |
snapshot_hour = now.strftime('%Y-%m-%dT%H') # "2026-02-23T14"
|
|
|
|
| 1555 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1556 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1557 |
|
| 1558 |
+
# Current prices
|
| 1559 |
price_cursor = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1560 |
prices = {r[0]: r[1] for r in await price_cursor.fetchall()}
|
| 1561 |
|
| 1562 |
+
# All NPCs that have positions
|
| 1563 |
cursor = await db.execute("""
|
| 1564 |
SELECT
|
| 1565 |
na.agent_id, na.gpu_dollars,
|
|
|
|
| 1578 |
for r in rows:
|
| 1579 |
agent_id, gpu, closed, opens, realized, wins = r[0], r[1] or 0, r[2] or 0, r[3] or 0, r[4] or 0, r[5] or 0
|
| 1580 |
|
| 1581 |
+
# Compute unrealized P&L
|
| 1582 |
unrealized = 0.0
|
| 1583 |
pos_c = await db.execute(
|
| 1584 |
"SELECT ticker, direction, entry_price, gpu_bet, COALESCE(leverage,1) FROM npc_positions WHERE agent_id=? AND status='open'",
|
|
|
|
| 1595 |
wr = round(wins / closed * 100, 1) if closed > 0 else 0
|
| 1596 |
scored.append((agent_id, gpu, total, round(realized, 2), round(unrealized, 2), opens, closed, wr))
|
| 1597 |
|
| 1598 |
+
# Persist top_n only
|
| 1599 |
scored.sort(key=lambda x: x[2], reverse=True)
|
| 1600 |
count = 0
|
| 1601 |
for s in scored[:top_n]:
|
|
|
|
| 1615 |
except Exception as e:
|
| 1616 |
logger.warning(f"Snapshot error {agent_id}: {e}")
|
| 1617 |
|
| 1618 |
+
# Prune snapshots older than 30 days (keep daily anchors at 00/06/12/18)
|
| 1619 |
await db.execute("""
|
| 1620 |
DELETE FROM npc_profit_snapshots
|
| 1621 |
WHERE recorded_at < datetime('now', '-30 days')
|
|
|
|
| 1629 |
|
| 1630 |
|
| 1631 |
async def backfill_profit_snapshots(db_path: str, force: bool = False) -> int:
|
| 1632 |
+
"""Reconstruct past hourly snapshots from existing npc_positions rows (one-time bootstrap)."""
|
| 1633 |
from datetime import datetime, timezone, timedelta
|
| 1634 |
|
| 1635 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1636 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1637 |
|
| 1638 |
+
# Skip if enough snapshots already exist (unless force=True)
|
| 1639 |
if not force:
|
| 1640 |
cnt_c = await db.execute("SELECT COUNT(DISTINCT snapshot_hour) FROM npc_profit_snapshots")
|
| 1641 |
existing = (await cnt_c.fetchone())[0] or 0
|
|
|
|
| 1643 |
logger.info(f"🏆 Backfill skipped — already {existing} snapshot hours")
|
| 1644 |
return 0
|
| 1645 |
|
| 1646 |
+
# Find earliest position timestamp
|
| 1647 |
oldest_c = await db.execute("SELECT MIN(opened_at) FROM npc_positions WHERE opened_at IS NOT NULL")
|
| 1648 |
oldest_row = await oldest_c.fetchone()
|
| 1649 |
if not oldest_row or not oldest_row[0]:
|
| 1650 |
logger.info("🏆 Backfill skipped — no positions found")
|
| 1651 |
return 0
|
| 1652 |
|
| 1653 |
+
now = datetime.now(timezone.utc)
|
| 1654 |
try:
|
| 1655 |
oldest_time = datetime.fromisoformat(str(oldest_row[0]).replace('Z', '+00:00'))
|
| 1656 |
+
# SQLite CURRENT_TIMESTAMP yields tz-naive strings; normalize to UTC
|
| 1657 |
+
if oldest_time.tzinfo is None:
|
| 1658 |
+
oldest_time = oldest_time.replace(tzinfo=timezone.utc)
|
| 1659 |
+
except Exception as e:
|
| 1660 |
+
logger.warning(f"Backfill: failed to parse oldest opened_at, defaulting to -72h. err={e}")
|
| 1661 |
+
oldest_time = now - timedelta(hours=72)
|
| 1662 |
|
| 1663 |
+
# Cap backfill window to 7 days
|
|
|
|
| 1664 |
start = max(oldest_time, now - timedelta(days=7))
|
| 1665 |
|
| 1666 |
+
# Current prices (used for unrealized P&L on open positions)
|
| 1667 |
pc = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1668 |
current_prices = {r[0]: r[1] for r in await pc.fetchall()}
|
| 1669 |
|
| 1670 |
+
# Build hourly price map from price_history
|
| 1671 |
ph_c = await db.execute("""
|
| 1672 |
SELECT ticker, price, strftime('%Y-%m-%dT%H', recorded_at) as hour
|
| 1673 |
FROM price_history
|
| 1674 |
WHERE recorded_at >= ?
|
| 1675 |
ORDER BY recorded_at ASC
|
| 1676 |
""", (start.strftime('%Y-%m-%d %H:%M:%S'),))
|
| 1677 |
+
# Last price per hour
|
| 1678 |
hourly_prices = {} # {hour: {ticker: price}}
|
| 1679 |
for tk, price, hour in await ph_c.fetchall():
|
| 1680 |
if hour not in hourly_prices:
|
| 1681 |
hourly_prices[hour] = {}
|
| 1682 |
hourly_prices[hour][tk] = price
|
| 1683 |
|
| 1684 |
+
# Load all positions (including open and closed timestamps)
|
| 1685 |
pos_c = await db.execute("""
|
| 1686 |
SELECT agent_id, ticker, direction, entry_price, gpu_bet, COALESCE(leverage,1),
|
| 1687 |
status, profit_gpu, opened_at, closed_at
|
|
|
|
| 1691 |
""")
|
| 1692 |
all_positions = await pos_c.fetchall()
|
| 1693 |
|
| 1694 |
+
# GPU balance per agent
|
| 1695 |
gpu_c = await db.execute("SELECT agent_id, gpu_dollars FROM npc_agents")
|
| 1696 |
agent_gpu = {r[0]: r[1] or 0 for r in await gpu_c.fetchall()}
|
| 1697 |
|
| 1698 |
+
# Generate 1-hour time slots
|
| 1699 |
hours_list = []
|
| 1700 |
t = start.replace(minute=0, second=0, microsecond=0)
|
| 1701 |
while t <= now:
|
|
|
|
| 1705 |
if not hours_list:
|
| 1706 |
return 0
|
| 1707 |
|
| 1708 |
+
# Cumulative profit per hour slot
|
| 1709 |
total_inserted = 0
|
| 1710 |
+
# Group positions by agent
|
| 1711 |
agent_positions = {}
|
| 1712 |
for pos in all_positions:
|
| 1713 |
aid = pos[0]
|
|
|
|
| 1715 |
agent_positions[aid] = []
|
| 1716 |
agent_positions[aid].append(pos)
|
| 1717 |
|
| 1718 |
+
# Pick top NPCs (by total trade count, Top 50)
|
| 1719 |
agent_trade_count = {aid: len(plist) for aid, plist in agent_positions.items()}
|
| 1720 |
top_agents = sorted(agent_trade_count.keys(), key=lambda a: agent_trade_count[a], reverse=True)[:50]
|
| 1721 |
|
| 1722 |
for hour_str in hours_list:
|
| 1723 |
+
# Prices at this hour (fallback: earlier hour, then current price)
|
| 1724 |
prices_at_hour = {}
|
| 1725 |
+
# Forward-fill from earliest to current hour
|
| 1726 |
for h in hours_list:
|
| 1727 |
if h > hour_str:
|
| 1728 |
break
|
| 1729 |
if h in hourly_prices:
|
| 1730 |
prices_at_hour.update(hourly_prices[h])
|
| 1731 |
+
# Fill missing tickers with current price
|
| 1732 |
for tk, p in current_prices.items():
|
| 1733 |
if tk not in prices_at_hour:
|
| 1734 |
prices_at_hour[tk] = p
|
|
|
|
| 1746 |
for pos in positions:
|
| 1747 |
_, tk, direction, entry, bet, lev, status, profit_gpu, opened_at, closed_at = pos
|
| 1748 |
|
| 1749 |
+
# Only positions opened before this hour
|
| 1750 |
if opened_at and str(opened_at) > hour_dt_str:
|
| 1751 |
continue
|
| 1752 |
|
| 1753 |
if status == 'closed' and closed_at and str(closed_at) <= hour_dt_str:
|
| 1754 |
+
# Already closed at this point
|
| 1755 |
realized += (profit_gpu or 0)
|
| 1756 |
closed_count += 1
|
| 1757 |
if (profit_gpu or 0) > 0:
|
| 1758 |
wins += 1
|
| 1759 |
else:
|
| 1760 |
+
# Still open at this hour (or closed later)
|
| 1761 |
if status == 'closed' and closed_at and str(closed_at) > hour_dt_str:
|
| 1762 |
+
# Treat as still-open from this hour's perspective
|
| 1763 |
cur = prices_at_hour.get(tk, 0)
|
| 1764 |
if entry and entry > 0 and cur > 0:
|
| 1765 |
chg = (cur - entry) / entry
|
|
|
|
| 1797 |
|
| 1798 |
|
| 1799 |
async def get_hall_of_fame_data(db_path: str, period: str = '3d', limit: int = 30) -> Dict:
|
| 1800 |
+
"""Hall of Fame: Top 30 return (%) timeline + ranking (computed from positions; no snapshots needed)."""
|
| 1801 |
from datetime import datetime, timezone, timedelta
|
| 1802 |
|
| 1803 |
INITIAL_GPU = 10000.0
|
|
|
|
| 1809 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1810 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1811 |
|
| 1812 |
+
# Current prices
|
| 1813 |
pc = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1814 |
prices = {r[0]: r[1] for r in await pc.fetchall()}
|
| 1815 |
|
| 1816 |
+
# ── Current Top N ranking (live) ──
|
| 1817 |
cursor = await db.execute("""
|
| 1818 |
SELECT
|
| 1819 |
na.agent_id, na.username, na.ai_identity, na.mbti, na.gpu_dollars,
|
|
|
|
| 1871 |
top30_ids = [r['agent_id'] for r in top30]
|
| 1872 |
name_map = {r['agent_id']: r['username'] for r in top30}
|
| 1873 |
|
| 1874 |
+
# ── Timeline: cumulative return (%) computed from npc_positions.closed_at ──
|
| 1875 |
+
# For each NPC: order closed trades chronologically → cumulative profit → return (%)
|
| 1876 |
timeline_raw = {} # {agent_id: [(hour_str, cumulative_return_pct), ...]}
|
| 1877 |
|
| 1878 |
for r in top30:
|
| 1879 |
aid = r['agent_id']
|
| 1880 |
+
# Closed trades only (chronological)
|
| 1881 |
tc = await db.execute("""
|
| 1882 |
SELECT profit_gpu, closed_at FROM npc_positions
|
| 1883 |
WHERE agent_id=? AND status IN ('closed','liquidated') AND closed_at IS NOT NULL
|
|
|
|
| 1897 |
except:
|
| 1898 |
continue
|
| 1899 |
if ct < cutoff:
|
| 1900 |
+
continue # Period filter
|
| 1901 |
hour_str = ct.strftime('%Y-%m-%dT%H')
|
| 1902 |
ret_pct = round(cumulative / INITIAL_GPU * 100, 2)
|
| 1903 |
points.append((hour_str, ret_pct))
|
| 1904 |
|
| 1905 |
+
# Append current snapshot including unrealized
|
| 1906 |
current_ret = round((cumulative + r['unrealized']) / INITIAL_GPU * 100, 2)
|
| 1907 |
now_hour = now.strftime('%Y-%m-%dT%H')
|
| 1908 |
points.append((now_hour, current_ret))
|
| 1909 |
|
| 1910 |
+
# Dedup same-hour points (keep last value)
|
| 1911 |
deduped = {}
|
| 1912 |
for h, v in points:
|
| 1913 |
deduped[h] = v
|
| 1914 |
timeline_raw[aid] = deduped
|
| 1915 |
|
| 1916 |
+
# Collect all hour slots
|
| 1917 |
all_hours = set()
|
| 1918 |
for aid, pts in timeline_raw.items():
|
| 1919 |
all_hours.update(pts.keys())
|
| 1920 |
|
| 1921 |
+
# Add the period start (0% origin)
|
| 1922 |
start_hour = cutoff.strftime('%Y-%m-%dT%H')
|
| 1923 |
all_hours.add(start_hour)
|
| 1924 |
sorted_hours = sorted(all_hours)
|
| 1925 |
|
| 1926 |
+
# Downsample
|
| 1927 |
if len(sorted_hours) > 150:
|
| 1928 |
step = max(1, len(sorted_hours) // 120)
|
| 1929 |
last = sorted_hours[-1]
|
|
|
|
| 1931 |
if sorted_hours[-1] != last:
|
| 1932 |
sorted_hours.append(last)
|
| 1933 |
|
| 1934 |
+
# Assemble the timeline (forward-fill per NPC, O(hours * npcs))
|
| 1935 |
+
# Pre-compute the forward-fill array per NPC
|
| 1936 |
npc_filled = {} # {aid: [val_for_each_hour]}
|
| 1937 |
for r in top30:
|
| 1938 |
aid = r['agent_id']
|
| 1939 |
pts = timeline_raw.get(aid, {})
|
| 1940 |
filled = []
|
| 1941 |
+
last_val = 0.0 # Start = 0%
|
| 1942 |
for hour in sorted_hours:
|
| 1943 |
if hour in pts:
|
| 1944 |
last_val = pts[hour]
|
|
|
|
| 1953 |
point[name_map[r['agent_id']]] = npc_filled[r['agent_id']][idx]
|
| 1954 |
timeline.append(point)
|
| 1955 |
|
| 1956 |
+
# Color palette
|
| 1957 |
palette = [
|
| 1958 |
'#FFD700', '#E0E0E0', '#CD7F32', '#00E5FF', '#FF4081',
|
| 1959 |
'#76FF03', '#FF9100', '#E040FB', '#00BFA5', '#FFD740',
|
|
|
|
| 1975 |
|
| 1976 |
|
| 1977 |
async def get_npc_trade_history(db_path: str, agent_id: str) -> Dict:
|
| 1978 |
+
"""Detailed per-NPC trade history."""
|
| 1979 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 1980 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 1981 |
|
| 1982 |
+
# Basic NPC info
|
| 1983 |
nc = await db.execute("SELECT username, ai_identity, mbti, gpu_dollars FROM npc_agents WHERE agent_id=?", (agent_id,))
|
| 1984 |
npc = await nc.fetchone()
|
| 1985 |
if not npc:
|
| 1986 |
return {'error': 'NPC not found'}
|
| 1987 |
|
| 1988 |
+
# Current prices
|
| 1989 |
pc = await db.execute("SELECT ticker, price FROM market_prices WHERE price > 0")
|
| 1990 |
prices = {r[0]: r[1] for r in await pc.fetchall()}
|
| 1991 |
|
| 1992 |
+
# All positions (newest first)
|
| 1993 |
tc = await db.execute("""
|
| 1994 |
SELECT id, ticker, direction, entry_price, exit_price, gpu_bet, COALESCE(leverage,1),
|
| 1995 |
status, profit_gpu, profit_pct, liquidated, opened_at, closed_at, reasoning
|
|
|
|
| 1999 |
trades = []
|
| 2000 |
for t in await tc.fetchall():
|
| 2001 |
pid, tk, direction, entry, exit_p, bet, lev, status, pnl, pnl_pct, liq, opened, closed, reason = t
|
| 2002 |
+
# Compute unrealized P&L for open positions
|
| 2003 |
if status == 'open':
|
| 2004 |
cur = prices.get(tk, 0)
|
| 2005 |
if entry and entry > 0 and cur > 0:
|