-
Notifications
You must be signed in to change notification settings - Fork 108
Expand file tree
/
Copy pathtrump_monitor.py
More file actions
813 lines (674 loc) · 29.3 KB
/
Copy pathtrump_monitor.py
File metadata and controls
813 lines (674 loc) · 29.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
#!/usr/bin/env python3
"""
川普密碼 — 即時監控 + 多組預測引擎
每 5 分鐘抓最新推文,跑 12 組預測模型,追蹤哪組命中
用法:
python3 trump_monitor.py # 即時監控(持續運行)
python3 trump_monitor.py --backtest # 用歷史資料回測所有預測組
python3 trump_monitor.py --status # 看目前各組預測命中率
"""
import json
import csv
import logging
import re
import sys
import time
import urllib.request
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from pathlib import Path
from utils import est_hour, market_session, emotion_score
BASE = Path(__file__).parent
ARCHIVE_URL = "https://ix.cnn.io/data/truth-social/truth_archive.csv"
DATA = BASE / "data"
PREDICTIONS_FILE = DATA / "predictions_log.json"
SCORES_FILE = DATA / "prediction_scores.json"
ALERTS_FILE = BASE / "alerts_log.json"
LAST_POST_FILE = BASE / "last_seen_post.txt"
# ============================================================
# 信號分類器
# ============================================================
def classify_signals(content):
"""把一篇貼文分類成多種信號"""
cl = content.lower()
signals = set()
# 政策信號
if any(w in cl for w in ['tariff', 'tariffs', 'duty', 'duties', 'reciprocal']):
signals.add('TARIFF')
if any(w in cl for w in ['deal', 'agreement', 'negotiate', 'talks', 'signed']):
signals.add('DEAL')
if any(w in cl for w in ['pause', 'delay', 'exempt', 'exception', 'suspend', 'postpone']):
signals.add('RELIEF')
if any(w in cl for w in ['immediately', 'effective', 'hereby', 'executive order', 'just signed', 'i have directed']):
signals.add('ACTION')
if any(w in cl for w in ['ban', 'block', 'restrict', 'sanction', 'punish']):
signals.add('THREAT')
# 情緒信號
if any(w in cl for w in ['stock market', 'all time high', 'record high', 'dow', 'nasdaq', 'market up']):
signals.add('MARKET_BRAG')
if any(w in cl for w in ['fake news', 'corrupt', 'fraud', 'witch hunt', 'disgrace', 'hoax']):
signals.add('ATTACK')
if any(w in cl for w in ['great', 'tremendous', 'incredible', 'historic', 'beautiful', 'amazing']):
signals.add('POSITIVE')
# 地緣信號
if any(w in cl for w in ['china', 'chinese', 'beijing', 'xi jinping']):
signals.add('CHINA')
if any(w in cl for w in ['iran', 'iranian', 'tehran']):
signals.add('IRAN')
if any(w in cl for w in ['russia', 'russian', 'putin', 'ukraine', 'zelensky']):
signals.add('RUSSIA')
# 新密碼追蹤
if 'save america act' in cl:
signals.add('NEW_SAVE_ACT')
if 'president djt' in cl:
signals.add('SIG_DJT')
if 'president of the united states' in cl:
signals.add('SIG_POTUS')
if 'thank you for your attention' in cl:
signals.add('SIG_TYFA')
return signals
# est_hour, market_session, emotion_score 從 utils.py 匯入
# ============================================================
# 12 組預測模型
# ============================================================
class PredictionEngine:
"""多組預測模型同時運行"""
def __init__(self):
self.models = {
# --- A 組:單信號模型 ---
'A1_tariff_bearish': {
'name': '盤中關稅→隔天跌',
'desc': '盤中出現 TARIFF 信號 ≥2 次 → 預測隔天 S&P 收跌',
'direction': 'SHORT',
'hold': 1,
'trigger': self._trigger_a1,
},
'A2_deal_bullish': {
'name': 'DEAL 出現→隔天漲',
'desc': '出現 DEAL 信號且無 TARIFF → 預測隔天 S&P 收漲',
'direction': 'LONG',
'hold': 1,
'trigger': self._trigger_a2,
},
'A3_relief_rocket': {
'name': '盤前 RELIEF→當天飆',
'desc': '盤前出現暫緩/豁免 → 預測當天 S&P 大漲',
'direction': 'LONG',
'hold': 0, # 0 = 當天
'trigger': self._trigger_a3,
},
# --- B 組:多信號組合模型 ---
'B1_triple_signal': {
'name': '三信號齊發→買3天',
'desc': 'TARIFF+DEAL+RELIEF 同天 → 底部信號,3 天內漲',
'direction': 'LONG',
'hold': 3,
'trigger': self._trigger_b1,
},
'B2_tariff_to_deal': {
'name': '連3天關稅→出現Deal→轉折',
'desc': '連續 3 天 TARIFF 後出現 DEAL → 轉折買入',
'direction': 'LONG',
'hold': 2,
'trigger': self._trigger_b2,
},
'B3_action_pre': {
'name': '盤前 ACTION+正面情緒→漲',
'desc': '盤前簽署行政命令 + 正面情緒 → 看多',
'direction': 'LONG',
'hold': 1,
'trigger': self._trigger_b3,
},
# --- C 組:行為異常模型 ---
'C1_burst_silence': {
'name': '正面轟炸→沉默→做多',
'desc': '1 小時 ≥5 篇後沉默 ≥3 小時,且 burst 期間正面情緒 > 負面 → 做多',
'direction': 'LONG',
'hold': 1,
'trigger': self._trigger_c1,
},
'C2_brag_top': {
'name': '炫耀股市→短期到頂',
'desc': '一天炫耀股市 ≥3 次 → 短期高點',
'direction': 'SHORT',
'hold': 2,
'trigger': self._trigger_c2,
},
'C3_night_alert': {
'name': '深夜關稅推文→開盤跳空',
'desc': '深夜/凌晨提關稅 → 隔天開盤跳空',
'direction': 'SHORT',
'hold': 1,
'trigger': self._trigger_c3,
},
# --- D 組:換碼偵測模型 ---
'D1_new_phrase': {
'name': '新口號出現→波動加大',
'desc': '出現過去 30 天沒出現過的新政策詞彙 → 波動率上升',
'direction': 'VOLATILE',
'hold': 3,
'trigger': self._trigger_d1,
},
'D2_sig_change': {
'name': '簽名切換→正式聲明',
'desc': '使用 POTUS 級簽名 → 重大政策即將落地',
'direction': 'VOLATILE',
'hold': 2,
'trigger': self._trigger_d2,
},
'D3_volume_spike': {
'name': '發文量暴增→恐慌底部',
'desc': '日發文量 > 過去7天均值×2 → 恐慌極值',
'direction': 'LONG',
'hold': 3,
'trigger': self._trigger_d3,
},
}
# 各模型累計成績
self.scores = self._load_scores()
# 歷史上下文
self.context = {
'prev_days': [], # 前 7 天的每日摘要
'recent_phrases': set(), # 近 30 天出現過的 phrase
}
# 已觸發的 (model_id, date) 組合,用於去重
self._triggered_set = set()
# 從現有 trades 初始化已觸發記錄
for mid, s in self.scores.items():
for t in s.get('trades', []):
if t.get('date'):
self._triggered_set.add((mid, t['date']))
def _load_scores(self):
if SCORES_FILE.exists():
with open(SCORES_FILE, encoding='utf-8') as f:
return json.load(f)
return {m: {'predictions': 0, 'correct': 0, 'wrong': 0, 'pending': 0,
'total_return': 0, 'trades': []}
for m in self.models}
def save_scores(self):
with open(SCORES_FILE, 'w', encoding='utf-8') as f:
json.dump(self.scores, f, ensure_ascii=False, indent=2)
# --- 觸發條件 ---
def _trigger_a1(self, day_summary):
"""盤中 TARIFF ≥2"""
return day_summary.get('open_tariff', 0) >= 2
def _trigger_a2(self, day_summary):
"""DEAL 出現且 TARIFF=0"""
return day_summary.get('deal', 0) >= 1 and day_summary.get('tariff', 0) == 0
def _trigger_a3(self, day_summary):
"""盤前 RELIEF"""
return day_summary.get('pre_relief', 0) >= 1
def _trigger_b1(self, day_summary):
"""三信號齊發"""
return (day_summary.get('tariff', 0) >= 1 and
day_summary.get('deal', 0) >= 1 and
day_summary.get('relief', 0) >= 1)
def _trigger_b2(self, day_summary):
"""連3天TARIFF後出現DEAL"""
prev = self.context.get('prev_days', [])
if len(prev) < 3:
return False
streak = all(d.get('tariff', 0) >= 1 for d in prev[-3:])
return streak and day_summary.get('deal', 0) >= 1
def _trigger_b3(self, day_summary):
"""盤前ACTION + 正面"""
return (day_summary.get('pre_action', 0) >= 1 and
day_summary.get('positive', 0) >= 2)
def _trigger_c1(self, day_summary):
"""轟炸後沉默(含情緒過濾)
2026-03-15 修正:burst 期間若負面情緒(attack/threat/tariff)多於
正面情緒(positive/deal/relief/market_brag),代表沉默是在消化壞消息,
不應做多。只有正面 > 負面時才觸發。
"""
if not day_summary.get('burst_then_silence', False):
return False
# 情緒過濾:負面轟炸後的沉默 ≠ 利多
attack_count = day_summary.get('burst_attack_count', 0)
positive_count = day_summary.get('burst_positive_count', 0)
if attack_count > positive_count:
return False
return True
def _trigger_c2(self, day_summary):
"""炫耀股市 ≥3"""
return day_summary.get('market_brag', 0) >= 3
def _trigger_c3(self, day_summary):
"""深夜關稅"""
return day_summary.get('night_tariff', 0) >= 1
def _trigger_d1(self, day_summary):
"""新口號出現"""
return day_summary.get('new_phrase_detected', False)
def _trigger_d2(self, day_summary):
"""POTUS 級簽名"""
return day_summary.get('sig_potus', 0) >= 1
def _trigger_d3(self, day_summary):
"""發文量暴增"""
prev = self.context.get('prev_days', [])
if len(prev) < 7:
return False
avg = sum(d.get('post_count', 0) for d in prev[-7:]) / 7
return avg > 0 and day_summary.get('post_count', 0) > avg * 2
def run_predictions(self, day_summary, date):
"""對今天的數據跑所有模型,產生預測"""
predictions = []
for model_id, model in self.models.items():
try:
if model['trigger'](day_summary):
# Finding #4: 檢查今天是否已經觸發過此模型(日期去重)
if (model_id, date) in self._triggered_set:
continue # 今天已觸發過,跳過
if model_id not in self.scores:
self.scores[model_id] = {
'predictions': 0, 'correct': 0, 'wrong': 0,
'pending': 0, 'total_return': 0, 'trades': []
}
pred = {
'model_id': model_id,
'model_name': model['name'],
'date_signal': date,
'direction': model['direction'],
'hold_days': model['hold'],
'status': 'PENDING',
'created_at': datetime.now(timezone.utc).isoformat(),
'day_summary': {k: v for k, v in day_summary.items()
if not isinstance(v, (set, list))},
}
predictions.append(pred)
# 更新計分 + 記錄已觸發
self.scores[model_id]['predictions'] += 1
self.scores[model_id]['pending'] += 1
self._triggered_set.add((model_id, date))
except Exception as e:
logging.exception(f"Model {model_id} failed: {e}")
return predictions
# ============================================================
# 資料抓取
# ============================================================
def fetch_latest_posts(limit=50):
"""從 CNN Archive 抓最新推文"""
try:
req = urllib.request.Request(ARCHIVE_URL)
with urllib.request.urlopen(req, timeout=30) as resp:
content = resp.read().decode('utf-8')
reader = csv.DictReader(content.splitlines())
rows = list(reader)
# 只取最新 N 篇有文字、非 RT 的
results = []
for row in rows[:limit * 3]:
if row['content'].strip() and not row['content'].strip().startswith('RT @'):
results.append({
'id': row['id'],
'created_at': row['created_at'],
'content': row['content'],
'url': row.get('url', ''),
})
if len(results) >= limit:
break
return results
except Exception as e:
print(f" ⚠️ 抓取失敗: {e}")
return []
def summarize_day(day_posts):
"""把一天的推文彙整成 day_summary"""
summary = defaultdict(int)
summary['post_count'] = len(day_posts)
summary['contents'] = []
intervals = []
burst_then_silence = False
for i, p in enumerate(day_posts):
content = p['content']
signals = classify_signals(content)
session = market_session(p['created_at'])
h, m = est_hour(p['created_at'])
for sig in signals:
summary[sig.lower()] += 1
if session == 'PRE_MARKET':
summary[f'pre_{sig.lower()}'] += 1
elif session == 'MARKET_OPEN':
summary[f'open_{sig.lower()}'] += 1
# 深夜推文
if h < 5 or h >= 23:
if 'TARIFF' in signals:
summary['night_tariff'] += 1
summary['emotion_sum'] += emotion_score(content)
summary['contents'].append(content[:80])
# 計算間隔
if i > 0:
dt1 = datetime.fromisoformat(day_posts[i-1]['created_at'].replace('Z', '+00:00'))
dt2 = datetime.fromisoformat(p['created_at'].replace('Z', '+00:00'))
gap = (dt2 - dt1).total_seconds() / 60
intervals.append(gap)
# 偵測轟炸→沉默
# 2026-03-15 修正:同時記錄 burst 期間的正負面情緒計數,供 C1 過濾用
if intervals:
burst_count = sum(1 for g in intervals if g < 5)
silence = max(intervals) if intervals else 0
if burst_count >= 3 and silence >= 180:
summary['burst_then_silence'] = True
# 統計 burst 期間(間隔 < 5 分鐘的連續推文)的正面/負面信號
burst_attack = 0
burst_positive = 0
for idx_iv, gap in enumerate(intervals):
if gap < 5:
# intervals[i] 對應 day_posts[i] 與 day_posts[i+1] 之間的間隔
# 把這兩篇都算進 burst
for pidx in (idx_iv, idx_iv + 1):
if pidx < len(day_posts):
sigs = classify_signals(day_posts[pidx]['content'])
if 'ATTACK' in sigs or 'THREAT' in sigs or 'TARIFF' in sigs:
burst_attack += 1
if 'POSITIVE' in sigs or 'DEAL' in sigs or 'RELIEF' in sigs or 'MARKET_BRAG' in sigs:
burst_positive += 1
summary['burst_attack_count'] = burst_attack
summary['burst_positive_count'] = burst_positive
# 簽名偵測
for p in day_posts:
c = p['content']
if 'PRESIDENT OF THE UNITED STATES' in c:
summary['sig_potus'] += 1
if 'President DJT' in c:
summary['sig_djt'] += 1
summary['avg_emotion'] = summary['emotion_sum'] / max(len(day_posts), 1)
return dict(summary)
# ============================================================
# 回測模式
# ============================================================
def run_backtest():
"""用歷史資料回測所有 12 組預測模型"""
print("=" * 90)
print("🔬 回測模式:用歷史資料驗證 12 組預測模型")
print("=" * 90)
# 載入資料
with open(BASE / "clean_president.json", encoding='utf-8') as f:
posts = json.load(f)
with open(DATA / "market_SP500.json", encoding='utf-8') as f:
sp500 = json.load(f)
sp_by_date = {r['date']: r for r in sp500}
originals = sorted(
[p for p in posts if p['has_text'] and not p['is_retweet']],
key=lambda p: p['created_at']
)
# 按天分組
daily_posts = defaultdict(list)
for p in originals:
daily_posts[p['created_at'][:10]].append(p)
engine = PredictionEngine()
all_predictions = []
sorted_dates = sorted(daily_posts.keys())
for idx, date in enumerate(sorted_dates):
day_summary = summarize_day(daily_posts[date])
# 更新上下文
engine.context['prev_days'] = [
summarize_day(daily_posts.get(sorted_dates[j], []))
for j in range(max(0, idx-7), idx)
]
# 跑預測
preds = engine.run_predictions(day_summary, date)
# 驗證預測
for pred in preds:
hold = pred['hold_days']
direction = pred['direction']
# 找入場/出場日
td = date
if td not in sp_by_date:
dt = datetime.strptime(td, '%Y-%m-%d')
for i in range(1, 5):
d = (dt + timedelta(days=i)).strftime('%Y-%m-%d')
if d in sp_by_date:
td = d
break
else:
continue
if hold == 0:
# 當天
if td in sp_by_date:
sp = sp_by_date[td]
ret = (sp['close'] - sp['open']) / sp['open'] * 100
else:
continue
else:
# N天後
entry_day = td
exit_day = entry_day
for _ in range(hold):
dt = datetime.strptime(exit_day, '%Y-%m-%d')
for i in range(1, 6):
d = (dt + timedelta(days=i)).strftime('%Y-%m-%d')
if d in sp_by_date:
exit_day = d
break
if exit_day not in sp_by_date or entry_day not in sp_by_date:
continue
entry_p = sp_by_date[entry_day]['open']
exit_p = sp_by_date[exit_day]['close']
ret = (exit_p - entry_p) / entry_p * 100
# 判斷預測是否正確
if direction == 'LONG':
correct = ret > 0
elif direction == 'SHORT':
correct = ret < 0
elif direction == 'VOLATILE':
correct = abs(ret) > 0.5 # 波動超過 0.5%
else:
correct = False
pred['actual_return'] = round(ret, 3)
pred['correct'] = correct
pred['status'] = 'VERIFIED'
# 更新計分
mid = pred['model_id']
if mid in engine.scores:
engine.scores[mid]['pending'] = max(0, engine.scores[mid].get('pending', 0) - 1)
if correct:
engine.scores[mid]['correct'] += 1
else:
engine.scores[mid]['wrong'] += 1
engine.scores[mid]['total_return'] += ret
engine.scores[mid]['trades'].append({
'date': date, 'return': round(ret, 3), 'correct': correct
})
all_predictions.extend(preds)
# === 打印成績單 ===
print(f"\n📊 12 組預測模型成績單")
print(f"{'='*90}")
print(f" {'模型':4s} {'名稱':25s} | {'預測':>4s} | {'命中':>4s} | {'命中率':>6s} | {'平均報酬':>8s} | {'累積':>8s} | 判定")
print(f" {'-'*4} {'-'*25}-+-{'-'*4}-+-{'-'*4}-+-{'-'*6}-+-{'-'*8}-+-{'-'*8}-+-{'-'*6}")
rankings = []
for mid, model in engine.models.items():
s = engine.scores.get(mid, {})
total = s.get('predictions', 0)
correct = s.get('correct', 0)
wrong = s.get('wrong', 0)
total_ret = s.get('total_return', 0)
if total == 0:
continue
hit_rate = correct / total * 100 if total > 0 else 0
avg_ret = total_ret / total if total > 0 else 0
# 判定
if hit_rate >= 60 and avg_ret > 0:
verdict = "⭐有效"
elif hit_rate >= 55:
verdict = "🟡待觀察"
elif hit_rate >= 50:
verdict = "➡️中性"
else:
verdict = "❌無效"
rankings.append((mid, model['name'], total, correct, hit_rate, avg_ret, total_ret, verdict))
# 按命中率排序
rankings.sort(key=lambda x: (-x[4], -x[5]))
for mid, name, total, correct, hit_rate, avg_ret, total_ret, verdict in rankings:
print(f" {mid:4s} {name:25s} | {total:4d} | {correct:4d} | {hit_rate:5.1f}% | {avg_ret:+.3f}% | {total_ret:+.2f}% | {verdict}")
# 存檔
engine.save_scores()
# 存所有預測
with open(PREDICTIONS_FILE, 'w', encoding='utf-8') as f:
json.dump(all_predictions, f, ensure_ascii=False, indent=2)
print(f"\n💾 預測記錄存入 {PREDICTIONS_FILE.name}")
print(f"💾 成績存入 {SCORES_FILE.name}")
# === 最佳組合 ===
print(f"\n{'='*90}")
print("🏆 最佳組合策略:取命中率 Top 3 模型同時確認才動手")
print("=" * 90)
top3 = [r[0] for r in rankings[:3]]
print(f" Top 3: {', '.join(top3)}")
# 找三個模型同一天都觸發的日子
pred_by_date = defaultdict(set)
pred_ret_by_date = {}
for p in all_predictions:
if p.get('status') == 'VERIFIED':
pred_by_date[p['date_signal']].add(p['model_id'])
pred_ret_by_date[(p['date_signal'], p['model_id'])] = p.get('actual_return', 0)
combo_days = []
for date, models in pred_by_date.items():
overlap = set(top3) & models
if len(overlap) >= 2: # 至少 2 個 Top 模型同意
rets = [pred_ret_by_date.get((date, m), 0) for m in overlap]
avg_r = sum(rets) / len(rets) if rets else 0
combo_days.append((date, len(overlap), avg_r, overlap))
if combo_days:
combo_days.sort(key=lambda x: x[0])
wins = sum(1 for _, _, r, _ in combo_days if r > 0)
total_r = sum(r for _, _, r, _ in combo_days)
avg_combo = total_r / len(combo_days)
print(f" 同時觸發天數: {len(combo_days)}")
print(f" 勝率: {wins/len(combo_days)*100:.1f}%")
print(f" 平均報酬: {avg_combo:+.3f}%")
print(f"\n 明細:")
for date, n_models, ret, models in combo_days:
arrow = "✅" if ret > 0 else "❌"
print(f" {date} | {n_models}模型同意 | {ret:+.3f}% {arrow} | {','.join(sorted(models))}")
return engine
# ============================================================
# 即時監控模式
# ============================================================
def run_monitor():
"""即時監控(每 5 分鐘)"""
print("=" * 90)
print("🔴 川普密碼即時監控 — 啟動中")
print(f" 更新頻率: 每 5 分鐘")
print(f" 資料來源: CNN Truth Social Archive")
print(f" 預測模型: 12 組同時運行")
print("=" * 90)
engine = PredictionEngine()
# 讀取上次看到的最新推文
last_seen = ""
if LAST_POST_FILE.exists():
last_seen = LAST_POST_FILE.read_text().strip()
cycle = 0
while True:
cycle += 1
now = datetime.now(timezone.utc)
est_h, est_m = est_hour(now.isoformat())
print(f"\n{'─'*60}")
print(f" 第 {cycle} 輪 | UTC {now.strftime('%H:%M')} | EST {est_h:02d}:{est_m:02d}")
# 抓最新推文
new_posts = fetch_latest_posts(20)
if not new_posts:
print(" ⚠️ 無法抓取,5 分鐘後重試")
time.sleep(300)
continue
latest_id = new_posts[0]['id']
latest_time = new_posts[0]['created_at']
# 檢查是否有新推文
if latest_id == last_seen:
print(f" 💤 沒有新推文 (最新: {latest_time[:16]})")
time.sleep(300)
continue
# 有新推文!
new_count = 0
for p in new_posts:
if p['id'] == last_seen:
break
new_count += 1
print(f" 🆕 發現 {new_count} 篇新推文!")
# 顯示新推文
for p in new_posts[:new_count]:
signals = classify_signals(p['content'])
session = market_session(p['created_at'])
emo = emotion_score(p['content'])
h, m = est_hour(p['created_at'])
signal_str = ' '.join(f"[{s}]" for s in sorted(signals)) if signals else '(無信號)'
print(f"\n 📝 EST {h:02d}:{m:02d} | {session} | 情緒:{emo:.0f}")
print(f" 信號: {signal_str}")
print(f" 內容: {p['content'][:120]}...")
# 按今天分組彙整
today = now.strftime('%Y-%m-%d')
today_posts = [p for p in new_posts if p['created_at'][:10] == today]
if today_posts:
day_summary = summarize_day(today_posts)
# 跑預測
preds = engine.run_predictions(day_summary, today)
if preds:
print(f"\n 🎯 觸發 {len(preds)} 個預測模型:")
for pred in preds:
dir_icon = {'LONG': '📈看多', 'SHORT': '📉看空', 'VOLATILE': '🌊波動'}
print(f" [{pred['model_id']}] {pred['model_name']}")
print(f" → {dir_icon.get(pred['direction'], '?')} | 持有 {pred['hold_days']} 天")
else:
print(f"\n 😴 今天暫無模型觸發")
# 更新最新 ID
last_seen = latest_id
LAST_POST_FILE.write_text(latest_id)
# 顯示各模型累計成績
print(f"\n 📊 模型即時成績:")
for mid, s in engine.scores.items():
total = s['predictions']
if total > 0:
correct = s['correct']
rate = correct / max(total - s['pending'], 1) * 100
print(f" {mid:20s} | {total}次 | 命中{rate:.0f}%")
engine.save_scores()
print(f"\n ⏳ 等待 5 分鐘...")
time.sleep(300)
# ============================================================
# 狀態查看
# ============================================================
def show_status():
"""顯示各模型目前成績"""
print("=" * 90)
print("📊 川普密碼 — 各模型預測成績")
print("=" * 90)
if not SCORES_FILE.exists():
print(" 尚未有成績,請先跑 --backtest")
return
with open(SCORES_FILE, encoding='utf-8') as f:
scores = json.load(f)
engine = PredictionEngine()
print(f"\n {'模型':25s} | {'預測':>4s} | {'命中':>4s} | {'失敗':>4s} | {'命中率':>6s} | {'累積報酬':>8s}")
print(f" {'-'*25}-+-{'-'*4}-+-{'-'*4}-+-{'-'*4}-+-{'-'*6}-+-{'-'*8}")
for mid, model in engine.models.items():
s = scores.get(mid, {})
total = s.get('predictions', 0)
correct = s.get('correct', 0)
wrong = s.get('wrong', 0)
total_ret = s.get('total_return', 0)
rate = correct / max(total - s.get('pending', 0), 1) * 100
print(f" {model['name']:25s} | {total:4d} | {correct:4d} | {wrong:4d} | {rate:5.1f}% | {total_ret:+.2f}%")
# ============================================================
# 主程式
# ============================================================
if __name__ == '__main__':
if '--backtest' in sys.argv:
run_backtest()
elif '--status' in sys.argv:
show_status()
elif '--once' in sys.argv:
# 只跑一次(不迴圈)
print("🔍 單次掃描...")
new_posts = fetch_latest_posts(30)
if new_posts:
today = new_posts[0]['created_at'][:10]
today_posts = [p for p in new_posts if p['created_at'][:10] == today]
day_summary = summarize_day(today_posts)
engine = PredictionEngine()
preds = engine.run_predictions(day_summary, today)
print(f"\n📝 今天 ({today}) {len(today_posts)} 篇推文")
print(f"🎯 觸發 {len(preds)} 個預測:")
for pred in preds:
dir_icon = {'LONG': '📈看多', 'SHORT': '📉看空', 'VOLATILE': '🌊波動'}
print(f" [{pred['model_id']}] {pred['model_name']} → {dir_icon.get(pred['direction'], '?')}")
# 顯示今天的信號
print(f"\n📊 今日信號:")
for k, v in sorted(day_summary.items()):
if isinstance(v, (int, float)) and v > 0 and k != 'post_count':
print(f" {k}: {v}")
else:
run_monitor()