seawolf2357 commited on
Commit
3ca3de8
·
verified ·
1 Parent(s): fa891a4

fix: tz-naive datetime crash + initial-backup safety + English-only sweep

Browse files
Files changed (1) hide show
  1. npc_intelligence.py +81 -81
npc_intelligence.py CHANGED
@@ -1,16 +1,16 @@
1
  """
2
- 🧠 NPC Intelligence Engine — 자율 지능 시스템
3
  =============================================
4
- NPC가 스스로 뉴스를 읽고, 분석하고, 목표가를 설정하고, 투자의견을 생성하는 자율 지능 엔진.
5
- 모든 출력은 NPC "개인적 분석"으로 포장됨.
6
 
7
- 핵심 모듈:
8
- 1. MarketIndexCollector: S&P 500, NASDAQ, DOW, VIX 실시간 수집
9
- 2. ScreeningEngine: RSI, PER, 52주고점, 시가총액 확장
10
- 3. NPCNewsEngine: Brave API 뉴스 수집 → NPC 관점 분석
11
- 4. NPCTargetPriceEngine: 동적 목표가 + 투자의견(Strong Buy~Sell)
12
- 5. NPCElasticityEngine: 상승/하락 확률 + 리스크-리워드
13
- 6. NPCResearchEngine: 조사자감사자→감독자 3단계 심층 분석
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
- # ===== 섹터별 평균 PER =====
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
- """확장 스크리닝 데이터 수집 (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,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
- """변동률 기반 RSI 추정 (14일 평균 대용)"""
191
- # 실제 14일 데이터 없이 현재 변동률로 추정
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
- """API 실패 시뮬레이션 데이터"""
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
- """확장 스크리닝 데이터 DB 저장"""
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
- """NPC가 자율적으로 뉴스를 수집하고 분석하는 시스템.
253
- 모든 분석은 NPC의 '개인적 견해' 포장됨."""
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
- # NPC 성격별 해석 프레이밍
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
- """뉴스 관련 DB 테이블 생성"""
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
- # 24시간 이상 뉴스 삭제
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
- """NPC가 자율적으로 목표가와 투자의견을 생성하는 엔진"""
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
- # 주식: PER + 기술적 분석 기반
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
- # 52주 고점 대비 기술적 반등 여력
561
  if from_high < 0:
562
  upside_factors.append(abs(from_high) * 0.5)
563
 
564
- # 52주 저점 대비 하락 리스크
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 심층 리서치 엔진 (조사자감사자감독자 3단계)
637
  # ===================================================================
638
  class NPCResearchEngine:
639
- """NPC 자율 심층 분석 — 3단계 SOMA 협업으로 프레이밍"""
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
- # NPC 분석가 3 선정 (또는 기본값)
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
- # LLM 사용 가능 심층 분석
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 with 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,16 +783,16 @@ ${target['target_price']:,.2f} ({'+' if target['upside'] >= 0 else ''}{target['u
783
  'final_recommendation': '',
784
  }
785
  patterns = [
786
- (r'##\s*(핵심\s*요약|Executive\s*Summary|Executive)', 'executive_summary'),
787
- (r'##\s*(회사\s*개요|Company\s*Overview)', 'company_overview'),
788
- (r'##\s*(재무\s*분석|Financial\s*Analysis)', 'financial_analysis'),
789
- (r'##\s*(기술적\s*분석|Technical\s*Analysis)', 'technical_analysis'),
790
- (r'##\s*(산업\s*분석|Industry\s*Analysis)', 'industry_analysis'),
791
- (r'##\s*(리스크|Risk\s*Assessment|Risk)', 'risk_assessment'),
792
- (r'##\s*(투자\s*논리|Investment\s*Thesis)', 'investment_thesis'),
793
- (r'##\s*(목표\s*주가|Price\s*Target)', 'price_targets'),
794
- (r'##\s*(카탈리스트|Catalyst)', 'catalysts'),
795
- (r'##\s*(최종\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,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
- """심층 분석 DB 테이블"""
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
- """Intelligence 모듈 전체 DB 초기화"""
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
- """전체 Intelligence 사이클 실행 (스케줄러에서 호출) — ★ 비동기 안전"""
914
  logger.info("🧠 Full Intelligence Cycle starting...")
915
 
916
- # 1) 시장 지수 수집 (★ 동기 requests → to_thread로 비동기 래핑)
917
  indices = await asyncio.to_thread(MarketIndexCollector.fetch_indices)
918
  await save_indices_to_db(db_path, indices)
919
 
920
- # 2) 확장 스크리닝 데이터 (★ 동기 requests → 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) 뉴스 수집 + NPC 분석 (★ search_news 내부 requests → to_thread)
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) 상위 5개 종목 심층 분석
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 analysisframed 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']