opencode-skills-antigravity 1.0.11 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/aegisops-ai/SKILL.md +127 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/xvary-stock-research/LICENSE +21 -0
- package/bundled-skills/xvary-stock-research/SKILL.md +103 -0
- package/bundled-skills/xvary-stock-research/assets/nvda-deep-dive-hero.png +0 -0
- package/bundled-skills/xvary-stock-research/assets/nvda-deep-dive-scenarios.png +0 -0
- package/bundled-skills/xvary-stock-research/assets/nvda-deep-dive-thesis.png +0 -0
- package/bundled-skills/xvary-stock-research/assets/social-preview.png +0 -0
- package/bundled-skills/xvary-stock-research/examples/nvda-analysis.md +60 -0
- package/bundled-skills/xvary-stock-research/references/edgar-guide.md +53 -0
- package/bundled-skills/xvary-stock-research/references/methodology.md +153 -0
- package/bundled-skills/xvary-stock-research/references/scoring.md +111 -0
- package/bundled-skills/xvary-stock-research/tests/test_edgar.py +90 -0
- package/bundled-skills/xvary-stock-research/tests/test_market.py +113 -0
- package/bundled-skills/xvary-stock-research/tools/edgar.py +495 -0
- package/bundled-skills/xvary-stock-research/tools/market.py +302 -0
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# XVARY Methodology (Public Framework)
|
|
2
|
+
|
|
3
|
+
This document is the **public framework** for XVARY Research.
|
|
4
|
+
|
|
5
|
+
It is intentionally the **menu, not the recipe**: stage names, logic flow, and decision philosophy are published; internal prompts, thresholds, and convergence algorithms are not.
|
|
6
|
+
|
|
7
|
+
Full narrative: [xvary.com/methodology](https://xvary.com/methodology)
|
|
8
|
+
|
|
9
|
+
## Research Philosophy
|
|
10
|
+
|
|
11
|
+
XVARY is built around five principles:
|
|
12
|
+
|
|
13
|
+
1. **Variant perception first**: value comes from being directionally right where consensus is wrong.
|
|
14
|
+
2. **Evidence before narrative**: facts constrain the story, not the other way around.
|
|
15
|
+
3. **Conviction is earned**: scores reflect cross-validated support, not tone or confidence theater.
|
|
16
|
+
4. **Adversarial challenge is mandatory**: every thesis gets attacked before publication.
|
|
17
|
+
5. **Kill-file discipline**: each call includes explicit thesis-invalidating conditions.
|
|
18
|
+
|
|
19
|
+
## 22-Stage Operational DAG (21-Stage Research Spine + Finalize)
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
flowchart TD
|
|
23
|
+
s1[directive_selection] --> s2[phase_a]
|
|
24
|
+
s2 --> s3[data_quality_gate]
|
|
25
|
+
s3 --> s4[evidence_gap_analysis]
|
|
26
|
+
s4 --> s5[kvd_hypothesis]
|
|
27
|
+
s4 --> s6[pane_selection]
|
|
28
|
+
s6 --> s7[quant_foundation]
|
|
29
|
+
s7 --> s8[model_quality_gate]
|
|
30
|
+
s6 --> s9[phase_b]
|
|
31
|
+
s5 --> s9
|
|
32
|
+
s9 --> s10[triangulation]
|
|
33
|
+
s10 --> s11[pillar_discovery]
|
|
34
|
+
s11 --> s12[phase_c]
|
|
35
|
+
s11 --> s13[why_tree]
|
|
36
|
+
s12 --> s14[quality_gate]
|
|
37
|
+
s13 --> s14
|
|
38
|
+
s14 --> s15[challenge]
|
|
39
|
+
s15 --> s16[synthesis]
|
|
40
|
+
s16 --> s17[audit]
|
|
41
|
+
s17 --> s18[report_json]
|
|
42
|
+
s18 --> s19[audience_calibration]
|
|
43
|
+
s18 --> s20[compliance_audit]
|
|
44
|
+
s19 --> s21[completion_loop]
|
|
45
|
+
s20 --> s21
|
|
46
|
+
s21 --> s22[finalize]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> The operational DAG has 22 nodes in code (`finalize` included). Publicly we refer to the core research spine as the 21-stage methodology and treat finalization as release control.
|
|
50
|
+
|
|
51
|
+
### Stage Intent (One-Line)
|
|
52
|
+
|
|
53
|
+
1. `directive_selection`: choose sector/style evidence directives.
|
|
54
|
+
2. `phase_a`: collect baseline facts, filings, market context, and broad evidence.
|
|
55
|
+
3. `data_quality_gate`: block low-integrity factual inputs.
|
|
56
|
+
4. `evidence_gap_analysis`: detect missing evidence and open targeted searches.
|
|
57
|
+
5. `kvd_hypothesis`: identify candidate key value drivers.
|
|
58
|
+
6. `pane_selection`: choose report panes for company profile.
|
|
59
|
+
7. `quant_foundation`: build model scaffolding (valuation/risk context).
|
|
60
|
+
8. `model_quality_gate`: sanity-check model outputs before synthesis.
|
|
61
|
+
9. `phase_b`: run enrichment search and deeper context collection.
|
|
62
|
+
10. `triangulation`: compare evidence across independent reasoning vectors.
|
|
63
|
+
11. `pillar_discovery`: derive weighted thesis pillars.
|
|
64
|
+
12. `phase_c`: execute module-level synthesis in parallel.
|
|
65
|
+
13. `why_tree`: decompose causal claims and dependency chains.
|
|
66
|
+
14. `quality_gate`: run structured quality tests and consistency checks.
|
|
67
|
+
15. `challenge`: adversarially test each pillar and assumptions.
|
|
68
|
+
16. `synthesis`: assemble conviction, variant view, and scenario posture.
|
|
69
|
+
17. `audit`: multi-role verification with follow-up rounds.
|
|
70
|
+
18. `report_json`: build structured report payload.
|
|
71
|
+
19. `audience_calibration`: ensure readability + decision-usefulness.
|
|
72
|
+
20. `compliance_audit`: verify methodology and policy compliance.
|
|
73
|
+
21. `completion_loop`: repair sparse or inconsistent sections.
|
|
74
|
+
22. `finalize`: release gating and artifact finalization.
|
|
75
|
+
|
|
76
|
+
## Quality Gates (Public Names + What They Check)
|
|
77
|
+
|
|
78
|
+
- **Data Quality Gate**: missingness, stale fields, broken units, filing coherence.
|
|
79
|
+
- **Model Quality Gate**: sanity bounds, impossible outputs, assumption integrity.
|
|
80
|
+
- **Quality Gate**: cross-module consistency, contradiction flags, evidence sufficiency.
|
|
81
|
+
- **Audience Calibration**: clarity, thesis readability, decision speed under time pressure.
|
|
82
|
+
- **Compliance Audit**: methodology adherence, sourcing hygiene, output policy checks.
|
|
83
|
+
- **Finalize Gate**: final validation + publication readiness.
|
|
84
|
+
|
|
85
|
+
## 23 Research Modules
|
|
86
|
+
|
|
87
|
+
1. `kvd`: key value-driver identification and trajectory framing.
|
|
88
|
+
2. `core_facts`: baseline thesis framing and variant setup.
|
|
89
|
+
3. `operations`: revenue engine, segment economics, moat mechanics.
|
|
90
|
+
4. `financials`: profitability, balance-sheet quality, cash conversion.
|
|
91
|
+
5. `valuation`: intrinsic range, scenario math, and expectation gap.
|
|
92
|
+
6. `management`: leadership quality, incentives, and execution credibility.
|
|
93
|
+
7. `competition`: market structure, rival dynamics, strategic pressure.
|
|
94
|
+
8. `risk`: kill criteria, thesis breakers, and downside maps.
|
|
95
|
+
9. `capital_allocation`: buybacks/dividends/M&A capital discipline.
|
|
96
|
+
10. `governance`: board structure, oversight quality, shareholder alignment.
|
|
97
|
+
11. `catalysts`: event map and timing-sensitive thesis triggers.
|
|
98
|
+
12. `product_tech`: product moat, roadmap durability, and innovation path.
|
|
99
|
+
13. `supply_chain`: supplier dependency, resilience, and bottleneck exposure.
|
|
100
|
+
14. `tam`: market size realism, penetration runway, and saturation risk.
|
|
101
|
+
15. `street`: consensus expectations vs. internal thesis.
|
|
102
|
+
16. `macro_sensitivity`: rates/FX/cycle sensitivity mapping.
|
|
103
|
+
17. `value_framework`: investment framework fit + decision rubric.
|
|
104
|
+
18. `quant_profile`: factor, drawdown, and liquidity behavior profile.
|
|
105
|
+
19. `signals`: alternative/leading indicators and signal dashboard.
|
|
106
|
+
20. `derivs`: options/short-interest positioning context.
|
|
107
|
+
21. `earnings_track`: beat/miss quality and guidance reliability.
|
|
108
|
+
22. `history`: strategic timeline and historical analog framing.
|
|
109
|
+
23. `executive_summary`: cross-module synthesis for fast decisioning.
|
|
110
|
+
|
|
111
|
+
## Conviction Scoring (Concept)
|
|
112
|
+
|
|
113
|
+
Conviction is built from weighted pillars rather than a single-model output:
|
|
114
|
+
|
|
115
|
+
- Pillar strength (how well each core claim is supported)
|
|
116
|
+
- Pillar dependency risk (how fragile each claim is)
|
|
117
|
+
- Cross-module consistency (do independent modules agree?)
|
|
118
|
+
- Adversarial challenge survival (did core claims hold up?)
|
|
119
|
+
- Downside asymmetry under identified kill criteria
|
|
120
|
+
|
|
121
|
+
Weights are dynamic by business model and evidence reliability. Exact calibration is proprietary.
|
|
122
|
+
|
|
123
|
+
## Kill-File Risks (Concept)
|
|
124
|
+
|
|
125
|
+
Every thesis is paired with explicit conditions that invalidate it. A kill file is not a downside list; it is the shortest set of assumptions that, if broken, forces re-underwriting.
|
|
126
|
+
|
|
127
|
+
Typical kill-file categories:
|
|
128
|
+
|
|
129
|
+
- Structural demand break
|
|
130
|
+
- Unit-economics deterioration
|
|
131
|
+
- Balance-sheet fragility
|
|
132
|
+
- Regulatory/regime shock
|
|
133
|
+
- Management credibility failure
|
|
134
|
+
|
|
135
|
+
## Five-Vector Triangulation (Concept)
|
|
136
|
+
|
|
137
|
+
Each ticker is evaluated through five independent vectors before synthesis:
|
|
138
|
+
|
|
139
|
+
1. **Accounting reality**
|
|
140
|
+
2. **Market-implied expectations**
|
|
141
|
+
3. **Operational execution**
|
|
142
|
+
4. **Strategic position / industry structure**
|
|
143
|
+
5. **Macro-regime sensitivity**
|
|
144
|
+
|
|
145
|
+
The goal is convergence testing: where vectors agree, conviction rises; where they diverge, uncertainty is made explicit.
|
|
146
|
+
|
|
147
|
+
## Intentionally Not Published
|
|
148
|
+
|
|
149
|
+
- Module prompt templates
|
|
150
|
+
- Prompt routing logic and fallback trees
|
|
151
|
+
- Threshold matrices and gating cutoffs
|
|
152
|
+
- Internal convergence scoring mechanics
|
|
153
|
+
- Sector-specific directive libraries
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# XVARY Scores (Public Definitions)
|
|
2
|
+
|
|
3
|
+
This file defines the **public** score framework used by the skill.
|
|
4
|
+
|
|
5
|
+
Important: production XVARY systems use proprietary calibrations. The equations below expose the logic shape, not private threshold tables.
|
|
6
|
+
|
|
7
|
+
## Score Scale
|
|
8
|
+
|
|
9
|
+
All scores are normalized to `0-100`.
|
|
10
|
+
|
|
11
|
+
- `80-100`: Strong
|
|
12
|
+
- `60-79`: Constructive
|
|
13
|
+
- `40-59`: Mixed
|
|
14
|
+
- `0-39`: Weak
|
|
15
|
+
|
|
16
|
+
## Inputs
|
|
17
|
+
|
|
18
|
+
Inputs come from:
|
|
19
|
+
|
|
20
|
+
- `tools/edgar.py` (filings + fundamentals)
|
|
21
|
+
- `tools/market.py` (price + valuation context)
|
|
22
|
+
|
|
23
|
+
The public skill uses the latest annual and quarterly data where available.
|
|
24
|
+
|
|
25
|
+
## 1) Momentum Score
|
|
26
|
+
|
|
27
|
+
Measures forward drive in fundamentals + market behavior.
|
|
28
|
+
|
|
29
|
+
Public formula shape:
|
|
30
|
+
|
|
31
|
+
`Momentum = 100 * (w1*Growth + w2*Revision + w3*RelativeStrength + w4*OperatingLeverage)`
|
|
32
|
+
|
|
33
|
+
Component definitions (normalized to `0-1`):
|
|
34
|
+
|
|
35
|
+
- `Growth`: revenue/EPS growth persistence
|
|
36
|
+
- `Revision`: direction of estimate/expectation changes
|
|
37
|
+
- `RelativeStrength`: recent relative price performance
|
|
38
|
+
- `OperatingLeverage`: incremental profit conversion on growth
|
|
39
|
+
|
|
40
|
+
## 2) Stability Score
|
|
41
|
+
|
|
42
|
+
Measures durability and variance control.
|
|
43
|
+
|
|
44
|
+
Public formula shape:
|
|
45
|
+
|
|
46
|
+
`Stability = 100 * (w1*MarginStability + w2*CashFlowStability + w3*CyclicalityBuffer + w4*ExecutionConsistency)`
|
|
47
|
+
|
|
48
|
+
Components:
|
|
49
|
+
|
|
50
|
+
- `MarginStability`: volatility in gross/operating profile
|
|
51
|
+
- `CashFlowStability`: operating cash-flow consistency
|
|
52
|
+
- `CyclicalityBuffer`: sensitivity to external demand shocks
|
|
53
|
+
- `ExecutionConsistency`: beat/miss and guidance reliability trend
|
|
54
|
+
|
|
55
|
+
## 3) Financial Health Score
|
|
56
|
+
|
|
57
|
+
Measures solvency quality and balance-sheet resilience.
|
|
58
|
+
|
|
59
|
+
Public formula shape:
|
|
60
|
+
|
|
61
|
+
`FinancialHealth = 100 * (w1*Liquidity + w2*Leverage + w3*Coverage + w4*CashConversion)`
|
|
62
|
+
|
|
63
|
+
Components:
|
|
64
|
+
|
|
65
|
+
- `Liquidity`: cash + near-term flexibility
|
|
66
|
+
- `Leverage`: debt load relative to earnings power
|
|
67
|
+
- `Coverage`: debt service coverage strength
|
|
68
|
+
- `CashConversion`: earnings-to-cash realization quality
|
|
69
|
+
|
|
70
|
+
## 4) Upside Estimate Score
|
|
71
|
+
|
|
72
|
+
Measures risk-reward asymmetry vs. implied expectations.
|
|
73
|
+
|
|
74
|
+
Public formula shape:
|
|
75
|
+
|
|
76
|
+
`Upside = 100 * (w1*IntrinsicGap + w2*ScenarioAsymmetry + w3*CatalystDensity + w4*ExpectationMispricing)`
|
|
77
|
+
|
|
78
|
+
Components:
|
|
79
|
+
|
|
80
|
+
- `IntrinsicGap`: conservative value range minus current price
|
|
81
|
+
- `ScenarioAsymmetry`: upside/downside distribution quality
|
|
82
|
+
- `CatalystDensity`: number and quality of near-term unlocks
|
|
83
|
+
- `ExpectationMispricing`: mismatch between consensus and thesis path
|
|
84
|
+
|
|
85
|
+
## Composite View (Optional)
|
|
86
|
+
|
|
87
|
+
Some outputs use an optional composite:
|
|
88
|
+
|
|
89
|
+
`Composite = a*Momentum + b*Stability + c*FinancialHealth + d*Upside`
|
|
90
|
+
|
|
91
|
+
Weights are intentionally configurable by sector/business model in production.
|
|
92
|
+
|
|
93
|
+
## Confidence Annotation
|
|
94
|
+
|
|
95
|
+
Each score can include a confidence tag based on evidence depth:
|
|
96
|
+
|
|
97
|
+
- `High`: robust multi-source evidence, low internal contradiction
|
|
98
|
+
- `Medium`: adequate evidence, some assumptions open
|
|
99
|
+
- `Low`: sparse data or unresolved contradictions
|
|
100
|
+
|
|
101
|
+
## Kill Criteria Coupling
|
|
102
|
+
|
|
103
|
+
Scores are never final without kill criteria.
|
|
104
|
+
|
|
105
|
+
If a listed kill criterion triggers, the thesis should be re-underwritten regardless of score level.
|
|
106
|
+
|
|
107
|
+
## Not Included in Public Docs
|
|
108
|
+
|
|
109
|
+
- Production weight values (`w1..w4`, `a..d`)
|
|
110
|
+
- Threshold cutoffs and regime-specific overrides
|
|
111
|
+
- Internal fallback logic for sparse/contradictory data
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from tools import edgar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EdgarTests(unittest.TestCase):
|
|
9
|
+
def test_shares_outstanding_does_not_include_weighted_average_concepts(self) -> None:
|
|
10
|
+
concepts = edgar._FIELD_CONCEPTS["balance_sheet"]["shares_outstanding"]
|
|
11
|
+
self.assertNotIn("WeightedAverageNumberOfDilutedSharesOutstanding", concepts)
|
|
12
|
+
self.assertNotIn("WeightedAverageShares", concepts)
|
|
13
|
+
|
|
14
|
+
def test_best_entry_uses_concept_priority_before_recency(self) -> None:
|
|
15
|
+
records = [
|
|
16
|
+
{
|
|
17
|
+
"concept": "Revenue",
|
|
18
|
+
"unit": "USD",
|
|
19
|
+
"form": "10-K",
|
|
20
|
+
"period_end": "2026-12-31",
|
|
21
|
+
"filed": "2027-02-01",
|
|
22
|
+
"period_months": 12,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"concept": "Revenues",
|
|
26
|
+
"unit": "USD",
|
|
27
|
+
"form": "10-K",
|
|
28
|
+
"period_end": "2025-12-31",
|
|
29
|
+
"filed": "2026-02-01",
|
|
30
|
+
"period_months": 12,
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
best = edgar._best_entry(
|
|
34
|
+
records,
|
|
35
|
+
quarterly=False,
|
|
36
|
+
statement="income_statement",
|
|
37
|
+
field="revenue",
|
|
38
|
+
)
|
|
39
|
+
self.assertIsNotNone(best)
|
|
40
|
+
assert best is not None
|
|
41
|
+
self.assertEqual(best["concept"], "Revenues")
|
|
42
|
+
|
|
43
|
+
def test_request_json_retries_then_succeeds(self) -> None:
|
|
44
|
+
class FakeResponse:
|
|
45
|
+
def __init__(self, status_code: int, payload: Optional[dict] = None) -> None:
|
|
46
|
+
self.status_code = status_code
|
|
47
|
+
self._payload = payload or {}
|
|
48
|
+
|
|
49
|
+
def raise_for_status(self) -> None:
|
|
50
|
+
if self.status_code >= 400:
|
|
51
|
+
raise edgar.requests.HTTPError(response=self)
|
|
52
|
+
|
|
53
|
+
def json(self) -> dict:
|
|
54
|
+
return self._payload
|
|
55
|
+
|
|
56
|
+
session = Mock()
|
|
57
|
+
session.get.side_effect = [
|
|
58
|
+
FakeResponse(503),
|
|
59
|
+
FakeResponse(200, {"ok": True}),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
with patch("tools.edgar.time.sleep") as sleep_mock:
|
|
63
|
+
data = edgar._request_json("https://example.com", session)
|
|
64
|
+
|
|
65
|
+
self.assertEqual(data, {"ok": True})
|
|
66
|
+
self.assertEqual(session.get.call_count, 2)
|
|
67
|
+
sleep_mock.assert_called_once()
|
|
68
|
+
|
|
69
|
+
def test_request_json_raises_after_max_retries(self) -> None:
|
|
70
|
+
class FakeResponse:
|
|
71
|
+
def __init__(self, status_code: int) -> None:
|
|
72
|
+
self.status_code = status_code
|
|
73
|
+
|
|
74
|
+
def raise_for_status(self) -> None:
|
|
75
|
+
raise edgar.requests.HTTPError(response=self)
|
|
76
|
+
|
|
77
|
+
def json(self) -> dict:
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
session = Mock()
|
|
81
|
+
session.get.return_value = FakeResponse(503)
|
|
82
|
+
|
|
83
|
+
with patch("tools.edgar.time.sleep"):
|
|
84
|
+
with self.assertRaises(edgar.requests.HTTPError):
|
|
85
|
+
edgar._request_json("https://example.com", session)
|
|
86
|
+
self.assertEqual(session.get.call_count, edgar._MAX_RETRIES)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
unittest.main()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from tools import market
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MarketTests(unittest.TestCase):
|
|
9
|
+
def test_get_ratios_short_circuits_after_first_provider_with_ratios(self) -> None:
|
|
10
|
+
calls: list[str] = []
|
|
11
|
+
|
|
12
|
+
def yahoo(_ticker: str):
|
|
13
|
+
calls.append("yahoo")
|
|
14
|
+
return {
|
|
15
|
+
"provider": "yahoo",
|
|
16
|
+
"price": 100.0,
|
|
17
|
+
"pe": 25.0,
|
|
18
|
+
"dividend_yield_pct": 1.2,
|
|
19
|
+
"beta": 1.1,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def finviz(_ticker: str):
|
|
23
|
+
calls.append("finviz")
|
|
24
|
+
return {
|
|
25
|
+
"provider": "finviz",
|
|
26
|
+
"price": 100.0,
|
|
27
|
+
"pe": 18.0,
|
|
28
|
+
"dividend_yield_pct": 2.0,
|
|
29
|
+
"beta": 0.9,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def stooq(_ticker: str):
|
|
33
|
+
calls.append("stooq")
|
|
34
|
+
return {
|
|
35
|
+
"provider": "stooq",
|
|
36
|
+
"price": 100.0,
|
|
37
|
+
"pe": None,
|
|
38
|
+
"dividend_yield_pct": None,
|
|
39
|
+
"beta": None,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
with patch("tools.market._fetch_yahoo", yahoo), patch(
|
|
43
|
+
"tools.market._fetch_finviz", finviz
|
|
44
|
+
), patch("tools.market._fetch_stooq", stooq):
|
|
45
|
+
result = market.get_ratios("AAPL")
|
|
46
|
+
|
|
47
|
+
self.assertEqual(result["provider"], "yahoo")
|
|
48
|
+
self.assertEqual(calls, ["yahoo"])
|
|
49
|
+
|
|
50
|
+
def test_get_ratios_uses_second_provider_when_first_has_no_ratios(self) -> None:
|
|
51
|
+
calls: list[str] = []
|
|
52
|
+
|
|
53
|
+
def yahoo(_ticker: str):
|
|
54
|
+
calls.append("yahoo")
|
|
55
|
+
return {
|
|
56
|
+
"provider": "yahoo",
|
|
57
|
+
"price": 100.0,
|
|
58
|
+
"pe": None,
|
|
59
|
+
"dividend_yield_pct": None,
|
|
60
|
+
"beta": None,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def finviz(_ticker: str):
|
|
64
|
+
calls.append("finviz")
|
|
65
|
+
return {
|
|
66
|
+
"provider": "finviz",
|
|
67
|
+
"price": 100.0,
|
|
68
|
+
"pe": 18.0,
|
|
69
|
+
"dividend_yield_pct": 2.0,
|
|
70
|
+
"beta": 0.9,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def stooq(_ticker: str):
|
|
74
|
+
calls.append("stooq")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
with patch("tools.market._fetch_yahoo", yahoo), patch(
|
|
78
|
+
"tools.market._fetch_finviz", finviz
|
|
79
|
+
), patch("tools.market._fetch_stooq", stooq):
|
|
80
|
+
result = market.get_ratios("AAPL")
|
|
81
|
+
|
|
82
|
+
self.assertEqual(result["provider"], "finviz")
|
|
83
|
+
self.assertEqual(calls, ["yahoo", "finviz"])
|
|
84
|
+
|
|
85
|
+
def test_http_get_json_retries_then_succeeds(self) -> None:
|
|
86
|
+
class FakeResponse:
|
|
87
|
+
def __init__(self, status_code: int, payload: Optional[dict] = None) -> None:
|
|
88
|
+
self.status_code = status_code
|
|
89
|
+
self._payload = payload or {}
|
|
90
|
+
|
|
91
|
+
def raise_for_status(self) -> None:
|
|
92
|
+
if self.status_code >= 400:
|
|
93
|
+
raise market.requests.HTTPError(response=self)
|
|
94
|
+
|
|
95
|
+
def json(self) -> dict:
|
|
96
|
+
return self._payload
|
|
97
|
+
|
|
98
|
+
with patch("tools.market.requests.get") as get_mock, patch(
|
|
99
|
+
"tools.market.time.sleep"
|
|
100
|
+
) as sleep_mock:
|
|
101
|
+
get_mock.side_effect = [
|
|
102
|
+
FakeResponse(503),
|
|
103
|
+
FakeResponse(200, {"ok": True}),
|
|
104
|
+
]
|
|
105
|
+
payload = market._http_get_json("https://example.com")
|
|
106
|
+
|
|
107
|
+
self.assertEqual(payload, {"ok": True})
|
|
108
|
+
self.assertEqual(get_mock.call_count, 2)
|
|
109
|
+
sleep_mock.assert_called_once()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
unittest.main()
|