
Stanley Druckenmiller Investment
- 1k installs
- 2.5k repo stars
- Updated July 26, 2026
- tradermonty/claude-trading-skills
stanley-druckenmiller-investment is a report-synthesis skill that aggregates multiple trading strategy skills into Stanley Druckenmiller-style conviction reports for developers and quants who need structured exposure gui
About
stanley-druckenmiller-investment is a conviction-report generator in tradermonty/claude-trading-skills that loads required and optional strategy skills and produces a Druckenmiller Strategy Synthesizer Report via report_generator.py. The output includes a conviction dashboard with a score out of 100, zone classification, recommended exposure range, and strongest/weakest component breakdowns. Developers reach for this skill when evaluating whether to commit capital, calibrate position sizing, or validate trading-feature designs against a multi-signal framework. The skill treats upstream strategy skills as composable inputs rather than running standalone market analysis.
- Generates a full Druckenmiller Strategy Synthesizer Report with Conviction Dashboard, Pattern Classification, and 7-Comp
- Calculates conviction score out of 100 with zone, exposure range, strongest/weakest components and recommended actions
- Detects investment patterns with match strength percentages and detailed component weightings
- Combines multiple loaded skills (required + optional) into one templated markdown investment thesis
- Outputs ready-to-use conviction guidance and recommended actions for decision making
Stanley Druckenmiller Investment by the numbers
- 1,030 all-time installs (skills.sh)
- +55 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #448 of 3,301 Productivity & Planning skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tradermonty/claude-trading-skills --skill stanley-druckenmiller-investmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1k |
|---|---|
| repo stars | ★ 2.5k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 26, 2026 |
| Repository | tradermonty/claude-trading-skills ↗ |
How do you synthesize trading conviction before committing capital?
Synthesize Stanley Druckenmiller-style investment conviction reports from multiple strategy skills before committing capital or building trading features.
Who is it for?
Developers building trading systems or evaluating investment theses who want a structured conviction score before writing execution code or placing trades.
Skip if: Developers seeking live market data feeds, order execution, or generic portfolio tracking without a multi-strategy synthesis workflow.
When should I use this skill?
The user wants a Druckenmiller-style conviction report, exposure recommendation, or multi-strategy synthesis before committing capital or building trading features.
What you get
Conviction dashboard report, exposure range recommendation, and strongest/weakest component scores
- Druckenmiller Strategy Synthesizer Report
- Conviction dashboard with exposure range
By the numbers
- Conviction score reported on a 0–100 scale
- Aggregates required plus optional upstream strategy skills as report inputs
Files
Druckenmiller Strategy Synthesizer
Purpose
Synthesize outputs from 8 upstream analysis skills (5 required + 3 optional) into a single composite conviction score (0-100), classify the market into one of 4 Druckenmiller patterns, and generate actionable allocation recommendations. This is a meta-skill that consumes structured JSON outputs from other skills — it requires no API keys of its own.
When to Use This Skill
English:
- User asks "What's my overall conviction?" or "How should I be positioned?"
- User wants a unified view synthesizing breadth, uptrend, top risk, macro, and FTD signals
- User asks about Druckenmiller-style portfolio positioning
- User requests strategy synthesis after running individual analysis skills
- User asks "Should I increase or decrease exposure?"
- User wants pattern classification (policy pivot, distortion, contrarian, wait)
Japanese:
- 「総合的な市場判断は?」「今のポジショニングは?」
- ブレッドス、アップトレンド、天井リスク、マクロの統合判断
- 「エクスポージャーを増やすべき?減らすべき?」
- 「ドラッケンミラー分析を実行して」
- 個別スキル実行後の戦略統合レポート
---
Input Requirements
Required Skills (5)
| # | Skill | JSON Prefix | Role |
|---|---|---|---|
| 1 | Market Breadth Analyzer | market_breadth_ | Market participation breadth |
| 2 | Uptrend Analyzer | uptrend_analysis_ | Sector uptrend ratios |
| 3 | Market Top Detector | market_top_ | Distribution / top risk (defense) |
| 4 | Macro Regime Detector | macro_regime_ | Macro regime transition (1-2Y structure) |
| 5 | FTD Detector | ftd_detector_ | Bottom confirmation / re-entry (offense) |
Optional Skills (3)
| # | Skill | JSON Prefix | Role |
|---|---|---|---|
| 6 | VCP Screener | vcp_screener_ | Momentum stock setups (VCP) |
| 7 | Theme Detector | theme_detector_ | Theme / sector momentum |
| 8 | CANSLIM Screener | canslim_screener_ | Growth stock setups + M(Market Direction) |
Run the required skills first. The synthesizer reads their JSON output from reports/.
---
Execution Workflow
Phase 1: Verify Prerequisites
Check that the 5 required skill JSON reports exist in reports/ and are recent (< 72 hours). If any are missing, run the corresponding skill first.
Phase 2: Execute Strategy Synthesizer
python3 skills/stanley-druckenmiller-investment/scripts/strategy_synthesizer.py \
--reports-dir reports/ \
--output-dir reports/ \
--max-age 72The script will: 1. Load and validate all upstream skill JSON reports 2. Extract normalized signals from each skill 3. Calculate 7 component scores (weighted 0-100) 4. Compute composite conviction score 5. Classify into one of 4 Druckenmiller patterns 6. Generate target allocation and position sizing 7. Output JSON and Markdown reports
Phase 3: Present Results
Present the generated Markdown report, highlighting:
- Conviction score and zone
- Detected pattern and match strength
- Strongest and weakest components
- Target allocation (equity/bonds/alternatives/cash)
- Position sizing parameters
- Relevant Druckenmiller principle
Phase 4: Provide Druckenmiller Context
Load appropriate reference documents to provide philosophical context:
- High conviction: Emphasize concentration and "fat pitch" principles
- Low conviction: Emphasize capital preservation and patience
- Pattern-specific: Apply relevant case study from
references/case-studies.md
---
7-Component Scoring System
| # | Component | Weight | Source Skill(s) | Key Signal |
|---|---|---|---|---|
| 1 | Market Structure | 18% | Breadth + Uptrend | Market participation health |
| 2 | Distribution Risk | 18% | Market Top (inverted) | Institutional selling risk |
| 3 | Bottom Confirmation | 12% | FTD Detector | Re-entry signal after correction |
| 4 | Macro Alignment | 18% | Macro Regime | Regime favorability |
| 5 | Theme Quality | 12% | Theme Detector | Sector momentum health |
| 6 | Setup Availability | 10% | VCP + CANSLIM | Quality stock setups |
| 7 | Signal Convergence | 12% | All 5 required | Cross-skill agreement |
4 Pattern Classifications
| Pattern | Trigger Conditions | Druckenmiller Principle |
|---|---|---|
| Policy Pivot Anticipation | Transitional regime + high transition probability | "Focus on central banks and liquidity" |
| Unsustainable Distortion | Top risk >= 60 + contraction/inflationary regime | "How much you lose when wrong matters most" |
| Extreme Sentiment Contrarian | FTD confirmed + high top risk + bearish breadth | "Most money made in bear markets" |
| Wait & Observe | Low conviction + mixed signals (default) | "When you don't see it, don't swing" |
Conviction Zone Mapping
| Score | Zone | Exposure | Guidance |
|---|---|---|---|
| 80-100 | Maximum Conviction | 90-100% | Fat pitch - swing hard |
| 60-79 | High Conviction | 70-90% | Standard risk management |
| 40-59 | Moderate Conviction | 50-70% | Reduce position sizes |
| 20-39 | Low Conviction | 20-50% | Preserve capital, minimal risk |
| 0-19 | Capital Preservation | 0-20% | Maximum defense |
---
Output Files
druckenmiller_strategy_YYYY-MM-DD_HHMMSS.json— Structured analysis datadruckenmiller_strategy_YYYY-MM-DD_HHMMSS.md— Human-readable report
API Requirements
None. This skill reads JSON outputs from other skills. No API keys required.
Reference Documents
references/investment-philosophy.md
- Core Druckenmiller principles: concentration, capital preservation, 18-month horizon
- Quantitative rules: daily vol targets, max position sizing
- Load when providing philosophical context for conviction assessment
references/market-analysis-guide.md
- Signal-to-action mapping framework
- Macro regime interpretation for allocation decisions
- Load when explaining component scores or allocation rationale
references/case-studies.md
- Historical examples: 1992 GBP, 2000 tech bubble, 2008 crisis
- Pattern classification examples with actual market conditions
- Load when user asks about historical parallels
references/conviction_matrix.md
- Quantitative signal-to-action mapping tables
- Market Top Zone x Macro Regime matrix
- Load when user needs precise exposure numbers for specific signal combinations
When to Load References
- First use: Load
investment-philosophy.mdfor framework understanding - Allocation questions: Load
market-analysis-guide.md+conviction_matrix.md - Historical context: Load
case-studies.md - Regular execution: References not needed — script handles scoring
---
Relationship to Other Skills
| Skill | Relationship | Time Horizon |
|---|---|---|
| Market Breadth Analyzer | Input (required) | Current snapshot |
| Uptrend Analyzer | Input (required) | Current snapshot |
| Market Top Detector | Input (required) | 2-8 weeks tactical |
| Macro Regime Detector | Input (required) | 1-2 years structural |
| FTD Detector | Input (required) | Days-weeks event |
| VCP Screener | Input (optional) | Setup-specific |
| Theme Detector | Input (optional) | Weeks-months thematic |
| CANSLIM Screener | Input (optional) | Setup-specific |
| This Skill | Synthesizer | Unified conviction |
<!-- Design Reference Template for report_generator.py NOT rendered at runtime. Update both files together. -->
Druckenmiller Strategy Synthesizer Report
Generated: {{metadata.generated_at}} Input Skills: {{metadata.skills_loaded}} loaded ({{metadata.required_count}} required + {{metadata.optional_count}} optional)
---
1. Conviction Dashboard
| Metric | Value |
|---|---|
| Conviction Score | {{conviction.conviction_score}}/100 |
| Zone | {{conviction.zone}} |
| Recommended Exposure | {{conviction.exposure_range}} |
| Strongest Component | {{conviction.strongest_component.label}} ({{conviction.strongest_component.score}}) |
| Weakest Component | {{conviction.weakest_component.label}} ({{conviction.weakest_component.score}}) |
Guidance: {{conviction.guidance}}
Recommended Actions:
{{#each conviction.actions}}
- {{this}}
{{/each}}
---
2. Pattern Classification
Detected Pattern: {{pattern.label}} (match: {{pattern.match_strength}}%)
{{pattern.description}}
| Pattern | Match Score |
|---|
{{#each pattern.all_pattern_scores}} | {{@key}} | {{this}} | {{/each}}
---
3. Component Scores (7 Components)
| # | Component | Weight | Score | Weighted |
|---|
{{#each conviction.component_scores}} | {{@index}} | {{this.label}} | {{this.weight}} | {{this.score}} | {{this.weighted_contribution}} | {{/each}} | | TOTAL | 100% | | {{conviction.conviction_score}} |
---
4. Input Skills Summary
Required Skills
| Skill | Score | Zone/State | Key Signal |
|---|
{{#each input_summary_required}} | {{this.name}} | {{this.score}} | {{this.zone}} | {{this.signal}} | {{/each}}
Optional Skills
| Skill | Score | Key Signal |
|---|
{{#each input_summary_optional}} | {{this.name}} | {{this.score}} | {{this.signal}} | {{/each}}
---
5. Target Allocation
| Asset Class | Allocation |
|---|---|
| Equity | {{allocation.target.equity}}% |
| Bonds | {{allocation.target.bonds}}% |
| Alternatives | {{allocation.target.alternatives}}% |
| Cash | {{allocation.target.cash}}% |
---
6. Position Sizing & Risk
| Parameter | Value |
|---|---|
| Max Single Position | {{position_sizing.max_single_position}}% |
| Daily Volatility Target | {{position_sizing.daily_vol_target}}% |
| Max Open Positions | {{position_sizing.max_positions}} |
---
7. Druckenmiller Principle
"{{druckenmiller_quote}}"
>
— Stanley Druckenmiller
Application: {{druckenmiller_application}}
---
Methodology
This report synthesizes outputs from 8 upstream analysis skills (5 required + 3 optional) into a single conviction score using Stanley Druckenmiller's investment philosophy.
7 Components (weighted 0-100):
1. Market Structure (18%): Breadth + Uptrend health 2. Distribution Risk (18%): Market Top risk (inverted) 3. Bottom Confirmation (12%): FTD Detector re-entry signal 4. Macro Alignment (18%): Macro Regime positioning 5. Theme Quality (12%): Theme Detector momentum 6. Setup Availability (10%): VCP + CANSLIM setups 7. Signal Convergence (12%): Cross-skill agreement
4 Patterns: Policy Pivot Anticipation, Unsustainable Distortion, Extreme Sentiment Contrarian, Wait & Observe
---
Disclaimer: This analysis is for educational and informational purposes only. Not investment advice. Past performance does not guarantee future results. Conduct your own research and consult a financial advisor before making investment decisions.
ドラッケンミラーの投資判断事例集
歴史的な成功事例
1. 1981年 ボルカー議長の金融引き締め
Pattern Classification: Policy Pivot Anticipation Conviction Level: Maximum Conviction (estimated 90+) Source: The New Market Wizards (Jack Schwager, 1992), Chapter on Druckenmiller
状況: インフレ率が二桁に達し、FRB議長ポール・ボルカーが強力な金融引き締めを実施
ドラッケンミラーの分析:
- 「この男(ボルカー)は絶対にインフレを許さない」と確信
- 市場コンセンサスはインフレ継続を予想
- 長期金利14%は将来のディスインフレを織り込んでいない
投資行動:
- ファンド資金の50%を年利14%の長期国債に集中投資
- 市場が予想するインフレ継続シナリオに逆張り
結果:
- インフレ率は急速に低下
- 長期国債価格は大幅上昇
- 巨額のリターンを獲得
教訓: 中央銀行の政策決意を正しく評価し、市場コンセンサスと逆の大胆な賭けをする
2. 1992年 ポンド危機(ブラック・ウェンズデー)
Pattern Classification: Unsustainable Distortion Conviction Level: Maximum Conviction (estimated 95+) Source: Soros on Soros (George Soros, 1995); Bloomberg profile (Sep 1992)
状況: 英国が欧州為替相場メカニズム(ERM)で無理なポンド高を維持
ドラッケンミラーの分析:
- 英国の経済ファンダメンタルズは弱い
- 高金利政策は持続不可能
- ドイツとの金利差が経済実態と乖離
投資行動:
- ソロスに「もっと大きく張るべき」と進言
- 100億ドル規模のポンド売りポジション構築
結果:
- 英国はERM離脱を余儀なくされる
- ポンドは急落
- 10億ドル以上の利益
教訓: 持続不可能な政策は必ず破綻する。確信があれば巨大なポジションも正当化される
3. 2007-2008年 金融危機
Pattern Classification: Unsustainable Distortion → Extreme Sentiment Contrarian (Phase 2: bottom re-entry) Conviction Level: High Conviction (defense) → Maximum Conviction (post-crash re-entry) Source: Duquesne Capital performance records; Bloomberg interviews (2008-2009)
状況: 2003-2004年のFRB低金利政策が住宅バブルを生成
ドラッケンミラーの分析:
- FRBの「長期間低金利」約束は明らかに行き過ぎ
- 住宅価格の上昇は持続不可能
- 金融システムに深刻な歪みが蓄積
投資行動:
- 2005年頃から住宅市場の破綻を予見し準備
- 債券ロング、金融株ショート
- 安全資産へのシフト
結果:
- 2008年もプラスリターンを達成
- 市場暴落の中で資産を守り増やす
教訓: 中央銀行の政策ミスは数年後に大きな危機をもたらす。早期に察知し準備することが重要
4. 2023年 慎重姿勢の維持
Pattern Classification: Wait & Observe Conviction Level: Low Conviction (estimated 25-35) Source: Bloomberg interview (Jun 2023); CNBC Delivering Alpha (Sep 2023)
状況: インフレ、金利上昇、地政学リスクなど不確実性が高い環境
ドラッケンミラーの分析:
- 「歴史上類を見ない複雑な環境」
- 明確な投資機会(fat pitch)が見当たらない
- リスク・リワードが不明確
投資行動:
- ポジションを極力抑制
- 日次ボラティリティを0.3-0.4%に管理
- 現金比率を高く維持
教訓: 見えない時は無理に振らない。機会を待つことも重要な戦略
投資判断のパターン分析
パターン1: 政策転換の先読み
特徴:
- 中央銀行の政策が転換点に近づく
- 市場はまだ従来の政策継続を織り込む
- 転換後の影響を他者より早く認識
アクション:
- 政策転換で最も恩恵/打撃を受ける資産を特定
- 市場が気づく前にポジション構築
- 転換が明確になったら追加投資
パターン2: 持続不可能な歪みの発見
特徴:
- 経済実態と乖離した価格・政策
- 当局や市場参加者の過信
- 破綻の触媒が見え始める
アクション:
- 歪みの解消で利益を得るポジション構築
- タイミングは柔軟に(早すぎることもある)
- 破綻が始まったら積極的に追撃
パターン3: 極端なセンチメントの逆張り
特徴:
- 市場が一方向に極端に傾く
- 恐怖や強欲が判断を歪める
- ファンダメンタルズが無視される
アクション:
- 感情的な売買の反対側に立つ
- 段階的にポジション構築
- センチメント転換の兆候を待つ
パターン4: 不確実性が高い時の様子見
特徴:
- 複数のシナリオが考えられる
- どの方向に動くか予測困難
- リスク・リワードが不明確
アクション:
- ポジションを最小化
- 流動性を確保
- 状況が明確になるまで待機
リアルタイムでの応用方法
ステップ1: 現状分析
1. 今、どの経済・政策サイクルにいるか? 2. 市場のコンセンサスは何か? 3. 持続不可能な歪みはないか?
ステップ2: 18か月先の予測
1. 現在の政策・トレンドは持続可能か? 2. 転換点となりうる触媒は何か? 3. 市場は何を織り込んでいないか?
ステップ3: 投資機会の評価
1. リスク・リワードは魅力的か? 2. 確信度はどの程度か? 3. タイミングは適切か?
ステップ4: ポジション決定
1. 確信度に応じたサイズ設定 2. 最大損失の想定と許容確認 3. エグジット条件の事前設定
ステップ5: 継続的モニタリング
1. 投資理由は有効か? 2. 新たな情報で判断を修正すべきか? 3. より良い機会が出現していないか?
Conviction Matrix - Signal-to-Action Mapping
Quantitative cross-reference tables for translating multi-skill signal combinations into specific exposure and action recommendations.
1. Market Top Zone x Macro Regime Matrix
Recommended equity exposure % based on the intersection of top risk and regime:
| Market Top Zone → | Green (0-20) | Yellow (21-40) | Orange (41-60) | Red (61-80) | Critical (81-100) |
|---|---|---|---|---|---|
| Broadening | 95-100% | 80-90% | 65-75% | 45-55% | 25-35% |
| Concentration | 85-95% | 70-80% | 55-65% | 35-45% | 15-25% |
| Transitional | 80-90% | 65-75% | 50-60% | 30-40% | 10-20% |
| Inflationary | 70-80% | 55-65% | 40-50% | 25-35% | 5-15% |
| Contraction | 60-70% | 45-55% | 30-40% | 15-25% | 0-10% |
Usage: Find the cell at the intersection of the current Market Top score zone and the Macro Regime. This gives the recommended equity allocation range.
2. Breadth Zone x VCP Availability Matrix
Recommended new position entry aggressiveness:
| Breadth Zone → | Healthy (60+) | Recovering (40-59) | Weak (20-39) | Critical (<20) |
|---|---|---|---|---|
| Textbook VCP (90+) | Full size, aggressive | 75% size | 50% size, pilot only | No new entries |
| Strong VCP (80-89) | Full size | 75% size | 25% size, pilot | No new entries |
| Good VCP (70-79) | 75% size | 50% size | Watchlist only | No new entries |
| Developing (60-69) | Watchlist | Watchlist | Pass | Pass |
3. FTD State x Market Top Risk Matrix
Recommended re-entry behavior after corrections:
| FTD State → | FTD Confirmed | Rally Attempt | No Signal | Rally Failed |
|---|---|---|---|---|
| Top < 30 (Low Risk) | Aggressive re-entry, 80%+ | Moderate entry, 60% | Normal operations | Monitor only |
| Top 30-50 (Moderate) | Measured re-entry, 60% | Small pilot, 30% | Stay cautious | Stay defensive |
| Top 50-70 (Elevated) | Selective entry, 40% | Watchlist only | Reduce exposure | Maximum defense |
| Top > 70 (High Risk) | Contrarian pilot, 25% | No new entries | Sell rallies | Full cash/hedge |
Key insight: FTD Confirmed + Top > 70 = Pattern 3 (Extreme Sentiment Contrarian). This is Druckenmiller's "most money made in bear markets" setup.
4. Macro Regime x Theme Quality Matrix
Recommended sector/theme tilt:
| Regime → | Broadening | Concentration | Transitional | Inflationary | Contraction |
|---|---|---|---|---|---|
| Hot themes (70+) | Ride themes, broad | Ride themes in mega-caps | Selective theme plays | Commodity themes only | Avoid, preserve cash |
| Moderate themes (40-69) | Diversified across themes | Index-weighted | Wait for clarity | Real assets focus | Defensive only |
| Cold themes (<40) | Value/cyclical rotate | Mega-cap safety | Cash heavy | Gold/commodities | Full defensive |
5. Signal Convergence Interpretation
| Convergence Score | Meaning | Action |
|---|---|---|
| 80-100 | All ducks in a row | Maximum conviction sizing |
| 60-79 | Most signals agree | Standard conviction sizing |
| 40-59 | Mixed signals | Reduced sizing, selective |
| 20-39 | Conflicting signals | Minimal positions, high cash |
| 0-19 | Complete disagreement | Sit out entirely |
6. Composite Conviction Decision Tree
Conviction >= 80 AND Convergence >= 70
→ Maximum exposure, concentrated positions
→ "Go for the jugular" - Druckenmiller
Conviction 60-79 AND Pattern = Policy Pivot
→ Overweight equity, lean into regime transition
→ "Focus on central banks and liquidity"
Conviction 40-59 AND Pattern = Unsustainable Distortion
→ Reduce to 40-50% equity, tighten all stops
→ "It's how much you lose when wrong"
Conviction < 40 AND FTD = Confirmed
→ Contrarian pilot positions (25-40% equity)
→ "Most money made in bear markets"
Conviction < 40 AND FTD != Confirmed
→ Capital preservation mode (10-30% equity)
→ "When you don't see it, don't swing"7. Position Sizing Quick Reference
| Conviction Zone | Max Single | Max Positions | Daily Vol Target |
|---|---|---|---|
| Maximum (80-100) | 25% | 8 | 0.40% |
| High (60-79) | 15% | 12 | 0.30% |
| Moderate (40-59) | 10% | 15 | 0.25% |
| Low (20-39) | 5% | 20 | 0.15% |
| Preservation (0-19) | 3% | 25 | 0.10% |
ドラッケンミラーの投資哲学と戦略
1. ポジション構築とリスク管理の原則
集中投資の哲学
- "ブタ(pig)になる": 確信度の高い機会には大胆に集中投資する (Source: The New Market Wizards, Jack Schwager, 1992)
- 分散投資の否定: 「分散投資はビジネススクールで教える最も誤った概念」 (Source: Lost Tree Club lecture, Jan 2015)
- 待つ戦略: 年に1-2回の絶好の機会を待ち、その時に大きく張る (Source: Bloomberg interview, Jun 2023)
- 格言: 「卵を一つのかごに入れ、そのかごを注意深く見張れ」
資金管理の鉄則
- 資本保全が最優先: 大きなドローダウンは絶対に避ける
- 損失の数学: 「50%の損失を取り戻すには100%の利益が必要」
- メリハリのある投資:
- 平時: 損失ゼロ~数%に抑える
- 確信時: +50~60%の大勝を狙う
- 確信度に応じたポジションサイズ: 見えない時はバットを振らない
損切りの徹底
- 即座の撤退: 投資理由が崩れたら即座にポジションを解消
- ソロスの教え: 「史上最高の損失の切り手」から学んだ損切りの重要性
- ヘッジの否定: 「ヘッジが必要なポジションなら、そもそも持つべきではない」
- 勝ちトレードの追加: 利益が乗っている時はより攻める
2. 相場観形成のプロセス
マクロ経済分析の視点
- 18か月先を見る: 「現在に投資するな、未来に投資せよ」 (Source: Duquesne Capital Annual Letters)
- 流動性重視: 「市場を動かすのは企業収益ではなく中央銀行と流動性」 (Source: Lost Tree Club lecture, Jan 2015)
- 期待の重要性: 「大事なのは人々がその企業についてこれから何を期待しているか」
中央銀行の政策分析
- 政策ミスの察知: 中央銀行の行き過ぎた政策を見抜く
- 転換点の予測: 金融政策の大きな転換点を先読み
- 歴史的事例:
- 1980年代初頭: ボルカー議長のインフレ抑制を確信し長期国債に集中投資
- 2003-2004年: FRBの低金利政策の行き過ぎを警戒
- 2007-2008年: 住宅バブル崩壊を予見し準備
グローバル・マルチアセット視点
- 5-6つの資産バスケット: 株式・債券・通貨・コモディティを俯瞰
- 最適な戦場選び: 最も有利な市場・資産を選択
- 待つも相場: 明確な機会がない時は現金比率を高める
- ボラティリティ管理: 日次変動を0.3-0.4%程度に抑える
3. ベアマーケット戦略
弱気相場での優位性
- 実績: 「利益の約80%はベアマーケットで稼いだ」
- 機会の源泉: 中央銀行の政策ミスによる市場の歪みを突く
具体的戦術
- 空売りの難しさ: 「株式の空売りは非常に難しい」と認識
- 債券ロング: 景気後退局面では長期国債で利益を狙う
- 安全資産シフト: 国債、金、安定通貨への乗り換え
- 歪みの活用: 過剰な悲観から生じる価格歪みを攻めの機会とする
ベアマーケットの鉄則
1. 早期察知: 危機の兆候をいち早く捉える 2. 防御態勢: 株式エクスポージャーを急減 3. 逆張りの勇気: 過剰反応による歪みに賭ける 4. 生存最優先: 大損を避け、次の機会まで生き残る
4. Quantitative Rules (定量ルール)
Position Sizing
- Maximum single position: 25% of portfolio (Maximum Conviction only)
- Normal conviction: 10-15% per position
- Pilot positions: 3-5% per position when testing a thesis
Volatility Management
- Daily portfolio volatility target: 0.3-0.4% (standard mode)
- Reduced mode (unclear signals): 0.1-0.2% daily vol
- Maximum concentration: 40% in any single asset class tilt vs benchmark
Capital Preservation Thresholds
- Maximum drawdown tolerance: -5% triggers defensive mode
- Annual target structure: 0% in bad years, +30-60% in good years
- "Two-strike rule": Two consecutive losing months → cut all risk to minimum
Entry/Exit Quantitative Criteria
- Minimum risk/reward ratio: 3:1 for standard entries, 5:1 for concentrated bets
- Stop-loss framework: Time-based (thesis timeframe) + Price-based (technical invalidation)
- Profit target: Let winners run; only exit on thesis change, NOT price targets
See references/conviction_matrix.md for signal-to-action mapping tables.5. 投資判断の心構え
オープンマインドと柔軟性
- 市場の声を聴く: 自分の予測に固執しない
- 即座の修正: 誤りに気づいたら即座に方向転換
- 謙虚さ: 市場に対する謙虚な姿勢を保つ
メンタル管理
- 自己認識: 「調子が良い時と悪い時を自覚せよ」
- 調子による調整:
- 不調時: バント程度にとどめる
- 好調時: ホームランを狙う
長期的成功の秘訣
- 大胆さと慎重さの両立: 攻めと守りのメリハリ
- 忍耐力: 機会を待つ力
- 決断力: チャンスと見たら最大限のリスクを取る勇気
ドラッケンミラー流市場分析ガイド
マクロ経済分析フレームワーク
1. 中央銀行政策の評価
分析ポイント
- 金利政策の方向性: 緩和・中立・引き締めのサイクル位置
- 流動性の変化: マネーサプライ、QE/QTの動向
- 政策の持続可能性: 行き過ぎた政策の兆候を探る
- 市場期待との乖離: 中銀の意図と市場の理解のギャップ
警戒すべき政策ミスのサイン
- 長期間の極端な低金利維持
- インフレ圧力を無視した緩和継続
- 急激すぎる政策転換
- 市場との対話の失敗
2. 18か月先の未来予測
予測の構成要素
1. 経済サイクルの位置
- 拡大初期・中期・後期・減速・後退のどの段階か
- 次の転換点はいつ頃か
2. 政策サイクルとの関係
- 金融政策は経済に対して先行的か後追いか
- 財政政策の影響度
3. 市場期待の織り込み度
- コンセンサス予想は楽観的か悲観的か
- サプライズの可能性はどこにあるか
3. グローバル資産配分の決定
資産クラス別評価基準
株式
- 流動性環境(緩和的か引き締め的か)
- 企業収益の方向性(改善か悪化か)
- バリュエーション(割高か割安か)
- センチメント(楽観か悲観か)
債券
- 金利の方向性予測
- イールドカーブの形状
- クレジットスプレッドの動向
- インフレ期待
通貨
- 金利差の動向
- 経常収支の状況
- 政治的安定性
- 資本フローの方向
コモディティ
- 需給バランス
- ドルの強弱
- インフレ/デフレ圧力
- 地政学リスク
From Data to Decision (シグナル→アクション変換)
The Strategy Synthesizer uses the following rules to convert multi-skill signals into actionable decisions. See references/conviction_matrix.md for the full cross-reference tables.
Green Light Conditions (Aggressive Posture)
- Breadth composite >= 60 AND Uptrend zone = Bull/Strong Bull
- Market Top score < 40 (low distribution risk)
- Macro Regime = broadening AND confidence = high
- FTD state = FTD_CONFIRMED (if applicable)
- Action: 80-100% equity, concentrated positions, standard stops
Yellow Light Conditions (Cautious Posture)
- Breadth composite 40-59 OR Uptrend zone = Neutral
- Market Top score 40-60 (moderate risk)
- Macro Regime = transitional
- Mixed signal convergence (convergence score 40-60)
- Action: 50-70% equity, reduced sizing, tighter stops
Red Light Conditions (Defensive Posture)
- Breadth composite < 40 OR Uptrend zone = Bear
- Market Top score >= 60 (elevated/high risk)
- Macro Regime = contraction
- FTD state = RALLY_FAILED
- Action: 0-30% equity, high cash, no new entries
Pattern-Specific Overrides
- Policy Pivot detected: Overweight bonds + equity even if signals are mixed (anticipate regime shift)
- Unsustainable Distortion detected: Override green light → reduce to yellow light minimum
- Extreme Contrarian detected: Override red light → allow pilot equity entries (FTD confirmation)
---
ポジション構築の実践
確信度の評価基準
高確信度(大きく張る)の条件
1. 複数の要因が同じ方向を示す("ダックが列を成す") 2. 市場のコンセンサスと大きく乖離 3. リスク・リワードが非常に有利 4. 明確な触媒(カタリスト)が存在
低確信度(様子見)のサイン
- 相反するシグナルが混在
- 不確実性が極めて高い
- リスク・リワードが不明確
- タイミングが読めない
エントリーとエグジットの判断
エントリー条件
1. テクニカル確認: トレンドの初期段階を確認 2. ファンダメンタルズ: 投資テーマが明確 3. センチメント: 過度な楽観・悲観の存在 4. リスク管理: 最大損失が許容範囲内
エグジット条件
- 投資理由の消失: 当初のシナリオが崩れた
- 目標達成: 想定した利益に到達
- より良い機会: 他により魅力的な投資機会が出現
- リスク環境の変化: 市場環境が大きく変化
危機対応プロトコル
ベアマーケット突入の兆候
1. 金融政策の転換点
- 緩和から引き締めへの明確な転換
- 流動性の急速な収縮
2. 信用市場のストレス
- クレジットスプレッドの急拡大
- 銀行間市場の機能不全
3. センチメントの極端な楽観
- 全員が強気
- リスクの過小評価
- レバレッジの過剰利用
危機時の行動指針
1. 即座の防御態勢
- リスク資産の削減
- レバレッジの解消
- 流動性の確保
2. 安全資産へのシフト
- 長期国債
- 金
- 安全通貨(円、スイスフラン等)
3. 逆張り機会の探索
- 過度な売りによる歪み
- 質への逃避による優良資産の割安化
- 政策対応による転換点
日常的なモニタリング項目
毎日チェックすべき指標
- 主要国の金利動向
- 株式市場の内部構造(上昇/下落銘柄数等)
- 通貨市場の動き
- VIX等のボラティリティ指標
- クレジット市場の動向
週次・月次でレビューすべき項目
- 経済指標の予想と実績の乖離
- 中央銀行高官の発言トーン
- 資金フローデータ
- ポジショニングデータ
- センチメント指標
四半期ごとの大局観チェック
- 投資テーマの妥当性確認
- 18か月先予測の修正
- ポートフォリオ全体のリスク評価
- 新たな投資機会の発掘
#!/usr/bin/env python3
"""
Strategy Synthesizer - Allocation Engine
Generates target asset allocation based on conviction score, pattern,
and macro regime. Provides position sizing guidance.
Asset classes: equity, bonds, alternatives (gold/commodities), cash
All allocations sum to 100%.
"""
# ---------------------------------------------------------------------------
# Base allocations by conviction zone (sum to 100%)
# ---------------------------------------------------------------------------
ZONE_BASE_ALLOCATIONS = {
"Maximum Conviction": {
"equity": 90,
"bonds": 0,
"alternatives": 5,
"cash": 5,
},
"High Conviction": {
"equity": 75,
"bonds": 5,
"alternatives": 5,
"cash": 15,
},
"Moderate Conviction": {
"equity": 55,
"bonds": 10,
"alternatives": 10,
"cash": 25,
},
"Low Conviction": {
"equity": 30,
"bonds": 15,
"alternatives": 15,
"cash": 40,
},
"Capital Preservation": {
"equity": 10,
"bonds": 20,
"alternatives": 20,
"cash": 50,
},
}
# ---------------------------------------------------------------------------
# Regime adjustments (additive shifts)
# ---------------------------------------------------------------------------
REGIME_ADJUSTMENTS = {
"broadening": {"equity": +5, "bonds": 0, "alternatives": 0, "cash": -5},
"concentration": {"equity": +3, "bonds": 0, "alternatives": 0, "cash": -3},
"transitional": {"equity": 0, "bonds": 0, "alternatives": 0, "cash": 0},
"inflationary": {"equity": -5, "bonds": -5, "alternatives": +10, "cash": 0},
"contraction": {"equity": -10, "bonds": +5, "alternatives": 0, "cash": +5},
}
# ---------------------------------------------------------------------------
# Pattern adjustments (additive shifts)
# ---------------------------------------------------------------------------
PATTERN_ADJUSTMENTS = {
"policy_pivot_anticipation": {"equity": +5, "bonds": +3, "alternatives": -3, "cash": -5},
"unsustainable_distortion": {"equity": -10, "bonds": 0, "alternatives": +5, "cash": +5},
"extreme_sentiment_contrarian": {"equity": +10, "bonds": -5, "alternatives": 0, "cash": -5},
"wait_and_observe": {"equity": -5, "bonds": 0, "alternatives": 0, "cash": +5},
}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def generate_allocation(
conviction_score: float,
zone: str,
pattern: str,
regime: str,
) -> dict:
"""
Generate target allocation based on conviction, pattern, and regime.
Returns dict with equity, bonds, alternatives, cash (sum = 100).
"""
# Start with zone base allocation
base = dict(ZONE_BASE_ALLOCATIONS.get(zone, ZONE_BASE_ALLOCATIONS["Moderate Conviction"]))
# Apply regime adjustment
regime_adj = REGIME_ADJUSTMENTS.get(regime, REGIME_ADJUSTMENTS["transitional"])
for asset, shift in regime_adj.items():
base[asset] += shift
# Apply pattern adjustment
pattern_adj = PATTERN_ADJUSTMENTS.get(pattern, PATTERN_ADJUSTMENTS["wait_and_observe"])
for asset, shift in pattern_adj.items():
base[asset] += shift
# Clamp all to non-negative
for asset in base:
base[asset] = max(0, base[asset])
# Re-normalize to 100%
total = sum(base.values())
if total > 0:
for asset in base:
base[asset] = round(base[asset] / total * 100, 1)
else:
base = {"equity": 0, "bonds": 0, "alternatives": 0, "cash": 100}
# Fix rounding to exactly 100
diff = 100 - sum(base.values())
if abs(diff) > 0:
# Add/subtract from largest allocation
largest = max(base, key=base.get)
base[largest] = round(base[largest] + diff, 1)
return base
def calculate_position_sizing(
conviction_score: float,
zone: str,
) -> dict:
"""
Calculate position sizing parameters based on conviction level.
Returns:
max_single_position: Max % of portfolio in one position
daily_vol_target: Target daily portfolio volatility %
max_positions: Max number of open positions
"""
sizing_map = {
"Maximum Conviction": {
"max_single_position": 25,
"daily_vol_target": 0.4,
"max_positions": 8,
},
"High Conviction": {
"max_single_position": 15,
"daily_vol_target": 0.3,
"max_positions": 12,
},
"Moderate Conviction": {
"max_single_position": 10,
"daily_vol_target": 0.25,
"max_positions": 15,
},
"Low Conviction": {
"max_single_position": 5,
"daily_vol_target": 0.15,
"max_positions": 20,
},
"Capital Preservation": {
"max_single_position": 3,
"daily_vol_target": 0.1,
"max_positions": 25,
},
}
return sizing_map.get(zone, sizing_map["Moderate Conviction"])
#!/usr/bin/env python3
"""
Strategy Synthesizer - Report Generator
Generates JSON and Markdown reports for the Druckenmiller strategy analysis.
The Markdown output structure follows the specification defined in:
assets/strategy_report_template.md
"""
import json
def generate_json_report(analysis: dict, output_file: str):
"""Save full analysis as JSON."""
with open(output_file, "w") as f:
json.dump(analysis, f, indent=2, default=str)
print(f"JSON report saved to: {output_file}")
def generate_markdown_report(analysis: dict, output_file: str):
"""Generate comprehensive Markdown report."""
lines = []
metadata = analysis.get("metadata", {})
conviction = analysis.get("conviction", {})
pattern = analysis.get("pattern", {})
allocation = analysis.get("allocation", {})
sizing = analysis.get("position_sizing", {})
input_summary = analysis.get("input_summary", {})
component_scores = conviction.get("component_scores", {})
score = conviction.get("conviction_score", 0)
zone = conviction.get("zone", "Unknown")
zone_color = conviction.get("zone_color", "")
# ================================================================
# Header
# ================================================================
lines.append("# Druckenmiller Strategy Synthesizer Report")
lines.append("")
lines.append(f"**Generated:** {metadata.get('generated_at', 'N/A')}")
lines.append(
f"**Input Skills:** {metadata.get('skills_loaded', 0)} loaded "
f"({metadata.get('required_count', 0)} required + "
f"{metadata.get('optional_count', 0)} optional)"
)
lines.append("")
# ================================================================
# Section 1: Conviction Dashboard
# ================================================================
lines.append("---")
lines.append("")
lines.append("## 1. Conviction Dashboard")
lines.append("")
zone_emoji = _zone_emoji(zone_color)
lines.append("| Metric | Value |")
lines.append("|--------|-------|")
lines.append(f"| **Conviction Score** | {zone_emoji} **{score}/100** |")
lines.append(f"| **Zone** | {zone} |")
lines.append(f"| **Recommended Exposure** | {conviction.get('exposure_range', 'N/A')} |")
strongest = conviction.get("strongest_component", {})
weakest = conviction.get("weakest_component", {})
lines.append(
f"| **Strongest Component** | {strongest.get('label', 'N/A')} ({strongest.get('score', 0)}) |"
)
lines.append(
f"| **Weakest Component** | {weakest.get('label', 'N/A')} ({weakest.get('score', 0)}) |"
)
lines.append("")
lines.append(f"> **Guidance:** {conviction.get('guidance', '')}")
lines.append("")
# Actions
actions = conviction.get("actions", [])
if actions:
lines.append("**Recommended Actions:**")
lines.append("")
for action in actions:
lines.append(f"- {action}")
lines.append("")
# ================================================================
# Section 2: Pattern Classification
# ================================================================
lines.append("---")
lines.append("")
lines.append("## 2. Pattern Classification")
lines.append("")
pattern_label = pattern.get("label", "Unknown")
match_strength = pattern.get("match_strength", 0)
lines.append(f"**Detected Pattern:** {pattern_label} (match: {match_strength}%)")
lines.append("")
lines.append(f"> {pattern.get('description', '')}")
lines.append("")
# All pattern scores
all_scores = pattern.get("all_pattern_scores", {})
if all_scores:
lines.append("| Pattern | Match Score |")
lines.append("|---------|-----------|")
for p_name, p_score in sorted(all_scores.items(), key=lambda x: -x[1]):
marker = " **DETECTED**" if p_name == pattern.get("pattern") else ""
lines.append(f"| {p_name.replace('_', ' ').title()} | {p_score}{marker} |")
lines.append("")
# ================================================================
# Section 3: Component Scores
# ================================================================
lines.append("---")
lines.append("")
lines.append("## 3. Component Scores (7 Components)")
lines.append("")
lines.append("| # | Component | Weight | Eff. Weight | Score | Weighted |")
lines.append("|---|-----------|--------|-------------|-------|----------|")
for i, (key, comp) in enumerate(component_scores.items(), 1):
comp_score = comp.get("score", 0)
weight = comp.get("weight", 0)
eff_weight = comp.get("effective_weight", weight)
available = comp.get("available", True)
weighted = comp.get("weighted_contribution", 0)
label = comp.get("label", key)
if not available:
label = f"{label} (N/A)"
bar = _score_bar(comp_score)
lines.append(
f"| {i} | {label} | {weight * 100:.0f}% | {eff_weight * 100:.1f}% | {bar} {comp_score} | {weighted} |"
)
lines.append(f"| | **TOTAL** | **100%** | **100%** | | **{score}** |")
lines.append("")
# ================================================================
# Section 4: Input Skills Summary
# ================================================================
lines.append("---")
lines.append("")
lines.append("## 4. Input Skills Summary")
lines.append("")
required_keys = [
"market_breadth",
"uptrend_analysis",
"market_top",
"macro_regime",
"ftd_detector",
]
optional_keys = ["vcp_screener", "theme_detector", "canslim_screener"]
lines.append("### Required Skills")
lines.append("")
lines.append("| Skill | Score | Zone/State | Key Signal |")
lines.append("|-------|-------|-----------|------------|")
for key in required_keys:
sig = input_summary.get(key, {})
sig_score = sig.get("composite_score", sig.get("quality_score", "N/A"))
zone_or_state = sig.get("zone", sig.get("state", "N/A"))
key_signal = _format_key_signal(key, sig)
lines.append(
f"| {key.replace('_', ' ').title()} | {sig_score} | {zone_or_state} | {key_signal} |"
)
lines.append("")
lines.append("### Optional Skills")
lines.append("")
lines.append("| Skill | Score | Key Signal |")
lines.append("|-------|-------|------------|")
for key in optional_keys:
sig = input_summary.get(key, {})
if sig:
sig_score = sig.get("derived_score", "N/A")
key_signal = _format_key_signal(key, sig)
lines.append(f"| {key.replace('_', ' ').title()} | {sig_score} | {key_signal} |")
else:
lines.append(f"| {key.replace('_', ' ').title()} | -- | Not available |")
lines.append("")
# ================================================================
# Section 5: Target Allocation
# ================================================================
lines.append("---")
lines.append("")
lines.append("## 5. Target Allocation")
lines.append("")
target = allocation.get("target", {})
lines.append("| Asset Class | Allocation |")
lines.append("|-------------|-----------|")
for asset in ["equity", "bonds", "alternatives", "cash"]:
pct = target.get(asset, 0)
bar = _alloc_bar(pct)
lines.append(f"| {asset.title()} | {bar} {pct}% |")
lines.append("")
# ================================================================
# Section 6: Position Sizing & Risk
# ================================================================
lines.append("---")
lines.append("")
lines.append("## 6. Position Sizing & Risk")
lines.append("")
lines.append("| Parameter | Value |")
lines.append("|-----------|-------|")
lines.append(f"| Max Single Position | {sizing.get('max_single_position', 'N/A')}% |")
lines.append(f"| Daily Volatility Target | {sizing.get('daily_vol_target', 'N/A')}% |")
lines.append(f"| Max Open Positions | {sizing.get('max_positions', 'N/A')} |")
lines.append("")
# ================================================================
# Section 7: Druckenmiller Principle
# ================================================================
lines.append("---")
lines.append("")
lines.append("## 7. Druckenmiller Principle")
lines.append("")
principle = _select_principle(zone, pattern.get("pattern", ""))
lines.append(f'> *"{principle["quote"]}"*')
lines.append(">")
lines.append("> — Stanley Druckenmiller")
lines.append("")
lines.append(f"**Application:** {principle['application']}")
lines.append("")
# ================================================================
# Methodology
# ================================================================
lines.append("---")
lines.append("")
lines.append("## Methodology")
lines.append("")
lines.append(
"This report synthesizes outputs from 8 upstream analysis skills "
"(5 required + 3 optional) into a single conviction score using "
"Stanley Druckenmiller's investment philosophy."
)
lines.append("")
lines.append("**7 Components** (weighted 0-100):")
lines.append("")
lines.append("1. Market Structure (18%): Breadth + Uptrend health")
lines.append("2. Distribution Risk (18%): Market Top risk (inverted)")
lines.append("3. Bottom Confirmation (12%): FTD Detector re-entry signal")
lines.append("4. Macro Alignment (18%): Macro Regime positioning")
lines.append("5. Theme Quality (12%): Theme Detector momentum")
lines.append("6. Setup Availability (10%): VCP + CANSLIM setups")
lines.append("7. Signal Convergence (12%): Cross-skill agreement")
lines.append("")
lines.append(
"**4 Patterns:** Policy Pivot Anticipation, Unsustainable Distortion, "
"Extreme Sentiment Contrarian, Wait & Observe"
)
lines.append("")
# Disclaimer
lines.append("---")
lines.append("")
lines.append(
"**Disclaimer:** This analysis is for educational and informational purposes only. "
"Not investment advice. Past performance does not guarantee future results. "
"Conduct your own research and consult a financial advisor before making "
"investment decisions."
)
lines.append("")
with open(output_file, "w") as f:
f.write("\n".join(lines))
print(f"Markdown report saved to: {output_file}")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _zone_emoji(color: str) -> str:
mapping = {
"green": "🟢",
"light_green": "🟢",
"yellow": "🟡",
"orange": "🟠",
"red": "🔴",
}
return mapping.get(color, "⚪")
def _score_bar(score: float) -> str:
if score >= 80:
return "████"
elif score >= 60:
return "███░"
elif score >= 40:
return "██░░"
elif score >= 20:
return "█░░░"
else:
return "░░░░"
def _alloc_bar(pct: float) -> str:
filled = int(pct / 10)
empty = 10 - filled
return "█" * filled + "░" * empty
def _format_key_signal(skill: str, sig: dict) -> str:
"""Format a one-line key signal per skill."""
if skill == "market_breadth":
return f"Zone: {sig.get('zone', 'N/A')}"
elif skill == "uptrend_analysis":
return f"Zone: {sig.get('zone', 'N/A')}"
elif skill == "market_top":
return f"Risk: {sig.get('zone', 'N/A')}"
elif skill == "macro_regime":
return f"Regime: {sig.get('regime', 'N/A')} ({sig.get('confidence', 'N/A')})"
elif skill == "ftd_detector":
return f"State: {sig.get('state', 'N/A')}, Quality: {sig.get('quality_score', 0)}"
elif skill == "vcp_screener":
return f"Textbook: {sig.get('textbook_count', 0)}, Strong: {sig.get('strong_count', 0)}"
elif skill == "theme_detector":
return f"Hot: {sig.get('hot_count', 0)}, Exhausting: {sig.get('exhaustion_count', 0)}"
elif skill == "canslim_screener":
return (
f"M Score: {sig.get('m_score', 'N/A')}, Exceptional: {sig.get('exceptional_count', 0)}"
)
return "N/A"
def _select_principle(zone: str, pattern: str) -> dict:
"""Select a relevant Druckenmiller quote for the current situation."""
if zone == "Maximum Conviction":
return {
"quote": "The way to build long-term returns is through preservation of "
"capital and home runs. When you have tremendous conviction on a "
"trade, you have to go for the jugular.",
"application": "All signals aligned. This is the moment to concentrate "
"positions and size up aggressively.",
}
elif pattern == "extreme_sentiment_contrarian":
return {
"quote": "I've made most of my money in bear markets. The biggest "
"opportunities come when everyone is running for the exits.",
"application": "FTD confirmed after extreme pessimism. Consider aggressive "
"re-entry as the bottom forms.",
}
elif pattern == "unsustainable_distortion":
return {
"quote": "It's not whether you're right or wrong that's important, "
"but how much money you make when you're right and how much "
"you lose when you're wrong.",
"application": "Distribution signals mounting. Reduce exposure, tighten "
"stops, and preserve capital for the next opportunity.",
}
elif zone == "Capital Preservation":
return {
"quote": "The first thing I heard when I got in the business was bulls "
"make money, bears make money, and pigs get slaughtered. "
"I'm here to tell you I was a pig.",
"application": "Conditions are hostile. Sit on the sidelines and wait. "
"The best trade is sometimes no trade.",
}
elif pattern == "policy_pivot_anticipation":
return {
"quote": "Earnings don't move the overall market; it's the Federal "
"Reserve Board... focus on the central banks, and focus on "
"the movement of liquidity.",
"application": "Macro regime is transitioning. Position ahead of the "
"policy pivot for asymmetric upside.",
}
else:
return {
"quote": "Don't invest in the present; invest in what you think the "
"world will look like in 18 months.",
"application": "Signals are mixed. Maintain discipline, stay patient, "
"and wait for a clearer setup.",
}
#!/usr/bin/env python3
"""
Strategy Synthesizer - Report Loader
Discovers, loads, and normalizes JSON output from 8 upstream skills.
5 required + 3 optional.
Required: market_breadth, uptrend_analysis, market_top, macro_regime, ftd_detector
Optional: vcp_screener, theme_detector, canslim_screener
"""
import glob
import json
import os
from datetime import datetime
from typing import Optional
# ---------------------------------------------------------------------------
# Skill definitions
# ---------------------------------------------------------------------------
REQUIRED_SKILLS = {
"market_breadth": {"prefix": "market_breadth_", "role": "Market participation breadth"},
"uptrend_analysis": {"prefix": "uptrend_analysis_", "role": "Sector uptrend ratios"},
"market_top": {"prefix": "market_top_", "role": "Distribution / top risk (defense)"},
"macro_regime": {"prefix": "macro_regime_", "role": "Macro regime transition (1-2Y structure)"},
"ftd_detector": {"prefix": "ftd_detector_", "role": "Bottom confirmation / re-entry (offense)"},
}
OPTIONAL_SKILLS = {
"vcp_screener": {"prefix": "vcp_screener_", "role": "Momentum stock setups (VCP)"},
"theme_detector": {"prefix": "theme_detector_", "role": "Theme / sector momentum"},
"canslim_screener": {
"prefix": "canslim_screener_",
"role": "Growth stock setups + M(Market Direction)",
},
}
ALL_SKILLS = {**REQUIRED_SKILLS, **OPTIONAL_SKILLS}
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def find_latest_report(
reports_dir: str,
prefix: str,
max_age_hours: float = 0,
) -> Optional[tuple[str, dict]]:
"""
Find the most recent JSON report matching *prefix* in *reports_dir*.
Returns (file_path, parsed_data) or None if no qualifying file found.
Files are matched by glob and sorted by filename (timestamp order).
If max_age_hours > 0, files older than that threshold are rejected.
"""
pattern = os.path.join(reports_dir, f"{prefix}*.json")
matches = sorted(glob.glob(pattern))
if not matches:
return None
# Most recent = last in sorted list (filenames contain timestamps)
for path in reversed(matches):
if max_age_hours > 0:
mtime = os.path.getmtime(path)
file_age_hours = (datetime.now().timestamp() - mtime) / 3600
if file_age_hours > max_age_hours:
continue
try:
with open(path) as f:
data = json.load(f)
return (path, data)
except (json.JSONDecodeError, OSError):
continue
return None
def load_all_reports(
reports_dir: str,
max_age_hours: float = 72,
) -> dict[str, dict]:
"""
Load all upstream skill JSON reports from *reports_dir*.
Returns dict mapping skill_name -> parsed JSON data.
Raises ValueError if any required skill report is missing or stale.
"""
reports = {}
missing_required = []
for skill_name, info in ALL_SKILLS.items():
result = find_latest_report(reports_dir, info["prefix"], max_age_hours)
if result is not None:
_path, data = result
reports[skill_name] = data
elif skill_name in REQUIRED_SKILLS:
missing_required.append(skill_name)
if missing_required:
raise ValueError(
f"Missing required skill reports: {', '.join(missing_required)}. "
f"Run these skills first or increase --max-age. "
f"Searched in: {reports_dir}"
)
return reports
def extract_signal(skill_name: str, report_data: dict) -> dict:
"""
Extract a normalized signal dict from a skill's JSON output.
Each skill has its own extraction logic. For skills without a native
composite_score (VCP, Theme, CANSLIM), a derived score is calculated.
"""
extractors = {
"market_breadth": _extract_breadth,
"uptrend_analysis": _extract_uptrend,
"market_top": _extract_market_top,
"macro_regime": _extract_macro_regime,
"ftd_detector": _extract_ftd,
"vcp_screener": _extract_vcp,
"theme_detector": _extract_theme,
"canslim_screener": _extract_canslim,
}
extractor = extractors.get(skill_name)
if extractor is None:
raise ValueError(f"Unknown skill: {skill_name}")
return extractor(report_data)
# ---------------------------------------------------------------------------
# Per-skill extractors
# ---------------------------------------------------------------------------
def _extract_breadth(data: dict) -> dict:
composite = data.get("composite", {})
return {
"source": "market_breadth",
"composite_score": composite.get("composite_score", 0),
"zone": composite.get("zone", "Unknown"),
"zone_color": composite.get("zone_color", ""),
"exposure_guidance": composite.get("exposure_guidance", ""),
}
def _extract_uptrend(data: dict) -> dict:
composite = data.get("composite", {})
warnings = composite.get("active_warnings", [])
return {
"source": "uptrend_analysis",
"composite_score": composite.get("composite_score", 0),
"zone": composite.get("zone", "Unknown"),
"zone_color": composite.get("zone_color", ""),
"exposure_guidance": composite.get("exposure_guidance", ""),
"warning_flags": [w.get("flag", "") for w in warnings],
}
def _extract_market_top(data: dict) -> dict:
composite = data.get("composite", {})
return {
"source": "market_top",
"composite_score": composite.get("composite_score", 0),
"zone": composite.get("zone", "Unknown"),
"zone_color": composite.get("zone_color", ""),
"risk_budget": composite.get("risk_budget", ""),
"strongest_warning": composite.get("strongest_warning", {}),
}
def _extract_macro_regime(data: dict) -> dict:
composite = data.get("composite", {})
regime = data.get("regime", {})
transition = regime.get("transition_probability", {})
return {
"source": "macro_regime",
"composite_score": composite.get("composite_score", 0),
"zone": composite.get("zone", "Unknown"),
"regime": regime.get("current_regime", "unknown"),
"regime_label": regime.get("regime_label", "Unknown"),
"confidence": regime.get("confidence", "unknown"),
"transition_level": transition.get("level", "unknown"),
"transition_range": transition.get("probability_range", ""),
}
def _extract_ftd(data: dict) -> dict:
market_state = data.get("market_state", {})
quality = data.get("quality_score", {})
post_ftd = data.get("post_ftd_distribution", {})
return {
"source": "ftd_detector",
"state": market_state.get("combined_state", "NO_SIGNAL"),
"dual_confirmation": market_state.get("dual_confirmation", False),
"ftd_index": market_state.get("ftd_index"),
"quality_score": quality.get("total_score", 0),
"signal": quality.get("signal", "No FTD"),
"exposure_range": quality.get("exposure_range", "0-25%"),
"post_ftd_distribution_count": post_ftd.get("distribution_count", 0),
}
def _extract_vcp(data: dict) -> dict:
"""
VCP screener lacks a composite_score. Derive one from:
- Rating distribution: textbook*25 + strong*15 + good*10 + developing*3
- Funnel health: trend_template_passed / universe ratio
"""
results = data.get("results", [])
funnel = data.get("metadata", {}).get("funnel", {})
# Count by rating
counts = {"textbook": 0, "strong": 0, "good": 0, "developing": 0}
for r in results:
rating = (r.get("rating") or "").lower()
if "textbook" in rating:
counts["textbook"] += 1
elif "strong" in rating:
counts["strong"] += 1
elif "good" in rating:
counts["good"] += 1
elif "developing" in rating:
counts["developing"] += 1
# Weighted quality score (raw points, capped at 100)
quality_raw = (
counts["textbook"] * 25
+ counts["strong"] * 15
+ counts["good"] * 10
+ counts["developing"] * 3
)
quality_score = min(quality_raw, 100)
# Funnel health: what % of universe passed trend template
universe = funnel.get("universe", 1)
tt_passed = funnel.get("trend_template_passed", 0)
funnel_ratio = tt_passed / max(universe, 1)
# Map funnel_ratio to 0-100: 20% pass rate = 100
funnel_score = min(funnel_ratio / 0.20 * 100, 100)
derived = round(quality_score * 0.6 + funnel_score * 0.4, 1)
return {
"source": "vcp_screener",
"derived_score": min(derived, 100),
"quality_score": quality_score,
"funnel_score": round(funnel_score, 1),
"textbook_count": counts["textbook"],
"strong_count": counts["strong"],
"good_count": counts["good"],
"developing_count": counts["developing"],
"total_candidates": len(results),
}
def _extract_theme(data: dict) -> dict:
"""
Theme detector lacks a composite_score. Derive one from:
- Hot theme count (heat >= 70)
- Early stage bonus
- Exhaustion penalty
"""
themes = data.get("themes", {}).get("all", [])
summary = data.get("summary", {})
bullish_count = summary.get("bullish_count", 0)
hot_count = 0
early_count = 0
exhaustion_count = 0
total_heat = 0
for t in themes:
heat = t.get("heat", 0)
stage = (t.get("stage") or "").lower()
direction = (t.get("direction") or "").lower()
if direction == "bullish" and heat >= 70:
hot_count += 1
if stage == "early":
early_count += 1
if stage in ("exhausting", "exhaustion"):
exhaustion_count += 1
if direction == "bullish":
total_heat += heat
# Base score from average heat of bullish themes
avg_heat = total_heat / max(bullish_count, 1)
base_score = avg_heat * 0.7 # Scale to ~70 max from heat alone
# Bonuses and penalties
early_bonus = early_count * 5 # Up to ~25
hot_bonus = hot_count * 3 # Up to ~15
exhaustion_penalty = exhaustion_count * 8 # Up to ~40
derived = round(base_score + early_bonus + hot_bonus - exhaustion_penalty, 1)
derived = max(0, min(derived, 100))
return {
"source": "theme_detector",
"derived_score": derived,
"hot_count": hot_count,
"early_count": early_count,
"exhaustion_count": exhaustion_count,
"bullish_count": bullish_count,
"avg_heat": round(avg_heat, 1),
}
def _extract_canslim(data: dict) -> dict:
"""
CANSLIM screener has composite_score per stock.
Derive overall score from top candidates + M component.
"""
results = data.get("results", [])
market = data.get("metadata", {}).get("market_condition", {})
m_score = market.get("M_score", 50)
m_trend = market.get("trend", "unknown")
# Average composite of top 5 stocks (or fewer)
top_scores = sorted(
[r.get("composite_score", 0) for r in results],
reverse=True,
)[:5]
avg_top = sum(top_scores) / max(len(top_scores), 1) if top_scores else 0
# Count by quality tier
exceptional_count = sum(1 for r in results if r.get("composite_score", 0) >= 90)
strong_count = sum(1 for r in results if 80 <= r.get("composite_score", 0) < 90)
# Derived score: average top quality + quality tier bonus
tier_bonus = exceptional_count * 3 + strong_count * 1.5
derived = round(min(avg_top * 0.7 + tier_bonus + m_score * 0.15, 100), 1)
return {
"source": "canslim_screener",
"derived_score": derived,
"m_score": m_score,
"m_trend": m_trend,
"top_avg_score": round(avg_top, 1),
"exceptional_count": exceptional_count,
"strong_count": strong_count,
"total_candidates": len(results),
}
#!/usr/bin/env python3
"""
Strategy Synthesizer - Conviction Scoring Engine
Combines 7 component scores into a weighted composite conviction score (0-100).
Maps to conviction zones and classifies into 4 Druckenmiller patterns.
Component Weights:
1. Market Structure: 18% (Breadth + Uptrend)
2. Distribution Risk: 18% (Market Top - inverted)
3. Bottom Confirmation: 12% (FTD Detector)
4. Macro Alignment: 18% (Macro Regime)
5. Theme Quality: 12% (Theme Detector)
6. Setup Availability: 10% (VCP + CANSLIM)
7. Signal Convergence: 12% (Cross-skill agreement)
Total: 100%
Conviction Zones:
80-100: Maximum Conviction - Exposure: 90-100%
60-79: High Conviction - Exposure: 70-90%
40-59: Moderate Conviction - Exposure: 50-70%
20-39: Low Conviction - Exposure: 20-50%
0-19: Capital Preservation - Exposure: 0-20%
"""
from typing import Optional
COMPONENT_WEIGHTS = {
"market_structure": 0.18,
"distribution_risk": 0.18,
"bottom_confirmation": 0.12,
"macro_alignment": 0.18,
"theme_quality": 0.12,
"setup_availability": 0.10,
"signal_convergence": 0.12,
}
COMPONENT_LABELS = {
"market_structure": "Market Structure (Breadth + Uptrend)",
"distribution_risk": "Distribution Risk (Market Top, inverted)",
"bottom_confirmation": "Bottom Confirmation (FTD Detector)",
"macro_alignment": "Macro Alignment (Regime)",
"theme_quality": "Theme Quality (Theme Detector)",
"setup_availability": "Setup Availability (VCP + CANSLIM)",
"signal_convergence": "Signal Convergence (Cross-Skill Agreement)",
}
# ---------------------------------------------------------------------------
# Component calculators
# ---------------------------------------------------------------------------
def calculate_market_structure(signals: dict) -> float:
"""Breadth health * 0.5 + Uptrend health * 0.5, with divergence adjustment."""
breadth = signals.get("market_breadth", {})
uptrend = signals.get("uptrend_analysis", {})
b_score = breadth.get("composite_score", 50)
u_score = uptrend.get("composite_score", 50)
base = b_score * 0.5 + u_score * 0.5
# Divergence penalty: if breadth and uptrend disagree significantly
divergence = abs(b_score - u_score)
if divergence > 30:
base -= (divergence - 30) * 0.3
return max(0, min(round(base, 1), 100))
def calculate_distribution_risk(signals: dict) -> float:
"""Inverted market top score: high top risk = low conviction."""
top = signals.get("market_top", {})
top_score = top.get("composite_score", 0)
return round(max(0, min(100 - top_score, 100)), 1)
def calculate_bottom_confirmation(signals: dict) -> float:
"""FTD state-based scoring for bottom confirmation / re-entry signal."""
ftd = signals.get("ftd_detector", {})
state = ftd.get("state", "NO_SIGNAL")
quality = ftd.get("quality_score", 0)
dual = ftd.get("dual_confirmation", False)
dist_count = ftd.get("post_ftd_distribution_count", 0)
state_scores = {
"FTD_CONFIRMED": 80,
"FTD_WINDOW": 60,
"RALLY_ATTEMPT": 55,
"NO_SIGNAL": 40,
"CORRECTION": 35,
"FTD_INVALIDATED": 15,
"RALLY_FAILED": 10,
}
base = state_scores.get(state, 40)
# Quality bonus for confirmed FTD
if state == "FTD_CONFIRMED":
quality_bonus = (quality - 50) * 0.2 # -10 to +10
base += quality_bonus
if dual:
base += 5
# Post-FTD distribution penalty
base -= dist_count * 5
return round(max(0, min(base, 100)), 1)
def calculate_macro_alignment(signals: dict) -> float:
"""Regime-based conviction scoring."""
macro = signals.get("macro_regime", {})
regime = macro.get("regime", "unknown")
composite = macro.get("composite_score", 50)
confidence = macro.get("confidence", "unknown")
regime_base = {
"broadening": 85,
"concentration": 65,
"transitional": 50,
"inflationary": 35,
"contraction": 20,
}
base = regime_base.get(regime, 50)
# Adjust by transition score
transition_adj = (composite - 50) * 0.2
base += transition_adj
# Confidence modifier
confidence_mult = {"high": 1.1, "medium": 1.0, "low": 0.9}
base *= confidence_mult.get(confidence, 1.0)
return round(max(0, min(base, 100)), 1)
def calculate_theme_quality(signals: dict) -> float:
"""Theme-derived score with lifecycle adjustment."""
theme = signals.get("theme_detector", {})
derived = theme.get("derived_score", 50)
return round(max(0, min(derived, 100)), 1)
def calculate_setup_availability(signals: dict) -> float:
"""
VCP + CANSLIM derived scores averaged when both available.
Fat pitch detection: exceptional setups = bonus.
"""
vcp = signals.get("vcp_screener", {})
canslim = signals.get("canslim_screener", {})
scores = []
fat_pitch_bonus = 0
if vcp:
scores.append(vcp.get("derived_score", 0))
if vcp.get("textbook_count", 0) >= 2:
fat_pitch_bonus += 10
if canslim:
scores.append(canslim.get("derived_score", 0))
if canslim.get("exceptional_count", 0) >= 3:
fat_pitch_bonus += 10
if not scores:
return 50.0 # Neutral default when no setup data
base = sum(scores) / len(scores) + fat_pitch_bonus
return round(max(0, min(base, 100)), 1)
def calculate_signal_convergence(signals: dict) -> float:
"""
Measure agreement among the 5 required skills.
"Ducks in a row" - Druckenmiller's key conviction criterion.
"""
required_scores = []
# Collect normalized bullish scores (higher = more bullish)
breadth = signals.get("market_breadth", {})
if breadth:
required_scores.append(breadth.get("composite_score", 50))
uptrend = signals.get("uptrend_analysis", {})
if uptrend:
required_scores.append(uptrend.get("composite_score", 50))
top = signals.get("market_top", {})
if top:
# Invert: low top risk = bullish
required_scores.append(100 - top.get("composite_score", 50))
macro = signals.get("macro_regime", {})
if macro:
required_scores.append(macro.get("composite_score", 50))
ftd = signals.get("ftd_detector", {})
if ftd:
required_scores.append(ftd.get("quality_score", 50))
if len(required_scores) < 3:
return 50.0 # Insufficient data
# Calculate agreement: low standard deviation = high convergence
mean = sum(required_scores) / len(required_scores)
variance = sum((s - mean) ** 2 for s in required_scores) / len(required_scores)
std_dev = variance**0.5
# Convert std_dev to convergence score
# std_dev 0 -> 100 (perfect agreement)
# std_dev 30+ -> 0 (complete disagreement)
convergence = max(0, 100 - std_dev * 3.33)
# Directional adjustment based on consensus
all_bullish = all(s > 55 for s in required_scores)
all_bearish = all(s < 45 for s in required_scores)
if all_bullish:
convergence = min(convergence + 15, 100)
elif all_bearish:
convergence = max(convergence - 15, 0)
return round(convergence, 1)
# ---------------------------------------------------------------------------
# Composite scorer
# ---------------------------------------------------------------------------
def calculate_composite_conviction(
signals: dict,
data_availability: Optional[dict[str, bool]] = None,
) -> dict:
"""
Calculate weighted composite conviction score from extracted signals.
Args:
signals: Dict mapping skill_name -> extracted signal dict
data_availability: Optional dict mapping component -> bool
Returns:
Dict with conviction_score, zone, exposure, guidance, etc.
"""
if data_availability is None:
data_availability = {}
# Determine which optional components have real data
has_theme = "theme_detector" in signals
has_setup = "vcp_screener" in signals or "canslim_screener" in signals
# Calculate each component
raw_scores = {
"market_structure": calculate_market_structure(signals),
"distribution_risk": calculate_distribution_risk(signals),
"bottom_confirmation": calculate_bottom_confirmation(signals),
"macro_alignment": calculate_macro_alignment(signals),
"theme_quality": calculate_theme_quality(signals),
"setup_availability": calculate_setup_availability(signals),
"signal_convergence": calculate_signal_convergence(signals),
}
# Determine availability per component
availability = {k: True for k in COMPONENT_WEIGHTS}
if not has_theme:
availability["theme_quality"] = False
if not has_setup:
availability["setup_availability"] = False
# Weight redistribution: proportionally allocate unavailable weight
available_components = {k for k, v in availability.items() if v}
base_weight_sum = sum(COMPONENT_WEIGHTS[k] for k in available_components)
effective_weights = {}
for k in COMPONENT_WEIGHTS:
if k in available_components and base_weight_sum > 0:
effective_weights[k] = COMPONENT_WEIGHTS[k] / base_weight_sum
else:
effective_weights[k] = 0.0
# Weighted composite using effective weights
composite = 0.0
for key in COMPONENT_WEIGHTS:
composite += raw_scores[key] * effective_weights[key]
composite = round(composite, 1)
# Zone interpretation
zone_info = _interpret_conviction_zone(composite)
# Strongest / weakest from available components only
available_scores = {k: v for k, v in raw_scores.items() if availability[k]}
strongest = max(available_scores, key=available_scores.get)
weakest = min(available_scores, key=available_scores.get)
# Data quality
available_count = sum(1 for v in availability.values() if v)
return {
"conviction_score": composite,
"zone": zone_info["zone"],
"zone_color": zone_info["color"],
"exposure_range": zone_info["exposure"],
"guidance": zone_info["guidance"],
"actions": zone_info["actions"],
"strongest_component": {
"component": strongest,
"label": COMPONENT_LABELS.get(strongest, strongest),
"score": raw_scores[strongest],
},
"weakest_component": {
"component": weakest,
"label": COMPONENT_LABELS.get(weakest, weakest),
"score": raw_scores[weakest],
},
"data_quality": {
"available_count": available_count,
"total_components": len(COMPONENT_WEIGHTS),
},
"component_scores": {
k: {
"score": raw_scores.get(k, 0),
"weight": w,
"effective_weight": round(effective_weights[k], 4),
"available": availability[k],
"weighted_contribution": round(raw_scores.get(k, 0) * effective_weights[k], 1),
"label": COMPONENT_LABELS[k],
}
for k, w in COMPONENT_WEIGHTS.items()
},
}
def _interpret_conviction_zone(composite: float) -> dict:
"""Map composite conviction to zone."""
if composite >= 80:
return {
"zone": "Maximum Conviction",
"color": "green",
"exposure": "90-100%",
"guidance": "All signals aligned. Druckenmiller 'fat pitch' - swing hard.",
"actions": [
"Maximum equity exposure (90-100%)",
"Concentrated positions in strongest setups",
"Aggressive position sizing on breakouts",
"Use leverage selectively on highest-conviction ideas",
],
}
elif composite >= 60:
return {
"zone": "High Conviction",
"color": "light_green",
"exposure": "70-90%",
"guidance": "Multiple signals confirm. Strong equity posture, standard risk management.",
"actions": [
"Above-average equity allocation (70-90%)",
"Standard position sizing on quality setups",
"New entries on VCP/CANSLIM signals",
"Maintain stop-losses at normal levels",
],
}
elif composite >= 40:
return {
"zone": "Moderate Conviction",
"color": "yellow",
"exposure": "50-70%",
"guidance": "Mixed signals. Maintain exposure but reduce position sizes.",
"actions": [
"Moderate equity allocation (50-70%)",
"Reduced position sizes (half normal)",
"Only A-grade setups",
"Tighter stop-losses",
"Raise cash allocation to 30-50%",
],
}
elif composite >= 20:
return {
"zone": "Low Conviction",
"color": "orange",
"exposure": "20-50%",
"guidance": "Unclear outlook. Preserve capital, minimal new risk.",
"actions": [
"Defensive posture (20-50% equity)",
"No new entries except rare opportunities",
"Profit-taking on weaker positions",
"High cash allocation (50-80%)",
"Monitor for signal improvement",
],
}
else:
return {
"zone": "Capital Preservation",
"color": "red",
"exposure": "0-20%",
"guidance": "Extreme caution. Druckenmiller: 'When you don't see it, don't swing.'",
"actions": [
"Maximum defensive posture (0-20% equity)",
"Close most positions",
"High cash / Treasuries / Gold",
"Watch for FTD signal for re-entry",
"Preserve capital as primary objective",
],
}
# ---------------------------------------------------------------------------
# Pattern classification
# ---------------------------------------------------------------------------
PATTERN_DEFINITIONS = {
"policy_pivot_anticipation": {
"label": "Policy Pivot Anticipation",
"description": "Central bank policy at inflection point. Market hasn't priced the turn.",
},
"unsustainable_distortion": {
"label": "Unsustainable Distortion",
"description": "Market structure is fragile. Distribution signals + macro contraction.",
},
"extreme_sentiment_contrarian": {
"label": "Extreme Sentiment Contrarian",
"description": "Bottom confirmed after extreme bearishness. Druckenmiller's bear market profit.",
},
"wait_and_observe": {
"label": "Wait & Observe",
"description": "Mixed signals, unclear direction. Preserve capital, wait for clarity.",
},
}
def classify_pattern(signals: dict, component_scores: dict, conviction_score: float) -> dict:
"""
Classify the current market into one of 4 Druckenmiller patterns.
Returns the best-matching pattern with match strength.
"""
macro = signals.get("macro_regime", {})
top = signals.get("market_top", {})
ftd = signals.get("ftd_detector", {})
breadth = signals.get("market_breadth", {})
regime = macro.get("regime", "unknown")
transition_level = macro.get("transition_level", "unknown")
top_score = top.get("composite_score", 0)
ftd_state = ftd.get("state", "NO_SIGNAL")
ftd_quality = ftd.get("quality_score", 0)
breadth_score = breadth.get("composite_score", 50)
pattern_scores = {}
# Pattern 1: Policy Pivot Anticipation
p1 = 0
if regime == "transitional":
p1 += 40
if transition_level in ("high", "moderate"):
p1 += 25
if top_score < 40: # No imminent top
p1 += 15
macro_composite = macro.get("composite_score", 0)
if macro_composite >= 50:
p1 += 20
pattern_scores["policy_pivot_anticipation"] = min(p1, 100)
# Pattern 2: Unsustainable Distortion
p2 = 0
if top_score >= 60:
p2 += 35
if regime in ("contraction", "inflationary"):
p2 += 30
theme = signals.get("theme_detector", {})
if theme.get("exhaustion_count", 0) >= 2:
p2 += 20
if breadth_score < 40:
p2 += 15
pattern_scores["unsustainable_distortion"] = min(p2, 100)
# Pattern 3: Extreme Sentiment Contrarian (FTD-driven)
p3 = 0
if ftd_state == "FTD_CONFIRMED":
p3 += 40
if ftd_quality >= 70:
p3 += 15
if top_score >= 70:
p3 += 20 # Came from high top risk
if breadth_score < 35:
p3 += 15 # Extreme pessimism in breadth
convergence = component_scores.get("signal_convergence", {})
conv_score = convergence.get("score", 50) if isinstance(convergence, dict) else convergence
if conv_score < 30: # Bearish convergence that's now reversing
p3 += 10
pattern_scores["extreme_sentiment_contrarian"] = min(p3, 100)
# Pattern 4: Wait & Observe (default when nothing strong)
p4 = 0
if conviction_score < 40:
p4 += 40
# Mixed signals indicator
scores_list = [
v.get("score", 50) if isinstance(v, dict) else 50 for v in component_scores.values()
]
if scores_list:
spread = max(scores_list) - min(scores_list)
if spread > 40:
p4 += 30
if regime == "transitional" and top_score >= 40 and top_score <= 60:
p4 += 20
pattern_scores["wait_and_observe"] = min(p4, 100)
# Select best pattern
best_pattern = max(pattern_scores, key=pattern_scores.get)
best_score = pattern_scores[best_pattern]
# If no strong match, default to wait_and_observe
if best_score < 40:
best_pattern = "wait_and_observe"
best_score = max(best_score, 50)
definition = PATTERN_DEFINITIONS[best_pattern]
return {
"pattern": best_pattern,
"label": definition["label"],
"description": definition["description"],
"match_strength": best_score,
"all_pattern_scores": pattern_scores,
}
#!/usr/bin/env python3
"""
Druckenmiller Strategy Synthesizer - Main Orchestrator
Integrates outputs from 8 upstream analysis skills (5 required + 3 optional)
into a unified conviction score, pattern classification, and allocation
recommendation based on Stanley Druckenmiller's investment philosophy.
Usage:
python3 strategy_synthesizer.py --reports-dir reports/
python3 strategy_synthesizer.py --reports-dir reports/ --output-dir reports/ --max-age 72
Output:
- JSON: druckenmiller_strategy_YYYY-MM-DD_HHMMSS.json
- Markdown: druckenmiller_strategy_YYYY-MM-DD_HHMMSS.md
"""
import argparse
import os
import sys
from datetime import datetime
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(__file__))
from allocation_engine import calculate_position_sizing, generate_allocation
from report_generator import generate_json_report, generate_markdown_report
from report_loader import OPTIONAL_SKILLS, REQUIRED_SKILLS, extract_signal, load_all_reports
from scorer import calculate_composite_conviction, classify_pattern
def parse_arguments():
parser = argparse.ArgumentParser(
description="Druckenmiller Strategy Synthesizer - Meta-Skill Orchestrator"
)
parser.add_argument(
"--reports-dir",
default="reports/",
help="Directory containing upstream skill JSON reports (default: reports/)",
)
parser.add_argument(
"--output-dir", default="reports/", help="Directory for output reports (default: reports/)"
)
parser.add_argument(
"--max-age",
type=float,
default=72,
help="Maximum age (hours) for input reports (default: 72)",
)
return parser.parse_args()
def main():
args = parse_arguments()
print("=" * 70)
print("Druckenmiller Strategy Synthesizer")
print("8-Skill Integration | Conviction Scoring | Pattern Classification")
print("=" * 70)
print()
# Ensure output directory exists
os.makedirs(args.output_dir, exist_ok=True)
# ========================================================================
# Step 1: Load Input Reports
# ========================================================================
print("Step 1: Loading Input Reports")
print("-" * 70)
try:
reports = load_all_reports(args.reports_dir, max_age_hours=args.max_age)
except ValueError as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
required_count = sum(1 for k in reports if k in REQUIRED_SKILLS)
optional_count = sum(1 for k in reports if k in OPTIONAL_SKILLS)
print(
f" Loaded {len(reports)} reports ({required_count} required + {optional_count} optional)"
)
for skill_name in reports:
marker = "REQ" if skill_name in REQUIRED_SKILLS else "OPT"
print(f" [{marker}] {skill_name}")
print()
# ========================================================================
# Step 2: Extract Signals
# ========================================================================
print("Step 2: Extracting Normalized Signals")
print("-" * 70)
signals = {}
for skill_name, report_data in reports.items():
sig = extract_signal(skill_name, report_data)
signals[skill_name] = sig
# Print key metric per skill
if "composite_score" in sig:
print(f" {skill_name}: score={sig['composite_score']}")
elif "derived_score" in sig:
print(f" {skill_name}: derived={sig['derived_score']}")
elif "quality_score" in sig:
print(
f" {skill_name}: quality={sig['quality_score']}, state={sig.get('state', 'N/A')}"
)
print()
# ========================================================================
# Step 3: Calculate Composite Conviction
# ========================================================================
print("Step 3: Calculating Composite Conviction")
print("-" * 70)
conviction = calculate_composite_conviction(signals)
score = conviction["conviction_score"]
zone = conviction["zone"]
print(f" Conviction Score: {score}/100")
print(f" Zone: {zone}")
print(f" Exposure Range: {conviction['exposure_range']}")
print(
f" Strongest: {conviction['strongest_component']['label']} "
f"({conviction['strongest_component']['score']})"
)
print(
f" Weakest: {conviction['weakest_component']['label']} "
f"({conviction['weakest_component']['score']})"
)
print()
# ========================================================================
# Step 4: Classify Pattern
# ========================================================================
print("Step 4: Pattern Classification")
print("-" * 70)
pattern = classify_pattern(signals, conviction["component_scores"], score)
print(f" Pattern: {pattern['label']} (match: {pattern['match_strength']}%)")
print(f" Description: {pattern['description']}")
print()
# ========================================================================
# Step 5: Generate Allocation
# ========================================================================
print("Step 5: Generating Target Allocation")
print("-" * 70)
regime = signals.get("macro_regime", {}).get("regime", "transitional")
target_alloc = generate_allocation(
conviction_score=score,
zone=zone,
pattern=pattern["pattern"],
regime=regime,
)
sizing = calculate_position_sizing(conviction_score=score, zone=zone)
print(f" Equity: {target_alloc['equity']}%")
print(f" Bonds: {target_alloc['bonds']}%")
print(f" Alternatives: {target_alloc['alternatives']}%")
print(f" Cash: {target_alloc['cash']}%")
print(f" Max Single Position: {sizing['max_single_position']}%")
print()
# ========================================================================
# Step 6: Generate Reports
# ========================================================================
print("Step 6: Generating Reports")
print("-" * 70)
analysis = {
"metadata": {
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"reports_dir": args.reports_dir,
"max_age_hours": args.max_age,
"skills_loaded": len(reports),
"required_count": required_count,
"optional_count": optional_count,
"skills_list": list(reports.keys()),
},
"conviction": conviction,
"pattern": pattern,
"allocation": {
"target": target_alloc,
"regime": regime,
"pattern": pattern["pattern"],
"zone": zone,
},
"position_sizing": sizing,
"input_summary": signals,
}
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
json_file = os.path.join(args.output_dir, f"druckenmiller_strategy_{timestamp}.json")
md_file = os.path.join(args.output_dir, f"druckenmiller_strategy_{timestamp}.md")
generate_json_report(analysis, json_file)
generate_markdown_report(analysis, md_file)
print()
print("=" * 70)
print("Strategy Synthesis Complete")
print("=" * 70)
print(f" Conviction: {score}/100 ({zone})")
print(f" Pattern: {pattern['label']}")
print(f" Equity Target: {target_alloc['equity']}%")
print(f" JSON: {json_file}")
print(f" Markdown: {md_file}")
print()
if __name__ == "__main__":
main()
"""Shared fixtures for Druckenmiller Strategy Synthesizer tests."""
import json
import os
import sys
from datetime import datetime, timedelta
import pytest
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures")
# Add parent dir (scripts/) to path so tests can import modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
def make_report(prefix: str, data: dict, reports_dir: str, age_hours: int = 0) -> str:
"""Write a JSON report file with a given age offset.
Sets both the filename timestamp and the file mtime so that
find_latest_report's age check works correctly.
"""
ts = datetime.now() - timedelta(hours=age_hours)
filename = f"{prefix}{ts.strftime('%Y-%m-%d_%H%M%S')}.json"
path = os.path.join(reports_dir, filename)
with open(path, "w") as f:
json.dump(data, f, indent=2)
# Set file mtime to match the age offset
mtime = ts.timestamp()
os.utime(path, (mtime, mtime))
return path
@pytest.fixture
def make_report_fn():
"""Expose make_report helper as a fixture."""
return make_report
@pytest.fixture
def tmp_reports(tmp_path):
"""Return a temporary reports directory path."""
d = tmp_path / "reports"
d.mkdir()
return str(d)
@pytest.fixture
def sample_breadth():
"""Minimal market_breadth report data."""
return {
"composite": {
"composite_score": 62.8,
"zone": "Healthy",
"zone_color": "blue",
"exposure_guidance": "75-90%",
},
}
@pytest.fixture
def sample_uptrend():
"""Minimal uptrend_analysis report data."""
return {
"composite": {
"composite_score": 66.0,
"zone": "Bull",
"zone_color": "light_green",
"exposure_guidance": "Normal Exposure, Lower End (80-90%)",
},
}
@pytest.fixture
def sample_market_top():
"""Minimal market_top report data."""
return {
"composite": {
"composite_score": 59.2,
"zone": "Orange (Elevated Risk)",
"zone_color": "orange",
"risk_budget": "60-75%",
},
}
@pytest.fixture
def sample_macro_regime():
"""Minimal macro_regime report data."""
return {
"composite": {
"composite_score": 49.0,
"zone": "Transition Zone (Preparing)",
"zone_color": "orange",
},
"regime": {
"current_regime": "broadening",
"regime_label": "Broadening",
"confidence": "medium",
"transition_probability": {
"level": "moderate",
"probability_range": "30-50%",
},
},
}
@pytest.fixture
def sample_ftd():
"""Minimal ftd_detector report data."""
return {
"market_state": {
"combined_state": "FTD_CONFIRMED",
"dual_confirmation": True,
"ftd_index": "Both",
},
"quality_score": {
"total_score": 85,
"signal": "Strong FTD",
"exposure_range": "75-100%",
},
"post_ftd_distribution": {
"distribution_count": 0,
"days_monitored": 3,
},
}
@pytest.fixture
def sample_vcp():
"""Minimal vcp_screener report data."""
return {
"metadata": {
"funnel": {
"universe": 503,
"trend_template_passed": 95,
"vcp_candidates": 95,
},
},
"results": [
{"symbol": "DG", "composite_score": 81.0, "rating": "Strong VCP", "valid_vcp": True},
{"symbol": "PLTR", "composite_score": 75.0, "rating": "Strong VCP", "valid_vcp": True},
{"symbol": "AXON", "composite_score": 70.0, "rating": "Good VCP", "valid_vcp": True},
{"symbol": "META", "composite_score": 55.0, "rating": "Developing", "valid_vcp": True},
],
}
@pytest.fixture
def sample_theme():
"""Minimal theme_detector report data."""
return {
"summary": {
"total_themes": 10,
"bullish_count": 8,
"bearish_count": 2,
},
"themes": {
"all": [
{
"name": "AI & Semiconductors",
"direction": "bullish",
"heat": 92.0,
"stage": "Mid",
"confidence": "High",
},
{
"name": "Oil & Gas",
"direction": "bullish",
"heat": 85.0,
"stage": "Exhausting",
"confidence": "Medium",
},
{
"name": "Biotech",
"direction": "bullish",
"heat": 60.0,
"stage": "Early",
"confidence": "Low",
},
],
},
}
@pytest.fixture
def sample_canslim():
"""Minimal canslim_screener report data."""
return {
"metadata": {
"market_condition": {
"trend": "strong_uptrend",
"M_score": 100,
},
"candidates_analyzed": 35,
},
"results": [
{
"symbol": "NVDA",
"composite_score": 97.2,
"rating": "Exceptional+",
"m_component": {"score": 100, "trend": "strong_uptrend"},
},
{
"symbol": "PLTR",
"composite_score": 82.0,
"rating": "Exceptional",
"m_component": {"score": 100, "trend": "strong_uptrend"},
},
{
"symbol": "CRWD",
"composite_score": 71.0,
"rating": "Strong",
"m_component": {"score": 80, "trend": "uptrend"},
},
],
"summary": {
"total_stocks": 35,
"exceptional": 3,
"strong": 5,
},
}
@pytest.fixture
def all_required_reports(
tmp_reports, sample_breadth, sample_uptrend, sample_market_top, sample_macro_regime, sample_ftd
):
"""Create all 5 required reports in tmp dir and return the dir."""
make_report("market_breadth_", sample_breadth, tmp_reports)
make_report("uptrend_analysis_", sample_uptrend, tmp_reports)
make_report("market_top_", sample_market_top, tmp_reports)
make_report("macro_regime_", sample_macro_regime, tmp_reports)
make_report("ftd_detector_", sample_ftd, tmp_reports)
return tmp_reports
@pytest.fixture
def full_reports(all_required_reports, sample_vcp, sample_theme, sample_canslim):
"""Create all 8 reports (5 required + 3 optional) and return the dir."""
make_report("vcp_screener_", sample_vcp, all_required_reports)
make_report("theme_detector_", sample_theme, all_required_reports)
make_report("canslim_screener_", sample_canslim, all_required_reports)
return all_required_reports
"""Tests for allocation_engine.py"""
from allocation_engine import (
ZONE_BASE_ALLOCATIONS,
calculate_position_sizing,
generate_allocation,
)
class TestBaseAllocations:
"""Verify base allocation structure and constraints."""
def test_all_zones_sum_to_100(self):
"""Every zone's base allocation must sum to 100%."""
for zone, alloc in ZONE_BASE_ALLOCATIONS.items():
total = sum(alloc.values())
assert abs(total - 100) < 0.01, f"Zone '{zone}' sums to {total}, expected 100"
def test_all_zones_present(self):
assert "Maximum Conviction" in ZONE_BASE_ALLOCATIONS
assert "High Conviction" in ZONE_BASE_ALLOCATIONS
assert "Moderate Conviction" in ZONE_BASE_ALLOCATIONS
assert "Low Conviction" in ZONE_BASE_ALLOCATIONS
assert "Capital Preservation" in ZONE_BASE_ALLOCATIONS
class TestGenerateAllocation:
"""Test the full allocation pipeline."""
def test_max_conviction_equity_heavy(self):
result = generate_allocation(
conviction_score=85,
zone="Maximum Conviction",
pattern="policy_pivot_anticipation",
regime="broadening",
)
assert result["equity"] >= 80
assert result["cash"] <= 10
assert _total(result) == 100
def test_capital_preservation_cash_heavy(self):
result = generate_allocation(
conviction_score=10,
zone="Capital Preservation",
pattern="unsustainable_distortion",
regime="contraction",
)
assert result["cash"] >= 35
assert result["equity"] <= 30
assert _total(result) == 100
def test_high_conviction_balanced(self):
result = generate_allocation(
conviction_score=70,
zone="High Conviction",
pattern="policy_pivot_anticipation",
regime="broadening",
)
assert result["equity"] >= 60
assert _total(result) == 100
def test_moderate_conviction(self):
result = generate_allocation(
conviction_score=50,
zone="Moderate Conviction",
pattern="wait_and_observe",
regime="transitional",
)
assert 40 <= result["equity"] <= 70
assert _total(result) == 100
def test_low_conviction(self):
result = generate_allocation(
conviction_score=30,
zone="Low Conviction",
pattern="wait_and_observe",
regime="contraction",
)
assert result["equity"] <= 50
assert result["cash"] >= 25
assert _total(result) == 100
def test_contraction_regime_shifts_to_defensive(self):
"""Contraction regime should reduce equity vs broadening."""
broadening = generate_allocation(
conviction_score=60,
zone="High Conviction",
pattern="wait_and_observe",
regime="broadening",
)
contraction = generate_allocation(
conviction_score=60,
zone="High Conviction",
pattern="wait_and_observe",
regime="contraction",
)
assert contraction["equity"] < broadening["equity"]
def test_wait_pattern_raises_cash(self):
"""wait_and_observe pattern should increase cash allocation."""
active = generate_allocation(
conviction_score=60,
zone="High Conviction",
pattern="policy_pivot_anticipation",
regime="broadening",
)
wait = generate_allocation(
conviction_score=60,
zone="High Conviction",
pattern="wait_and_observe",
regime="broadening",
)
assert wait["cash"] >= active["cash"]
def test_output_has_all_asset_classes(self):
result = generate_allocation(
conviction_score=50,
zone="Moderate Conviction",
pattern="wait_and_observe",
regime="transitional",
)
assert "equity" in result
assert "cash" in result
assert "bonds" in result
assert "alternatives" in result
def test_all_values_non_negative(self):
result = generate_allocation(
conviction_score=50,
zone="Moderate Conviction",
pattern="wait_and_observe",
regime="transitional",
)
for k, v in result.items():
if k != "rationale":
assert v >= 0, f"{k} is negative: {v}"
def test_extreme_contrarian_boosts_equity(self):
"""Extreme contrarian (FTD confirmed in bear) should be aggressive."""
normal = generate_allocation(
conviction_score=40,
zone="Moderate Conviction",
pattern="wait_and_observe",
regime="contraction",
)
contrarian = generate_allocation(
conviction_score=40,
zone="Moderate Conviction",
pattern="extreme_sentiment_contrarian",
regime="contraction",
)
assert contrarian["equity"] >= normal["equity"]
class TestPositionSizing:
"""Test position sizing calculations."""
def test_max_conviction_large_positions(self):
sizing = calculate_position_sizing(conviction_score=85, zone="Maximum Conviction")
assert sizing["max_single_position"] >= 15
assert sizing["daily_vol_target"] >= 0.3
def test_preservation_small_positions(self):
sizing = calculate_position_sizing(conviction_score=10, zone="Capital Preservation")
assert sizing["max_single_position"] <= 5
assert sizing["daily_vol_target"] <= 0.2
def test_output_structure(self):
sizing = calculate_position_sizing(conviction_score=50, zone="Moderate Conviction")
assert "max_single_position" in sizing
assert "daily_vol_target" in sizing
assert "max_positions" in sizing
def _total(alloc: dict) -> float:
"""Sum only numeric allocation values (rounded to avoid FP noise)."""
return round(sum(v for k, v in alloc.items() if isinstance(v, (int, float))), 1)
"""Tests for report_generator.py"""
import json
from report_generator import generate_json_report, generate_markdown_report
def _build_analysis(
score=65,
zone="High Conviction",
zone_color="light_green",
pattern="policy_pivot_anticipation",
match_strength=75,
include_optional=True,
):
"""Build a minimal but complete analysis dict for report generation."""
component_scores = {
"market_structure": {
"score": 70,
"weight": 0.18,
"effective_weight": 0.18,
"available": True,
"weighted_contribution": 12.6,
"label": "Market Structure (Breadth + Uptrend)",
},
"distribution_risk": {
"score": 60,
"weight": 0.18,
"effective_weight": 0.18,
"available": True,
"weighted_contribution": 10.8,
"label": "Distribution Risk (Market Top, inverted)",
},
"bottom_confirmation": {
"score": 80,
"weight": 0.12,
"effective_weight": 0.12,
"available": True,
"weighted_contribution": 9.6,
"label": "Bottom Confirmation (FTD Detector)",
},
"macro_alignment": {
"score": 75,
"weight": 0.18,
"effective_weight": 0.18,
"available": True,
"weighted_contribution": 13.5,
"label": "Macro Alignment (Regime)",
},
"theme_quality": {
"score": 50 if include_optional else 50,
"weight": 0.12,
"effective_weight": 0.12 if include_optional else 0.0,
"available": include_optional,
"weighted_contribution": 6.0 if include_optional else 0.0,
"label": "Theme Quality (Theme Detector)",
},
"setup_availability": {
"score": 55 if include_optional else 50,
"weight": 0.10,
"effective_weight": 0.10 if include_optional else 0.0,
"available": include_optional,
"weighted_contribution": 5.5 if include_optional else 0.0,
"label": "Setup Availability (VCP + CANSLIM)",
},
"signal_convergence": {
"score": 72,
"weight": 0.12,
"effective_weight": 0.12,
"available": True,
"weighted_contribution": 8.6,
"label": "Signal Convergence (Cross-Skill Agreement)",
},
}
input_summary = {
"market_breadth": {"composite_score": 65, "zone": "Healthy"},
"uptrend_analysis": {"composite_score": 70, "zone": "Bull"},
"market_top": {"composite_score": 40, "zone": "Yellow"},
"macro_regime": {
"composite_score": 60,
"regime": "broadening",
"confidence": "medium",
},
"ftd_detector": {"state": "FTD_CONFIRMED", "quality_score": 80},
}
if include_optional:
input_summary["vcp_screener"] = {
"derived_score": 55,
"textbook_count": 1,
"strong_count": 2,
}
input_summary["theme_detector"] = {
"derived_score": 50,
"hot_count": 3,
"exhaustion_count": 1,
}
input_summary["canslim_screener"] = {
"derived_score": 60,
"m_score": 80,
"exceptional_count": 2,
}
return {
"metadata": {
"generated_at": "2026-02-19 12:00:00",
"reports_dir": "reports/",
"max_age_hours": 72,
"skills_loaded": 8 if include_optional else 5,
"required_count": 5,
"optional_count": 3 if include_optional else 0,
"skills_list": list(input_summary.keys()),
},
"conviction": {
"conviction_score": score,
"zone": zone,
"zone_color": zone_color,
"exposure_range": "70-90%",
"guidance": "Multiple signals confirm.",
"actions": [
"Above-average equity allocation (70-90%)",
"New entries on quality setups",
],
"strongest_component": {
"component": "bottom_confirmation",
"label": "Bottom Confirmation (FTD Detector)",
"score": 80,
},
"weakest_component": {
"component": "theme_quality",
"label": "Theme Quality (Theme Detector)",
"score": 50,
},
"data_quality": {"available_count": 7, "total_components": 7},
"component_scores": component_scores,
},
"pattern": {
"pattern": pattern,
"label": "Policy Pivot Anticipation",
"description": "Central bank policy at inflection point.",
"match_strength": match_strength,
"all_pattern_scores": {
"policy_pivot_anticipation": match_strength,
"unsustainable_distortion": 20,
"extreme_sentiment_contrarian": 15,
"wait_and_observe": 40,
},
},
"allocation": {
"target": {"equity": 75, "bonds": 5, "alternatives": 5, "cash": 15},
"regime": "broadening",
"pattern": pattern,
"zone": zone,
},
"position_sizing": {
"max_single_position": 15,
"daily_vol_target": 0.3,
"max_positions": 12,
},
"input_summary": input_summary,
}
class TestJsonReport:
"""Test JSON report generation."""
def test_json_output_valid(self, tmp_path):
"""Generated JSON must be parseable."""
analysis = _build_analysis()
out = str(tmp_path / "report.json")
generate_json_report(analysis, out)
with open(out) as f:
data = json.load(f)
assert data["conviction"]["conviction_score"] == 65
def test_json_has_all_top_keys(self, tmp_path):
"""JSON must contain all top-level sections."""
analysis = _build_analysis()
out = str(tmp_path / "report.json")
generate_json_report(analysis, out)
with open(out) as f:
data = json.load(f)
for key in [
"metadata",
"conviction",
"pattern",
"allocation",
"position_sizing",
"input_summary",
]:
assert key in data, f"Missing top-level key: {key}"
def test_json_empty_input(self, tmp_path):
"""Empty analysis should still produce valid JSON."""
out = str(tmp_path / "empty.json")
generate_json_report({}, out)
with open(out) as f:
data = json.load(f)
assert isinstance(data, dict)
class TestMarkdownReport:
"""Test Markdown report generation."""
def _generate_md(self, tmp_path, **kwargs):
"""Helper to generate and read markdown."""
analysis = _build_analysis(**kwargs)
out = str(tmp_path / "report.md")
generate_markdown_report(analysis, out)
with open(out) as f:
return f.read()
def test_all_sections_present(self, tmp_path):
"""All 8 sections must appear in the markdown."""
md = self._generate_md(tmp_path)
expected_sections = [
"## 1. Conviction Dashboard",
"## 2. Pattern Classification",
"## 3. Component Scores",
"## 4. Input Skills Summary",
"## 5. Target Allocation",
"## 6. Position Sizing & Risk",
"## 7. Druckenmiller Principle",
"## Methodology",
]
for section in expected_sections:
assert section in md, f"Missing section: {section}"
def test_conviction_score_embedded(self, tmp_path):
"""Conviction score must appear in the dashboard."""
md = self._generate_md(tmp_path, score=72)
assert "72/100" in md
def test_pattern_displayed(self, tmp_path):
"""Detected pattern must appear."""
md = self._generate_md(tmp_path)
assert "Policy Pivot Anticipation" in md
def test_allocation_table(self, tmp_path):
"""Allocation table must show all 4 asset classes."""
md = self._generate_md(tmp_path)
for asset in ["Equity", "Bonds", "Alternatives", "Cash"]:
assert asset in md
def test_disclaimer_present(self, tmp_path):
"""Disclaimer must appear at the end."""
md = self._generate_md(tmp_path)
assert "Disclaimer" in md
assert "educational and informational purposes" in md
def test_optional_skills_not_available(self, tmp_path):
"""Optional skills without data should show 'Not available'."""
md = self._generate_md(tmp_path, include_optional=False)
assert "Not available" in md
def test_component_effective_weight_column(self, tmp_path):
"""Component table must include Eff. Weight column."""
md = self._generate_md(tmp_path)
assert "Eff. Weight" in md
def test_unavailable_component_marked_na(self, tmp_path):
"""Unavailable components should show (N/A) marker."""
md = self._generate_md(tmp_path, include_optional=False)
assert "(N/A)" in md
def test_pattern_scores_table(self, tmp_path):
"""All 4 pattern scores should appear."""
md = self._generate_md(tmp_path)
for pattern_name in [
"Policy Pivot Anticipation",
"Unsustainable Distortion",
"Extreme Sentiment Contrarian",
"Wait And Observe",
]:
assert pattern_name in md
def test_druckenmiller_quote(self, tmp_path):
"""A Druckenmiller quote should appear in section 7."""
md = self._generate_md(tmp_path)
assert "Stanley Druckenmiller" in md
"""Tests for report_loader.py"""
import os
import pytest
from report_loader import (
extract_signal,
find_latest_report,
load_all_reports,
)
class TestFindLatestReport:
"""Tests for find_latest_report()."""
def test_finds_most_recent(self, tmp_reports, sample_breadth, make_report_fn):
"""Should return the most recently timestamped file."""
make_report_fn("market_breadth_", {"old": True}, tmp_reports, age_hours=5)
make_report_fn("market_breadth_", sample_breadth, tmp_reports, age_hours=0)
make_report_fn("market_breadth_", {"mid": True}, tmp_reports, age_hours=2)
result = find_latest_report(tmp_reports, "market_breadth_")
assert result is not None
path, data = result
assert data.get("composite", {}).get("composite_score") == 62.8
def test_returns_none_when_no_match(self, tmp_reports):
"""Should return None if no files match the prefix."""
result = find_latest_report(tmp_reports, "nonexistent_")
assert result is None
def test_ignores_non_json(self, tmp_reports):
"""Should ignore .md files with matching prefix."""
md_path = os.path.join(tmp_reports, "market_breadth_2026-02-19.md")
with open(md_path, "w") as f:
f.write("# Report")
result = find_latest_report(tmp_reports, "market_breadth_")
assert result is None
def test_respects_max_age(self, tmp_reports, sample_breadth, make_report_fn):
"""Should reject reports older than max_age_hours."""
make_report_fn("market_breadth_", sample_breadth, tmp_reports, age_hours=100)
result = find_latest_report(tmp_reports, "market_breadth_", max_age_hours=72)
assert result is None
def test_accepts_within_max_age(self, tmp_reports, sample_breadth, make_report_fn):
"""Should accept reports within max_age_hours."""
make_report_fn("market_breadth_", sample_breadth, tmp_reports, age_hours=24)
result = find_latest_report(tmp_reports, "market_breadth_", max_age_hours=72)
assert result is not None
class TestLoadAllReports:
"""Tests for load_all_reports()."""
def test_loads_all_required(self, all_required_reports):
"""Should load all 5 required skill reports."""
reports = load_all_reports(all_required_reports)
assert "market_breadth" in reports
assert "uptrend_analysis" in reports
assert "market_top" in reports
assert "macro_regime" in reports
assert "ftd_detector" in reports
def test_loads_optional_when_present(self, full_reports):
"""Should include optional reports when available."""
reports = load_all_reports(full_reports)
assert "vcp_screener" in reports
assert "theme_detector" in reports
assert "canslim_screener" in reports
def test_missing_required_raises(self, tmp_reports, sample_breadth, make_report_fn):
"""Should raise ValueError when required report is missing."""
make_report_fn("market_breadth_", sample_breadth, tmp_reports)
with pytest.raises(ValueError, match="required"):
load_all_reports(tmp_reports)
def test_missing_optional_ok(self, all_required_reports):
"""Should succeed even without optional reports."""
reports = load_all_reports(all_required_reports)
assert "vcp_screener" not in reports
assert "theme_detector" not in reports
assert "canslim_screener" not in reports
assert len(reports) == 5
def test_stale_required_raises(
self,
tmp_reports,
sample_breadth,
sample_uptrend,
sample_market_top,
sample_macro_regime,
sample_ftd,
make_report_fn,
):
"""Should raise if required report exceeds max_age_hours."""
make_report_fn("market_breadth_", sample_breadth, tmp_reports, age_hours=100)
make_report_fn("uptrend_analysis_", sample_uptrend, tmp_reports)
make_report_fn("market_top_", sample_market_top, tmp_reports)
make_report_fn("macro_regime_", sample_macro_regime, tmp_reports)
make_report_fn("ftd_detector_", sample_ftd, tmp_reports)
with pytest.raises(ValueError, match="required"):
load_all_reports(tmp_reports, max_age_hours=72)
class TestExtractSignal:
"""Tests for extract_signal() signal extraction from each skill."""
def test_breadth_signal(self, sample_breadth):
sig = extract_signal("market_breadth", sample_breadth)
assert sig["composite_score"] == 62.8
assert sig["zone"] == "Healthy"
assert "source" in sig
def test_uptrend_signal(self, sample_uptrend):
sig = extract_signal("uptrend_analysis", sample_uptrend)
assert sig["composite_score"] == 66.0
assert sig["zone"] == "Bull"
def test_market_top_signal(self, sample_market_top):
sig = extract_signal("market_top", sample_market_top)
assert sig["composite_score"] == 59.2
def test_macro_regime_signal(self, sample_macro_regime):
sig = extract_signal("macro_regime", sample_macro_regime)
assert sig["composite_score"] == 49.0
assert sig["regime"] == "broadening"
assert sig["confidence"] == "medium"
def test_ftd_signal(self, sample_ftd):
sig = extract_signal("ftd_detector", sample_ftd)
assert sig["state"] == "FTD_CONFIRMED"
assert sig["quality_score"] == 85
assert sig["dual_confirmation"] is True
def test_vcp_derived_score(self, sample_vcp):
"""VCP score should be derived from rating distribution + funnel health."""
sig = extract_signal("vcp_screener", sample_vcp)
assert 0 <= sig["derived_score"] <= 100
assert sig["textbook_count"] == 0
assert sig["strong_count"] == 2
def test_theme_derived_score(self, sample_theme):
"""Theme score should be derived from hot themes + lifecycle."""
sig = extract_signal("theme_detector", sample_theme)
assert 0 <= sig["derived_score"] <= 100
assert sig["hot_count"] >= 0
def test_canslim_signal(self, sample_canslim):
"""CANSLIM signal extracts composite_score directly + M component."""
sig = extract_signal("canslim_screener", sample_canslim)
assert 0 <= sig["derived_score"] <= 100
assert sig["m_score"] == 100
assert sig["m_trend"] == "strong_uptrend"
def test_vcp_empty_results(self):
"""VCP with no results should yield low score."""
data = {
"metadata": {
"funnel": {"universe": 500, "trend_template_passed": 0, "vcp_candidates": 0}
},
"results": [],
}
sig = extract_signal("vcp_screener", data)
assert sig["derived_score"] <= 15
def test_theme_all_exhausting(self):
"""Themes all in exhaustion should get penalty."""
data = {
"summary": {"total_themes": 3, "bullish_count": 3, "bearish_count": 0},
"themes": {
"all": [
{
"name": "T1",
"direction": "bullish",
"heat": 90,
"stage": "Exhausting",
"confidence": "Low",
},
{
"name": "T2",
"direction": "bullish",
"heat": 80,
"stage": "Exhausting",
"confidence": "Low",
},
{
"name": "T3",
"direction": "bullish",
"heat": 70,
"stage": "Exhausting",
"confidence": "Low",
},
]
},
}
sig = extract_signal("theme_detector", data)
assert sig["derived_score"] < 60 # Exhaustion penalty applied
"""E2E tests for the full strategy_synthesizer pipeline."""
from allocation_engine import calculate_position_sizing, generate_allocation
from report_loader import OPTIONAL_SKILLS, REQUIRED_SKILLS, extract_signal, load_all_reports
from scorer import calculate_composite_conviction, classify_pattern
def _run_pipeline(reports_dir, max_age=72):
"""Execute the full synthesizer pipeline and return the analysis dict."""
reports = load_all_reports(reports_dir, max_age_hours=max_age)
signals = {}
for skill_name, report_data in reports.items():
signals[skill_name] = extract_signal(skill_name, report_data)
conviction = calculate_composite_conviction(signals)
score = conviction["conviction_score"]
zone = conviction["zone"]
pattern = classify_pattern(signals, conviction["component_scores"], score)
regime = signals.get("macro_regime", {}).get("regime", "transitional")
allocation = generate_allocation(
conviction_score=score,
zone=zone,
pattern=pattern["pattern"],
regime=regime,
)
sizing = calculate_position_sizing(conviction_score=score, zone=zone)
return {
"reports": reports,
"signals": signals,
"conviction": conviction,
"pattern": pattern,
"allocation": allocation,
"sizing": sizing,
}
class TestFullPipeline:
"""E2E pipeline tests using fixture reports."""
def test_full_pipeline_all_skills(self, full_reports):
"""8-skill full pipeline: load -> extract -> score -> pattern -> alloc."""
result = _run_pipeline(full_reports)
assert len(result["reports"]) == 8
assert len(result["signals"]) == 8
conv = result["conviction"]
assert 0 <= conv["conviction_score"] <= 100
assert conv["zone"] in [
"Maximum Conviction",
"High Conviction",
"Moderate Conviction",
"Low Conviction",
"Capital Preservation",
]
pattern = result["pattern"]
assert pattern["pattern"] in [
"policy_pivot_anticipation",
"unsustainable_distortion",
"extreme_sentiment_contrarian",
"wait_and_observe",
]
assert 0 <= pattern["match_strength"] <= 100
def test_full_pipeline_required_only(self, all_required_reports):
"""5 required skills only: pipeline completes with optional unavailable."""
result = _run_pipeline(all_required_reports)
assert len(result["reports"]) == 5
for req in REQUIRED_SKILLS:
assert req in result["signals"]
# Optional skills not in signals
for opt in OPTIONAL_SKILLS:
assert opt not in result["signals"]
# Theme and setup components should be unavailable
cs = result["conviction"]["component_scores"]
assert cs["theme_quality"]["available"] is False
assert cs["setup_availability"]["available"] is False
assert cs["theme_quality"]["effective_weight"] == 0.0
assert cs["setup_availability"]["effective_weight"] == 0.0
def test_conviction_score_consistency(self, full_reports):
"""Same input must produce identical output (deterministic)."""
r1 = _run_pipeline(full_reports)
r2 = _run_pipeline(full_reports)
assert r1["conviction"]["conviction_score"] == r2["conviction"]["conviction_score"]
assert r1["pattern"]["pattern"] == r2["pattern"]["pattern"]
assert r1["allocation"] == r2["allocation"]
def test_allocation_sums_to_100_across_zones(self, full_reports):
"""All 5 zones must produce allocations that sum to 100%."""
zones = [
"Maximum Conviction",
"High Conviction",
"Moderate Conviction",
"Low Conviction",
"Capital Preservation",
]
patterns = [
"policy_pivot_anticipation",
"unsustainable_distortion",
"extreme_sentiment_contrarian",
"wait_and_observe",
]
for zone in zones:
for pattern in patterns:
alloc = generate_allocation(
conviction_score=50,
zone=zone,
pattern=pattern,
regime="transitional",
)
total = sum(alloc.values())
assert abs(total - 100) < 0.2, (
f"Zone={zone}, Pattern={pattern}: allocation={total}%"
)
Related skills
How it compares
Pick stanley-druckenmiller-investment over single-strategy trading skills when the goal is cross-signal conviction scoring and exposure guidance rather than one isolated indicator.
FAQ
What does the stanley-druckenmiller-investment conviction score measure?
The stanley-druckenmiller-investment skill outputs a conviction score out of 100 alongside a zone label and recommended exposure range. The dashboard also ranks strongest and weakest strategy components to guide position sizing.
Which inputs does stanley-druckenmiller-investment require?
The stanley-druckenmiller-investment skill loads multiple upstream trading strategy skills—both required and optional—and synthesizes them through report_generator.py into a single Druckenmiller Strategy Synthesizer Report.
Is Stanley Druckenmiller Investment safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.