AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning

This commit is contained in:
Krilly
2026-03-04 13:29:22 +00:00
parent 29a98137a7
commit 57dd294675
13706 changed files with 2114953 additions and 237629 deletions
@@ -0,0 +1,309 @@
#!/usr/bin/env python3
"""
Bubble-O-Meter: 米国株式市場のバブル度を多面的に評価するスクリプト
8つの指標を0-2点で評価し、合計スコア(0-16点)でバブル度を判定:
- 0-4: 正常域
- 5-8: 警戒域
- 9-12: 熱狂域
- 13-16: 臨界域
使用方法:
python bubble_scorer.py --ticker SPY --period 1y
"""
import argparse
import json
from datetime import datetime, timedelta
from typing import Dict, List, Tuple
class BubbleScorer:
"""バブルスコアリングシステム"""
def __init__(self):
self.indicators = {
"mass_penetration": {
"name": "大衆浸透度",
"weight": 2,
"description": "非投資家層からの推奨・言及"
},
"media_saturation": {
"name": "メディア飽和",
"weight": 2,
"description": "検索・SNS・メディア露出の急騰"
},
"new_accounts": {
"name": "新規参入",
"weight": 2,
"description": "口座開設・資金流入の加速"
},
"new_issuance": {
"name": "新規発行氾濫",
"weight": 2,
"description": "IPO/SPAC/関連商品の乱立"
},
"leverage": {
"name": "レバレッジ",
"weight": 2,
"description": "証拠金・信用・資金調達レートの偏り"
},
"price_acceleration": {
"name": "価格加速度",
"weight": 2,
"description": "リターンが歴史分布上位に到達"
},
"valuation_disconnect": {
"name": "バリュエーション逸脱",
"weight": 2,
"description": "ファンダ説明が物語一辺倒に"
},
"breadth_expansion": {
"name": "相関と幅",
"weight": 2,
"description": "低質銘柄まで全面高"
}
}
def calculate_score(self, scores: Dict[str, int]) -> Dict:
"""
各指標のスコアから総合評価を計算
Args:
scores: 各指標のスコア辞書 (0-2点)
Returns:
評価結果の辞書
"""
total_score = sum(scores.values())
max_score = len(self.indicators) * 2
# バブル段階の判定
if total_score <= 4:
phase = "正常域"
risk_level = ""
action = "通常通りの投資戦略を継続"
elif total_score <= 8:
phase = "警戒域"
risk_level = ""
action = "部分利確の開始、新規ポジションのサイズ縮小"
elif total_score <= 12:
phase = "熱狂域"
risk_level = ""
action = "階段状利確の加速、ATRトレーリングストップ厳格化、総リスク予算30-50%削減"
else:
phase = "臨界域"
risk_level = "極めて高"
action = "大幅な利確またはフルヘッジ、新規参入停止、反転確認後のショートポジション検討"
# Minskyフェーズの推定
minsky_phase = self._estimate_minsky_phase(scores, total_score)
return {
"timestamp": datetime.now().isoformat(),
"total_score": total_score,
"max_score": max_score,
"percentage": round(total_score / max_score * 100, 1),
"phase": phase,
"risk_level": risk_level,
"minsky_phase": minsky_phase,
"recommended_action": action,
"indicator_scores": scores,
"detailed_indicators": self._format_indicator_details(scores)
}
def _estimate_minsky_phase(self, scores: Dict[str, int], total: int) -> str:
"""Minsky/Kindlebergerフェーズの推定"""
mass_pen = scores.get("mass_penetration", 0)
media = scores.get("media_saturation", 0)
price_acc = scores.get("price_acceleration", 0)
if total <= 4:
return "Displacement/Early Boom (きっかけ・初期拡張)"
elif total <= 8:
if media >= 1 and price_acc >= 1:
return "Boom (拡張期)"
else:
return "Displacement/Early Boom (きっかけ・初期拡張)"
elif total <= 12:
if mass_pen >= 2 and media >= 2:
return "Euphoria (熱狂期) - FOMOが制度化"
else:
return "Late Boom/Early Euphoria (拡張後期・熱狂初期)"
else:
if mass_pen >= 2:
return "Peak Euphoria/Profit Taking (熱狂ピーク・利確開始) - 反転間近"
else:
return "Euphoria (熱狂期)"
def _format_indicator_details(self, scores: Dict[str, int]) -> List[Dict]:
"""指標の詳細情報をフォーマット"""
details = []
for key, value in scores.items():
indicator = self.indicators.get(key, {})
status = "🔴高" if value == 2 else "🟡中" if value == 1 else "🟢低"
details.append({
"indicator": indicator.get("name", key),
"score": value,
"status": status,
"description": indicator.get("description", "")
})
return details
def get_scoring_guidelines(self) -> str:
"""各指標のスコアリングガイドラインを返す"""
guidelines = """
## バブルスコアリング・ガイドライン
### 1. 大衆浸透度 (Mass Penetration)
- 0点: 専門家・投資家層のみの議論
- 1点: 一般層にも認知されるが、まだ投資対象としては限定的
- 2点: 非投資家(タクシー運転手、美容師、家族)が積極的に推奨・言及
### 2. メディア飽和 (Media Saturation)
- 0点: 通常レベルの報道・検索トレンド
- 1点: 検索トレンド、SNS言及が平常の2-3倍
- 2点: テレビ特集、雑誌表紙、検索トレンド急騰(平常の5倍以上)
### 3. 新規参入 (New Accounts & Inflows)
- 0点: 通常レベルの口座開設・入金
- 1点: 口座開設が前年比50-100%
- 2点: 口座開設が前年比200%以上、「初めての投資」層の大量流入
### 4. 新規発行氾濫 (New Issuance Flood)
- 0点: 通常レベルのIPO/商品組成
- 1点: IPO/SPAC/関連ETFが前年比50%以上増加
- 2点: 低質なIPO乱立、「○○関連」ファンド・ETFの濫造
### 5. レバレッジ (Leverage Indicators)
- 0点: 証拠金残高・信用評価損益が正常範囲
- 1点: 証拠金残高が過去平均の1.5倍、先物ポジション偏り
- 2点: 証拠金残高が過去最高更新、資金調達レート高止まり、極端なポジション偏り
### 6. 価格加速度 (Price Acceleration)
- 0点: 年率リターンが歴史分布の中央値付近
- 1点: 年率リターンが過去90パーセンタイル超
- 2点: 年率リターンが過去95-99パーセンタイル、または加速度(2階微分)が正で増加
### 7. バリュエーション逸脱 (Valuation Disconnect)
- 0点: ファンダメンタルで合理的に説明可能
- 1点: 高バリュエーションだが「成長期待」で一応説明可能
- 2点: 説明が完全に「物語」「革命」「パラダイムシフト」に依存、「今回は違う」
### 8. 相関と幅 (Breadth & Correlation)
- 0点: 一部のリーダー銘柄のみ上昇
- 1点: セクター全体に波及、mid-capまで上昇
- 2点: 低質・low-cap銘柄まで全面高、「ゾンビ企業」も上昇(最後の買い手参入)
"""
return guidelines
def format_output(self, result: Dict) -> str:
"""結果を読みやすくフォーマット"""
output = f"""
{'='*60}
🔍 米国市場バブル度評価 - Bubble-O-Meter
{'='*60}
評価日時: {result['timestamp']}
【総合スコア】
{result['total_score']}/{result['max_score']}点 ({result['percentage']}%)
【市場フェーズ】
現在: {result['phase']} (リスク: {result['risk_level']})
Minskyフェーズ: {result['minsky_phase']}
【推奨アクション】
{result['recommended_action']}
{'='*60}
【指標別スコア】
{'='*60}
"""
for detail in result['detailed_indicators']:
output += f"\n{detail['status']} {detail['indicator']}: {detail['score']}/2点\n"
output += f" └─ {detail['description']}\n"
output += f"\n{'='*60}\n"
return output
def manual_assessment() -> Dict[str, int]:
"""対話型の手動評価"""
scorer = BubbleScorer()
print("\n" + "="*60)
print("🔍 米国市場バブル度評価 - Manual Assessment")
print("="*60)
print("\n各指標を0-2点で評価してください:")
print(scorer.get_scoring_guidelines())
scores = {}
for key, indicator in scorer.indicators.items():
while True:
try:
score = int(input(f"\n{indicator['name']} (0-2): "))
if 0 <= score <= 2:
scores[key] = score
break
else:
print("0, 1, 2 のいずれかを入力してください")
except ValueError:
print("数値を入力してください")
return scores
def main():
parser = argparse.ArgumentParser(
description="米国市場のバブル度を評価するBubble-O-Meter"
)
parser.add_argument(
"--manual",
action="store_true",
help="対話型の手動評価モード"
)
parser.add_argument(
"--scores",
type=str,
help="JSON形式のスコア文字列 (例: '{\"mass_penetration\":2,\"media_saturation\":1,...}')"
)
parser.add_argument(
"--output",
choices=["text", "json"],
default="text",
help="出力形式"
)
args = parser.parse_args()
scorer = BubbleScorer()
# スコアの取得
if args.manual:
scores = manual_assessment()
elif args.scores:
try:
scores = json.loads(args.scores)
except json.JSONDecodeError:
print("エラー: 無効なJSON形式です")
return 1
else:
print("エラー: --manual または --scores を指定してください")
print("\nガイドラインを表示:")
print(scorer.get_scoring_guidelines())
return 1
# 評価の実行
result = scorer.calculate_score(scores)
# 出力
if args.output == "json":
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(scorer.format_output(result))
return 0
if __name__ == "__main__":
exit(main())