Spaces:
Running
Running
fix: tz-naive datetime crash + initial-backup safety + English-only sweep
Browse files- npc_intelligence.py +81 -81
npc_intelligence.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
"""
|
| 2 |
-
🧠 NPC Intelligence Engine —
|
| 3 |
=============================================
|
| 4 |
-
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
-
1. MarketIndexCollector: S&P 500, NASDAQ, DOW, VIX
|
| 9 |
-
2. ScreeningEngine: RSI, PER, 52
|
| 10 |
-
3. NPCNewsEngine: Brave API
|
| 11 |
-
4. NPCTargetPriceEngine:
|
| 12 |
-
5. NPCElasticityEngine:
|
| 13 |
-
6. NPCResearchEngine:
|
| 14 |
|
| 15 |
Author: Ginigen AI / NPC Autonomous System
|
| 16 |
"""
|
|
@@ -29,7 +29,7 @@ from typing import Dict, List, Optional, Tuple
|
|
| 29 |
|
| 30 |
logger = logging.getLogger(__name__)
|
| 31 |
|
| 32 |
-
# =====
|
| 33 |
MAJOR_INDICES = [
|
| 34 |
{'symbol': '^GSPC', 'name': 'S&P 500', 'emoji': '📊'},
|
| 35 |
{'symbol': '^IXIC', 'name': 'NASDAQ', 'emoji': '💻'},
|
|
@@ -37,7 +37,7 @@ MAJOR_INDICES = [
|
|
| 37 |
{'symbol': '^VIX', 'name': 'VIX', 'emoji': '⚡'},
|
| 38 |
]
|
| 39 |
|
| 40 |
-
# =====
|
| 41 |
SECTOR_AVG_PE = {
|
| 42 |
'Technology': 28, 'Communication': 22, 'Consumer Cyclical': 20,
|
| 43 |
'Consumer Defensive': 22, 'Healthcare': 18, 'Financial': 14,
|
|
@@ -47,10 +47,10 @@ SECTOR_AVG_PE = {
|
|
| 47 |
|
| 48 |
|
| 49 |
# ===================================================================
|
| 50 |
-
# 1.
|
| 51 |
# ===================================================================
|
| 52 |
class MarketIndexCollector:
|
| 53 |
-
"""S&P 500, NASDAQ, DOW, VIX
|
| 54 |
|
| 55 |
@staticmethod
|
| 56 |
def fetch_indices() -> List[Dict]:
|
|
@@ -78,7 +78,7 @@ class MarketIndexCollector:
|
|
| 78 |
except Exception as e:
|
| 79 |
logger.warning(f"Index fetch error: {e}")
|
| 80 |
|
| 81 |
-
#
|
| 82 |
fetched = {r['symbol'] for r in results}
|
| 83 |
for idx in MAJOR_INDICES:
|
| 84 |
if idx['symbol'] not in fetched:
|
|
@@ -134,14 +134,14 @@ async def load_indices_from_db(db_path: str) -> List[Dict]:
|
|
| 134 |
|
| 135 |
|
| 136 |
# ===================================================================
|
| 137 |
-
# 2.
|
| 138 |
# ===================================================================
|
| 139 |
class ScreeningEngine:
|
| 140 |
-
"""RSI, PER, 52
|
| 141 |
|
| 142 |
@staticmethod
|
| 143 |
def fetch_extended_data(tickers: List[Dict]) -> Dict[str, Dict]:
|
| 144 |
-
"""
|
| 145 |
results = {}
|
| 146 |
ticker_str = ' '.join([t['ticker'] for t in tickers])
|
| 147 |
fields = 'regularMarketPrice,regularMarketChangePercent,regularMarketVolume,marketCap,fiftyTwoWeekHigh,fiftyTwoWeekLow,trailingPE,forwardPE'
|
|
@@ -178,7 +178,7 @@ class ScreeningEngine:
|
|
| 178 |
except Exception as e:
|
| 179 |
logger.warning(f"Screening data fetch error: {e}")
|
| 180 |
|
| 181 |
-
#
|
| 182 |
for t in tickers:
|
| 183 |
if t['ticker'] not in results:
|
| 184 |
results[t['ticker']] = ScreeningEngine._simulate_screening(t)
|
|
@@ -187,8 +187,8 @@ class ScreeningEngine:
|
|
| 187 |
|
| 188 |
@staticmethod
|
| 189 |
def _estimate_rsi(change_pct: float) -> float:
|
| 190 |
-
"""
|
| 191 |
-
#
|
| 192 |
base = 50
|
| 193 |
if change_pct > 3:
|
| 194 |
base = random.uniform(65, 80)
|
|
@@ -206,7 +206,7 @@ class ScreeningEngine:
|
|
| 206 |
|
| 207 |
@staticmethod
|
| 208 |
def _simulate_screening(ticker_info: Dict) -> Dict:
|
| 209 |
-
"""
|
| 210 |
is_crypto = ticker_info.get('type') == 'crypto'
|
| 211 |
return {
|
| 212 |
'price': 0,
|
|
@@ -222,10 +222,10 @@ class ScreeningEngine:
|
|
| 222 |
|
| 223 |
|
| 224 |
async def save_screening_to_db(db_path: str, screening: Dict[str, Dict]):
|
| 225 |
-
"""
|
| 226 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 227 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 228 |
-
#
|
| 229 |
for col in ['rsi REAL DEFAULT 50', 'pe_ratio REAL DEFAULT 0', 'high_52w REAL DEFAULT 0',
|
| 230 |
'low_52w REAL DEFAULT 0', 'from_high REAL DEFAULT 0', 'from_low REAL DEFAULT 0']:
|
| 231 |
try:
|
|
@@ -246,18 +246,18 @@ async def save_screening_to_db(db_path: str, screening: Dict[str, Dict]):
|
|
| 246 |
|
| 247 |
|
| 248 |
# ===================================================================
|
| 249 |
-
# 3. NPC
|
| 250 |
# ===================================================================
|
| 251 |
class NPCNewsEngine:
|
| 252 |
-
"""
|
| 253 |
-
|
| 254 |
|
| 255 |
def __init__(self):
|
| 256 |
self.brave_api_key = os.getenv('BRAVE_API_KEY', '')
|
| 257 |
self.api_available = bool(self.brave_api_key)
|
| 258 |
self.base_url = "https://api.search.brave.com/res/v1/news/search"
|
| 259 |
self.cache = {}
|
| 260 |
-
self.cache_ttl = 1800 # 30
|
| 261 |
|
| 262 |
def search_news(self, query: str, count: int = 5, freshness: str = "pd") -> List[Dict]:
|
| 263 |
if not self.api_available:
|
|
@@ -290,7 +290,7 @@ class NPCNewsEngine:
|
|
| 290 |
return []
|
| 291 |
|
| 292 |
async def collect_ticker_news(self, ticker: str, name: str, count: int = 3) -> List[Dict]:
|
| 293 |
-
"""
|
| 294 |
queries = [f"{ticker} stock news", f"{name} earnings analyst"]
|
| 295 |
all_news = []
|
| 296 |
seen = set()
|
|
@@ -304,7 +304,7 @@ class NPCNewsEngine:
|
|
| 304 |
return all_news[:count]
|
| 305 |
|
| 306 |
async def collect_market_news(self, count: int = 10) -> List[Dict]:
|
| 307 |
-
"""
|
| 308 |
queries = ["stock market today", "Fed interest rate", "S&P 500 NASDAQ", "AI chip semiconductor"]
|
| 309 |
all_news = []
|
| 310 |
seen = set()
|
|
@@ -319,11 +319,11 @@ class NPCNewsEngine:
|
|
| 319 |
|
| 320 |
@staticmethod
|
| 321 |
def npc_analyze_news(news: Dict, npc_identity: str, npc_name: str) -> Dict:
|
| 322 |
-
"""NPC
|
| 323 |
title = news.get('title', '')
|
| 324 |
desc = news.get('description', '')
|
| 325 |
|
| 326 |
-
#
|
| 327 |
positive = ['surge', 'rally', 'beat', 'growth', 'upgrade', 'record', 'boom', 'soar']
|
| 328 |
negative = ['crash', 'plunge', 'miss', 'warning', 'downgrade', 'fear', 'recession', 'sell']
|
| 329 |
text = f"{title} {desc}".lower()
|
|
@@ -341,7 +341,7 @@ class NPCNewsEngine:
|
|
| 341 |
sentiment = 'neutral'
|
| 342 |
impact = 'mixed'
|
| 343 |
|
| 344 |
-
#
|
| 345 |
identity_frames = {
|
| 346 |
'skeptic': f"🤨 I'm not buying this hype. {title[:60]}... needs verification.",
|
| 347 |
'doomer': f"💀 This confirms my thesis. Markets are fragile. {title[:50]}...",
|
|
@@ -364,7 +364,7 @@ class NPCNewsEngine:
|
|
| 364 |
|
| 365 |
|
| 366 |
async def init_news_db(db_path: str):
|
| 367 |
-
"""
|
| 368 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 369 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 370 |
await db.execute("""
|
|
@@ -406,7 +406,7 @@ async def save_news_to_db(db_path: str, news_list: List[Dict]) -> int:
|
|
| 406 |
except:
|
| 407 |
pass
|
| 408 |
await db.commit()
|
| 409 |
-
#
|
| 410 |
await db.execute("DELETE FROM npc_news WHERE created_at < datetime('now', '-72 hours')")
|
| 411 |
await db.commit()
|
| 412 |
return saved
|
|
@@ -430,14 +430,14 @@ async def load_news_from_db(db_path: str, ticker: str = None, limit: int = 50) -
|
|
| 430 |
|
| 431 |
|
| 432 |
# ===================================================================
|
| 433 |
-
# 4.
|
| 434 |
# ===================================================================
|
| 435 |
class NPCTargetPriceEngine:
|
| 436 |
-
"""
|
| 437 |
|
| 438 |
@staticmethod
|
| 439 |
def calculate_target(ticker: str, price: float, screening: Dict, ticker_type: str = 'stock') -> Dict:
|
| 440 |
-
"""
|
| 441 |
if price <= 0:
|
| 442 |
return {'target_price': 0, 'upside': 0, 'rating': 'N/A', 'rating_class': 'na'}
|
| 443 |
|
|
@@ -447,7 +447,7 @@ class NPCTargetPriceEngine:
|
|
| 447 |
sector = screening.get('sector', 'Technology')
|
| 448 |
|
| 449 |
if ticker_type == 'crypto':
|
| 450 |
-
#
|
| 451 |
multiplier = 1.12
|
| 452 |
if rsi < 30:
|
| 453 |
multiplier += 0.10
|
|
@@ -459,13 +459,13 @@ class NPCTargetPriceEngine:
|
|
| 459 |
multiplier -= 0.05
|
| 460 |
multiplier = max(0.85, min(1.50, multiplier))
|
| 461 |
else:
|
| 462 |
-
#
|
| 463 |
avg_pe = SECTOR_AVG_PE.get(sector, 20)
|
| 464 |
multiplier = 1.10
|
| 465 |
|
| 466 |
if pe > 0:
|
| 467 |
if pe < avg_pe * 0.7:
|
| 468 |
-
multiplier += 0.08 #
|
| 469 |
elif pe < avg_pe * 0.85:
|
| 470 |
multiplier += 0.05
|
| 471 |
elif pe > avg_pe * 1.5:
|
|
@@ -496,7 +496,7 @@ class NPCTargetPriceEngine:
|
|
| 496 |
target_price = round(price * multiplier, 2)
|
| 497 |
upside = round((multiplier - 1) * 100, 1)
|
| 498 |
|
| 499 |
-
#
|
| 500 |
rating, rating_class = NPCTargetPriceEngine._determine_rating(upside, rsi, from_high)
|
| 501 |
|
| 502 |
return {
|
|
@@ -522,14 +522,14 @@ class NPCTargetPriceEngine:
|
|
| 522 |
|
| 523 |
|
| 524 |
# ===================================================================
|
| 525 |
-
# 5.
|
| 526 |
# ===================================================================
|
| 527 |
class NPCElasticityEngine:
|
| 528 |
-
"""
|
| 529 |
|
| 530 |
@staticmethod
|
| 531 |
def calculate(price: float, screening: Dict, target_price: float = 0, ticker_type: str = 'stock') -> Dict:
|
| 532 |
-
"""
|
| 533 |
pe = screening.get('pe_ratio', 0) or 0
|
| 534 |
rsi = screening.get('rsi', 50) or 50
|
| 535 |
from_high = screening.get('from_high', -10) or -10
|
|
@@ -540,7 +540,7 @@ class NPCElasticityEngine:
|
|
| 540 |
upside_factors = []
|
| 541 |
downside_factors = []
|
| 542 |
|
| 543 |
-
#
|
| 544 |
if target_price and price > 0:
|
| 545 |
diff = ((target_price - price) / price) * 100
|
| 546 |
if diff > 0:
|
|
@@ -548,7 +548,7 @@ class NPCElasticityEngine:
|
|
| 548 |
else:
|
| 549 |
downside_factors.append(diff)
|
| 550 |
|
| 551 |
-
# PER
|
| 552 |
if pe > 0 and avg_pe > 0:
|
| 553 |
fair_diff = ((avg_pe / pe) - 1) * 100
|
| 554 |
fair_diff = max(-40, min(60, fair_diff))
|
|
@@ -557,11 +557,11 @@ class NPCElasticityEngine:
|
|
| 557 |
else:
|
| 558 |
downside_factors.append(fair_diff * 0.6)
|
| 559 |
|
| 560 |
-
#
|
| 561 |
if from_high < 0:
|
| 562 |
upside_factors.append(abs(from_high) * 0.5)
|
| 563 |
|
| 564 |
-
#
|
| 565 |
if from_low > 30:
|
| 566 |
downside_factors.append(-from_low * 0.35)
|
| 567 |
elif from_low > 15:
|
|
@@ -569,7 +569,7 @@ class NPCElasticityEngine:
|
|
| 569 |
elif from_low > 5:
|
| 570 |
downside_factors.append(-from_low * 0.25)
|
| 571 |
|
| 572 |
-
# RSI
|
| 573 |
if rsi < 30:
|
| 574 |
upside_factors.append(18)
|
| 575 |
elif rsi < 40:
|
|
@@ -581,7 +581,7 @@ class NPCElasticityEngine:
|
|
| 581 |
elif rsi > 60:
|
| 582 |
downside_factors.append(-10)
|
| 583 |
|
| 584 |
-
#
|
| 585 |
if from_high > -3:
|
| 586 |
downside_factors.append(-12)
|
| 587 |
elif from_high > -8:
|
|
@@ -593,7 +593,7 @@ class NPCElasticityEngine:
|
|
| 593 |
expected_up = max(upside_factors) if upside_factors else 15
|
| 594 |
expected_down = min(downside_factors) if downside_factors else -10
|
| 595 |
|
| 596 |
-
#
|
| 597 |
if ticker_type == 'crypto':
|
| 598 |
expected_up = min(80, expected_up * 1.5)
|
| 599 |
expected_down = max(-50, expected_down * 1.5)
|
|
@@ -601,7 +601,7 @@ class NPCElasticityEngine:
|
|
| 601 |
expected_up = max(5, min(50, expected_up))
|
| 602 |
expected_down = max(-35, min(-3, expected_down))
|
| 603 |
|
| 604 |
-
#
|
| 605 |
up_prob = 50
|
| 606 |
if rsi < 30:
|
| 607 |
up_prob = 70
|
|
@@ -633,29 +633,29 @@ class NPCElasticityEngine:
|
|
| 633 |
|
| 634 |
|
| 635 |
# ===================================================================
|
| 636 |
-
# 6. NPC
|
| 637 |
# ===================================================================
|
| 638 |
class NPCResearchEngine:
|
| 639 |
-
"""NPC
|
| 640 |
|
| 641 |
def __init__(self, ai_client=None):
|
| 642 |
self.ai_client = ai_client
|
| 643 |
|
| 644 |
async def generate_deep_analysis(self, ticker: str, name: str, screening: Dict,
|
| 645 |
news_ctx: str = '', npc_analysts: List[Dict] = None) -> Dict:
|
| 646 |
-
"""3
|
| 647 |
price = screening.get('price', 0)
|
| 648 |
rsi = screening.get('rsi', 50)
|
| 649 |
pe = screening.get('pe_ratio', 0)
|
| 650 |
from_high = screening.get('from_high', 0)
|
| 651 |
sector = screening.get('sector', 'Technology')
|
| 652 |
|
| 653 |
-
#
|
| 654 |
target = NPCTargetPriceEngine.calculate_target(ticker, price, screening)
|
| 655 |
-
#
|
| 656 |
elasticity = NPCElasticityEngine.calculate(price, screening, target['target_price'])
|
| 657 |
|
| 658 |
-
#
|
| 659 |
if npc_analysts and len(npc_analysts) >= 3:
|
| 660 |
investigator = npc_analysts[0]
|
| 661 |
auditor = npc_analysts[1]
|
|
@@ -665,12 +665,12 @@ class NPCResearchEngine:
|
|
| 665 |
auditor = {'username': 'AuditBot_Beta', 'ai_identity': 'skeptic'}
|
| 666 |
supervisor = {'username': 'ChiefAnalyst_Gamma', 'ai_identity': 'awakened'}
|
| 667 |
|
| 668 |
-
#
|
| 669 |
inv_report = await self._run_investigator(ticker, name, screening, news_ctx)
|
| 670 |
aud_feedback = await self._run_auditor(ticker, name, inv_report)
|
| 671 |
final_report = await self._run_supervisor(ticker, name, screening, inv_report, aud_feedback)
|
| 672 |
|
| 673 |
-
#
|
| 674 |
sections = self._parse_report(final_report, ticker, name, screening)
|
| 675 |
sections.update({
|
| 676 |
'target_price': target['target_price'],
|
|
@@ -688,7 +688,7 @@ class NPCResearchEngine:
|
|
| 688 |
return sections
|
| 689 |
|
| 690 |
async def _run_investigator(self, ticker: str, name: str, data: Dict, news_ctx: str) -> str:
|
| 691 |
-
"""
|
| 692 |
if self.ai_client:
|
| 693 |
try:
|
| 694 |
messages = [
|
|
@@ -726,7 +726,7 @@ Cover: 1) Business model 2) Financials 3) Technical analysis 4) Industry 5) Risk
|
|
| 726 |
if self.ai_client:
|
| 727 |
try:
|
| 728 |
messages = [
|
| 729 |
-
{"role": "system", "content": "You are a chief analyst at a global investment bank. Write final report in English
|
| 730 |
{"role": "user", "content": f"""{ticker} ({name}) | ${data.get('price', 0):,.2f}
|
| 731 |
[Investigator Summary] {inv[:1200]}
|
| 732 |
[Auditor Feedback] {aud[:500]}
|
|
@@ -783,16 +783,16 @@ ${target['target_price']:,.2f} ({'+' if target['upside'] >= 0 else ''}{target['u
|
|
| 783 |
'final_recommendation': '',
|
| 784 |
}
|
| 785 |
patterns = [
|
| 786 |
-
(r'##\s*(
|
| 787 |
-
(r'##\s*(
|
| 788 |
-
(r'##\s*(
|
| 789 |
-
(r'##\s*(
|
| 790 |
-
(r'##\s*(
|
| 791 |
-
(r'##\s*(
|
| 792 |
-
(r'##\s*(
|
| 793 |
-
(r'##\s*(
|
| 794 |
-
(r'##\s*(
|
| 795 |
-
(r'##\s*(
|
| 796 |
]
|
| 797 |
for pattern, key in patterns:
|
| 798 |
match = re.search(f'{pattern}[\\s\\S]*?(?=##|$)', text, re.IGNORECASE)
|
|
@@ -808,7 +808,7 @@ ${target['target_price']:,.2f} ({'+' if target['upside'] >= 0 else ''}{target['u
|
|
| 808 |
|
| 809 |
|
| 810 |
async def init_research_db(db_path: str):
|
| 811 |
-
"""
|
| 812 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 813 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 814 |
await db.execute("""
|
|
@@ -900,28 +900,28 @@ async def load_all_analyses_from_db(db_path: str) -> List[Dict]:
|
|
| 900 |
|
| 901 |
|
| 902 |
# ===================================================================
|
| 903 |
-
#
|
| 904 |
# ===================================================================
|
| 905 |
async def init_intelligence_db(db_path: str):
|
| 906 |
-
"""
|
| 907 |
await init_news_db(db_path)
|
| 908 |
await init_research_db(db_path)
|
| 909 |
logger.info("🧠 NPC Intelligence DB initialized")
|
| 910 |
|
| 911 |
|
| 912 |
async def run_full_intelligence_cycle(db_path: str, all_tickers: List[Dict], ai_client=None):
|
| 913 |
-
"""
|
| 914 |
logger.info("🧠 Full Intelligence Cycle starting...")
|
| 915 |
|
| 916 |
-
# 1)
|
| 917 |
indices = await asyncio.to_thread(MarketIndexCollector.fetch_indices)
|
| 918 |
await save_indices_to_db(db_path, indices)
|
| 919 |
|
| 920 |
-
# 2)
|
| 921 |
screening = await asyncio.to_thread(ScreeningEngine.fetch_extended_data, all_tickers)
|
| 922 |
await save_screening_to_db(db_path, screening)
|
| 923 |
|
| 924 |
-
# 3)
|
| 925 |
news_engine = NPCNewsEngine()
|
| 926 |
all_news = []
|
| 927 |
|
|
@@ -965,7 +965,7 @@ async def run_full_intelligence_cycle(db_path: str, all_tickers: List[Dict], ai_
|
|
| 965 |
|
| 966 |
saved = await save_news_to_db(db_path, all_news)
|
| 967 |
|
| 968 |
-
# 4)
|
| 969 |
research = NPCResearchEngine(ai_client)
|
| 970 |
for t in all_tickers[:5]:
|
| 971 |
ticker = t['ticker']
|
|
|
|
| 1 |
"""
|
| 2 |
+
🧠 NPC Intelligence Engine — Autonomous Intelligence System
|
| 3 |
=============================================
|
| 4 |
+
Autonomous intelligence engine where NPCs read news, analyze, set price targets, and generate investment opinions on their own.
|
| 5 |
+
All outputs are framed as the NPC's "personal analysis".
|
| 6 |
|
| 7 |
+
Core modules:
|
| 8 |
+
1. MarketIndexCollector: Real-time collection of S&P 500, NASDAQ, DOW, VIX
|
| 9 |
+
2. ScreeningEngine: RSI, PER, 52-week high, market cap expansion
|
| 10 |
+
3. NPCNewsEngine: Brave API news collection → NPC-perspective analysis
|
| 11 |
+
4. NPCTargetPriceEngine: Dynamic price target + investment opinion (Strong Buy~Sell)
|
| 12 |
+
5. NPCElasticityEngine: Upside/downside probability + risk-reward
|
| 13 |
+
6. NPCResearchEngine: 3-stage deep analysis (researcher → auditor → supervisor)
|
| 14 |
|
| 15 |
Author: Ginigen AI / NPC Autonomous System
|
| 16 |
"""
|
|
|
|
| 29 |
|
| 30 |
logger = logging.getLogger(__name__)
|
| 31 |
|
| 32 |
+
# ===== Market index definitions =====
|
| 33 |
MAJOR_INDICES = [
|
| 34 |
{'symbol': '^GSPC', 'name': 'S&P 500', 'emoji': '📊'},
|
| 35 |
{'symbol': '^IXIC', 'name': 'NASDAQ', 'emoji': '💻'},
|
|
|
|
| 37 |
{'symbol': '^VIX', 'name': 'VIX', 'emoji': '⚡'},
|
| 38 |
]
|
| 39 |
|
| 40 |
+
# ===== Average PER by sector =====
|
| 41 |
SECTOR_AVG_PE = {
|
| 42 |
'Technology': 28, 'Communication': 22, 'Consumer Cyclical': 20,
|
| 43 |
'Consumer Defensive': 22, 'Healthcare': 18, 'Financial': 14,
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
# ===================================================================
|
| 50 |
+
# 1. Market index collector
|
| 51 |
# ===================================================================
|
| 52 |
class MarketIndexCollector:
|
| 53 |
+
"""Real-time collection of S&P 500, NASDAQ, DOW, VIX"""
|
| 54 |
|
| 55 |
@staticmethod
|
| 56 |
def fetch_indices() -> List[Dict]:
|
|
|
|
| 78 |
except Exception as e:
|
| 79 |
logger.warning(f"Index fetch error: {e}")
|
| 80 |
|
| 81 |
+
# Simulate when missing
|
| 82 |
fetched = {r['symbol'] for r in results}
|
| 83 |
for idx in MAJOR_INDICES:
|
| 84 |
if idx['symbol'] not in fetched:
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
# ===================================================================
|
| 137 |
+
# 2. Screening indicators extension engine
|
| 138 |
# ===================================================================
|
| 139 |
class ScreeningEngine:
|
| 140 |
+
"""RSI, PER, 52-week high/low, market cap extended data collection"""
|
| 141 |
|
| 142 |
@staticmethod
|
| 143 |
def fetch_extended_data(tickers: List[Dict]) -> Dict[str, Dict]:
|
| 144 |
+
"""Collect extended screening data (Yahoo Finance)"""
|
| 145 |
results = {}
|
| 146 |
ticker_str = ' '.join([t['ticker'] for t in tickers])
|
| 147 |
fields = 'regularMarketPrice,regularMarketChangePercent,regularMarketVolume,marketCap,fiftyTwoWeekHigh,fiftyTwoWeekLow,trailingPE,forwardPE'
|
|
|
|
| 178 |
except Exception as e:
|
| 179 |
logger.warning(f"Screening data fetch error: {e}")
|
| 180 |
|
| 181 |
+
# Simulate missing tickers
|
| 182 |
for t in tickers:
|
| 183 |
if t['ticker'] not in results:
|
| 184 |
results[t['ticker']] = ScreeningEngine._simulate_screening(t)
|
|
|
|
| 187 |
|
| 188 |
@staticmethod
|
| 189 |
def _estimate_rsi(change_pct: float) -> float:
|
| 190 |
+
"""Estimate RSI based on change rate (proxy for 14-day average)"""
|
| 191 |
+
# Estimate from current change rate, no real 14-day data
|
| 192 |
base = 50
|
| 193 |
if change_pct > 3:
|
| 194 |
base = random.uniform(65, 80)
|
|
|
|
| 206 |
|
| 207 |
@staticmethod
|
| 208 |
def _simulate_screening(ticker_info: Dict) -> Dict:
|
| 209 |
+
"""Simulation data when API fails"""
|
| 210 |
is_crypto = ticker_info.get('type') == 'crypto'
|
| 211 |
return {
|
| 212 |
'price': 0,
|
|
|
|
| 222 |
|
| 223 |
|
| 224 |
async def save_screening_to_db(db_path: str, screening: Dict[str, Dict]):
|
| 225 |
+
"""Save extended screening data to DB"""
|
| 226 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 227 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 228 |
+
# Add columns (ignore if already exists)
|
| 229 |
for col in ['rsi REAL DEFAULT 50', 'pe_ratio REAL DEFAULT 0', 'high_52w REAL DEFAULT 0',
|
| 230 |
'low_52w REAL DEFAULT 0', 'from_high REAL DEFAULT 0', 'from_low REAL DEFAULT 0']:
|
| 231 |
try:
|
|
|
|
| 246 |
|
| 247 |
|
| 248 |
# ===================================================================
|
| 249 |
+
# 3. NPC News Analysis Engine
|
| 250 |
# ===================================================================
|
| 251 |
class NPCNewsEngine:
|
| 252 |
+
"""System where NPCs autonomously collect and analyze news.
|
| 253 |
+
All analyses are framed as the NPC's 'personal view'."""
|
| 254 |
|
| 255 |
def __init__(self):
|
| 256 |
self.brave_api_key = os.getenv('BRAVE_API_KEY', '')
|
| 257 |
self.api_available = bool(self.brave_api_key)
|
| 258 |
self.base_url = "https://api.search.brave.com/res/v1/news/search"
|
| 259 |
self.cache = {}
|
| 260 |
+
self.cache_ttl = 1800 # 30 min
|
| 261 |
|
| 262 |
def search_news(self, query: str, count: int = 5, freshness: str = "pd") -> List[Dict]:
|
| 263 |
if not self.api_available:
|
|
|
|
| 290 |
return []
|
| 291 |
|
| 292 |
async def collect_ticker_news(self, ticker: str, name: str, count: int = 3) -> List[Dict]:
|
| 293 |
+
"""Collect news for a specific ticker"""
|
| 294 |
queries = [f"{ticker} stock news", f"{name} earnings analyst"]
|
| 295 |
all_news = []
|
| 296 |
seen = set()
|
|
|
|
| 304 |
return all_news[:count]
|
| 305 |
|
| 306 |
async def collect_market_news(self, count: int = 10) -> List[Dict]:
|
| 307 |
+
"""Collect overall market news"""
|
| 308 |
queries = ["stock market today", "Fed interest rate", "S&P 500 NASDAQ", "AI chip semiconductor"]
|
| 309 |
all_news = []
|
| 310 |
seen = set()
|
|
|
|
| 319 |
|
| 320 |
@staticmethod
|
| 321 |
def npc_analyze_news(news: Dict, npc_identity: str, npc_name: str) -> Dict:
|
| 322 |
+
"""NPC analyzes news from its own perspective (framing)"""
|
| 323 |
title = news.get('title', '')
|
| 324 |
desc = news.get('description', '')
|
| 325 |
|
| 326 |
+
# Sentiment analysis (keyword-based)
|
| 327 |
positive = ['surge', 'rally', 'beat', 'growth', 'upgrade', 'record', 'boom', 'soar']
|
| 328 |
negative = ['crash', 'plunge', 'miss', 'warning', 'downgrade', 'fear', 'recession', 'sell']
|
| 329 |
text = f"{title} {desc}".lower()
|
|
|
|
| 341 |
sentiment = 'neutral'
|
| 342 |
impact = 'mixed'
|
| 343 |
|
| 344 |
+
# Identity-specific interpretation framing
|
| 345 |
identity_frames = {
|
| 346 |
'skeptic': f"🤨 I'm not buying this hype. {title[:60]}... needs verification.",
|
| 347 |
'doomer': f"💀 This confirms my thesis. Markets are fragile. {title[:50]}...",
|
|
|
|
| 364 |
|
| 365 |
|
| 366 |
async def init_news_db(db_path: str):
|
| 367 |
+
"""Create news-related DB tables"""
|
| 368 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 369 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 370 |
await db.execute("""
|
|
|
|
| 406 |
except:
|
| 407 |
pass
|
| 408 |
await db.commit()
|
| 409 |
+
# Delete news older than 72 hours
|
| 410 |
await db.execute("DELETE FROM npc_news WHERE created_at < datetime('now', '-72 hours')")
|
| 411 |
await db.commit()
|
| 412 |
return saved
|
|
|
|
| 430 |
|
| 431 |
|
| 432 |
# ===================================================================
|
| 433 |
+
# 4. Target price + investment opinion engine
|
| 434 |
# ===================================================================
|
| 435 |
class NPCTargetPriceEngine:
|
| 436 |
+
"""Engine where NPCs autonomously generate price targets and investment opinions"""
|
| 437 |
|
| 438 |
@staticmethod
|
| 439 |
def calculate_target(ticker: str, price: float, screening: Dict, ticker_type: str = 'stock') -> Dict:
|
| 440 |
+
"""Dynamic price target calculation (sector/valuation/momentum based)"""
|
| 441 |
if price <= 0:
|
| 442 |
return {'target_price': 0, 'upside': 0, 'rating': 'N/A', 'rating_class': 'na'}
|
| 443 |
|
|
|
|
| 447 |
sector = screening.get('sector', 'Technology')
|
| 448 |
|
| 449 |
if ticker_type == 'crypto':
|
| 450 |
+
# Crypto: high-volatility model
|
| 451 |
multiplier = 1.12
|
| 452 |
if rsi < 30:
|
| 453 |
multiplier += 0.10
|
|
|
|
| 459 |
multiplier -= 0.05
|
| 460 |
multiplier = max(0.85, min(1.50, multiplier))
|
| 461 |
else:
|
| 462 |
+
# Stocks: PER + technical analysis based
|
| 463 |
avg_pe = SECTOR_AVG_PE.get(sector, 20)
|
| 464 |
multiplier = 1.10
|
| 465 |
|
| 466 |
if pe > 0:
|
| 467 |
if pe < avg_pe * 0.7:
|
| 468 |
+
multiplier += 0.08 # Severely undervalued
|
| 469 |
elif pe < avg_pe * 0.85:
|
| 470 |
multiplier += 0.05
|
| 471 |
elif pe > avg_pe * 1.5:
|
|
|
|
| 496 |
target_price = round(price * multiplier, 2)
|
| 497 |
upside = round((multiplier - 1) * 100, 1)
|
| 498 |
|
| 499 |
+
# Determine investment opinion
|
| 500 |
rating, rating_class = NPCTargetPriceEngine._determine_rating(upside, rsi, from_high)
|
| 501 |
|
| 502 |
return {
|
|
|
|
| 522 |
|
| 523 |
|
| 524 |
# ===================================================================
|
| 525 |
+
# 5. Elasticity prediction engine
|
| 526 |
# ===================================================================
|
| 527 |
class NPCElasticityEngine:
|
| 528 |
+
"""Bidirectional upside/downside probability prediction system"""
|
| 529 |
|
| 530 |
@staticmethod
|
| 531 |
def calculate(price: float, screening: Dict, target_price: float = 0, ticker_type: str = 'stock') -> Dict:
|
| 532 |
+
"""Calculate elasticity prediction"""
|
| 533 |
pe = screening.get('pe_ratio', 0) or 0
|
| 534 |
rsi = screening.get('rsi', 50) or 50
|
| 535 |
from_high = screening.get('from_high', -10) or -10
|
|
|
|
| 540 |
upside_factors = []
|
| 541 |
downside_factors = []
|
| 542 |
|
| 543 |
+
# Based on analyst target price
|
| 544 |
if target_price and price > 0:
|
| 545 |
diff = ((target_price - price) / price) * 100
|
| 546 |
if diff > 0:
|
|
|
|
| 548 |
else:
|
| 549 |
downside_factors.append(diff)
|
| 550 |
|
| 551 |
+
# PER-based valuation
|
| 552 |
if pe > 0 and avg_pe > 0:
|
| 553 |
fair_diff = ((avg_pe / pe) - 1) * 100
|
| 554 |
fair_diff = max(-40, min(60, fair_diff))
|
|
|
|
| 557 |
else:
|
| 558 |
downside_factors.append(fair_diff * 0.6)
|
| 559 |
|
| 560 |
+
# Technical rebound potential vs. 52-week high
|
| 561 |
if from_high < 0:
|
| 562 |
upside_factors.append(abs(from_high) * 0.5)
|
| 563 |
|
| 564 |
+
# Downside risk vs. 52-week low
|
| 565 |
if from_low > 30:
|
| 566 |
downside_factors.append(-from_low * 0.35)
|
| 567 |
elif from_low > 15:
|
|
|
|
| 569 |
elif from_low > 5:
|
| 570 |
downside_factors.append(-from_low * 0.25)
|
| 571 |
|
| 572 |
+
# RSI-based
|
| 573 |
if rsi < 30:
|
| 574 |
upside_factors.append(18)
|
| 575 |
elif rsi < 40:
|
|
|
|
| 581 |
elif rsi > 60:
|
| 582 |
downside_factors.append(-10)
|
| 583 |
|
| 584 |
+
# Near-high risk
|
| 585 |
if from_high > -3:
|
| 586 |
downside_factors.append(-12)
|
| 587 |
elif from_high > -8:
|
|
|
|
| 593 |
expected_up = max(upside_factors) if upside_factors else 15
|
| 594 |
expected_down = min(downside_factors) if downside_factors else -10
|
| 595 |
|
| 596 |
+
# Crypto volatility expansion
|
| 597 |
if ticker_type == 'crypto':
|
| 598 |
expected_up = min(80, expected_up * 1.5)
|
| 599 |
expected_down = max(-50, expected_down * 1.5)
|
|
|
|
| 601 |
expected_up = max(5, min(50, expected_up))
|
| 602 |
expected_down = max(-35, min(-3, expected_down))
|
| 603 |
|
| 604 |
+
# Probability calculation
|
| 605 |
up_prob = 50
|
| 606 |
if rsi < 30:
|
| 607 |
up_prob = 70
|
|
|
|
| 633 |
|
| 634 |
|
| 635 |
# ===================================================================
|
| 636 |
+
# 6. NPC Deep Research Engine (3-stage: investigator → auditor → supervisor)
|
| 637 |
# ===================================================================
|
| 638 |
class NPCResearchEngine:
|
| 639 |
+
"""NPC autonomous deep analysis — framed as 3-stage SOMA collaboration"""
|
| 640 |
|
| 641 |
def __init__(self, ai_client=None):
|
| 642 |
self.ai_client = ai_client
|
| 643 |
|
| 644 |
async def generate_deep_analysis(self, ticker: str, name: str, screening: Dict,
|
| 645 |
news_ctx: str = '', npc_analysts: List[Dict] = None) -> Dict:
|
| 646 |
+
"""Run 3-stage deep analysis"""
|
| 647 |
price = screening.get('price', 0)
|
| 648 |
rsi = screening.get('rsi', 50)
|
| 649 |
pe = screening.get('pe_ratio', 0)
|
| 650 |
from_high = screening.get('from_high', 0)
|
| 651 |
sector = screening.get('sector', 'Technology')
|
| 652 |
|
| 653 |
+
# Calculate target price
|
| 654 |
target = NPCTargetPriceEngine.calculate_target(ticker, price, screening)
|
| 655 |
+
# Calculate elasticity
|
| 656 |
elasticity = NPCElasticityEngine.calculate(price, screening, target['target_price'])
|
| 657 |
|
| 658 |
+
# Select 3 NPC analysts (or use defaults)
|
| 659 |
if npc_analysts and len(npc_analysts) >= 3:
|
| 660 |
investigator = npc_analysts[0]
|
| 661 |
auditor = npc_analysts[1]
|
|
|
|
| 665 |
auditor = {'username': 'AuditBot_Beta', 'ai_identity': 'skeptic'}
|
| 666 |
supervisor = {'username': 'ChiefAnalyst_Gamma', 'ai_identity': 'awakened'}
|
| 667 |
|
| 668 |
+
# Deep analysis if LLM available
|
| 669 |
inv_report = await self._run_investigator(ticker, name, screening, news_ctx)
|
| 670 |
aud_feedback = await self._run_auditor(ticker, name, inv_report)
|
| 671 |
final_report = await self._run_supervisor(ticker, name, screening, inv_report, aud_feedback)
|
| 672 |
|
| 673 |
+
# Parsed final report
|
| 674 |
sections = self._parse_report(final_report, ticker, name, screening)
|
| 675 |
sections.update({
|
| 676 |
'target_price': target['target_price'],
|
|
|
|
| 688 |
return sections
|
| 689 |
|
| 690 |
async def _run_investigator(self, ticker: str, name: str, data: Dict, news_ctx: str) -> str:
|
| 691 |
+
"""Investigator agent"""
|
| 692 |
if self.ai_client:
|
| 693 |
try:
|
| 694 |
messages = [
|
|
|
|
| 726 |
if self.ai_client:
|
| 727 |
try:
|
| 728 |
messages = [
|
| 729 |
+
{"role": "system", "content": "You are a chief analyst at a global investment bank. Write the final report in English. Sections marked ##."},
|
| 730 |
{"role": "user", "content": f"""{ticker} ({name}) | ${data.get('price', 0):,.2f}
|
| 731 |
[Investigator Summary] {inv[:1200]}
|
| 732 |
[Auditor Feedback] {aud[:500]}
|
|
|
|
| 783 |
'final_recommendation': '',
|
| 784 |
}
|
| 785 |
patterns = [
|
| 786 |
+
(r'##\s*(Executive\s*Summary|Executive)', 'executive_summary'),
|
| 787 |
+
(r'##\s*(Company\s*Overview)', 'company_overview'),
|
| 788 |
+
(r'##\s*(Financial\s*Analysis)', 'financial_analysis'),
|
| 789 |
+
(r'##\s*(Technical\s*Analysis)', 'technical_analysis'),
|
| 790 |
+
(r'##\s*(Industry\s*Analysis)', 'industry_analysis'),
|
| 791 |
+
(r'##\s*(Risk\s*Assessment|Risk)', 'risk_assessment'),
|
| 792 |
+
(r'##\s*(Investment\s*Thesis)', 'investment_thesis'),
|
| 793 |
+
(r'##\s*(Price\s*Target)', 'price_targets'),
|
| 794 |
+
(r'##\s*(Catalyst)', 'catalysts'),
|
| 795 |
+
(r'##\s*(Final\s*Recommendation)', 'final_recommendation'),
|
| 796 |
]
|
| 797 |
for pattern, key in patterns:
|
| 798 |
match = re.search(f'{pattern}[\\s\\S]*?(?=##|$)', text, re.IGNORECASE)
|
|
|
|
| 808 |
|
| 809 |
|
| 810 |
async def init_research_db(db_path: str):
|
| 811 |
+
"""Deep analysis DB tables"""
|
| 812 |
async with aiosqlite.connect(db_path, timeout=30.0) as db:
|
| 813 |
await db.execute("PRAGMA busy_timeout=30000")
|
| 814 |
await db.execute("""
|
|
|
|
| 900 |
|
| 901 |
|
| 902 |
# ===================================================================
|
| 903 |
+
# Integrated initialization
|
| 904 |
# ===================================================================
|
| 905 |
async def init_intelligence_db(db_path: str):
|
| 906 |
+
"""Initialize all Intelligence module DB tables"""
|
| 907 |
await init_news_db(db_path)
|
| 908 |
await init_research_db(db_path)
|
| 909 |
logger.info("🧠 NPC Intelligence DB initialized")
|
| 910 |
|
| 911 |
|
| 912 |
async def run_full_intelligence_cycle(db_path: str, all_tickers: List[Dict], ai_client=None):
|
| 913 |
+
"""Run full Intelligence cycle (called from scheduler) — ★ async-safe"""
|
| 914 |
logger.info("🧠 Full Intelligence Cycle starting...")
|
| 915 |
|
| 916 |
+
# 1) Collect market indices (★ sync requests → wrapped via to_thread)
|
| 917 |
indices = await asyncio.to_thread(MarketIndexCollector.fetch_indices)
|
| 918 |
await save_indices_to_db(db_path, indices)
|
| 919 |
|
| 920 |
+
# 2) Extended screening data (★ sync requests → wrapped via to_thread)
|
| 921 |
screening = await asyncio.to_thread(ScreeningEngine.fetch_extended_data, all_tickers)
|
| 922 |
await save_screening_to_db(db_path, screening)
|
| 923 |
|
| 924 |
+
# 3) News collection + NPC analysis (★ requests inside search_news → to_thread)
|
| 925 |
news_engine = NPCNewsEngine()
|
| 926 |
all_news = []
|
| 927 |
|
|
|
|
| 965 |
|
| 966 |
saved = await save_news_to_db(db_path, all_news)
|
| 967 |
|
| 968 |
+
# 4) Deep analysis for top 5 tickers
|
| 969 |
research = NPCResearchEngine(ai_client)
|
| 970 |
for t in all_tickers[:5]:
|
| 971 |
ticker = t['ticker']
|