test(agent): remove per-capability test boilerplate
Browse filesAdding a capability meant editing five duplicated _FakeService classes
and an exact 30-tuple registry assertion. similarity_search shipped
broken this way and was repaired five days later.
The stubs now live in tests/fakes.py, where an unknown capability
resolves through __getattr__, so a new capability needs no test edit.
Registry-derived parity tests replace the ordered tuple and turn a
missing tool contract, missing parity route, or mismatched
response_model into one named failure instead of a cascade.
Because __getattr__ also hides a typo'd handler from every stub-backed
test, one check builds the registry against the real service.
tests/ goes on sys.path via pythonpath instead of pytest's implicit
conftest insertion, which breaks under --import-mode=importlib, if
tests/__init__.py is added, or once a nested conftest.py shadows the
name.
Also repairs tests/agent/test_session_store_light_metadata.py, which
was silently uncollectable: it imported tests.agent.test_hosted_runtime,
and tests/agent is not an importable package.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- pyproject.toml +8 -0
- tests/agent/test_capability_parity.py +177 -0
- tests/agent/test_hosted_runtime.py +2 -194
- tests/agent/test_loop.py +3 -182
- tests/agent/test_runtime.py +36 -193
- tests/agent/test_session_store_light_metadata.py +6 -5
- tests/agent/test_tools.py +4 -213
- tests/fakes.py +228 -0
|
@@ -92,6 +92,14 @@ packages = {find = {where = ["src"], namespaces = false}}
|
|
| 92 |
[tool.setuptools.package-data]
|
| 93 |
"*" = ["*.json"]
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
[tool.ruff]
|
| 96 |
line-length = 119
|
| 97 |
|
|
|
|
| 92 |
[tool.setuptools.package-data]
|
| 93 |
"*" = ["*.json"]
|
| 94 |
|
| 95 |
+
[tool.pytest.ini_options]
|
| 96 |
+
# Put `tests/` on sys.path so shared test doubles live in an ordinary module
|
| 97 |
+
# (`tests/fakes.py`) that every test package can import. Relying on pytest's
|
| 98 |
+
# implicit rootdir insertion instead would break under
|
| 99 |
+
# `--import-mode=importlib`, if `tests/__init__.py` were added, or as soon as a
|
| 100 |
+
# nested `conftest.py` shadowed the name.
|
| 101 |
+
pythonpath = ["tests"]
|
| 102 |
+
|
| 103 |
[tool.ruff]
|
| 104 |
line-length = 119
|
| 105 |
|
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Parity checks between the capability registry and its downstream surfaces.
|
| 2 |
+
|
| 3 |
+
`src/TerraFin/agent/runtime/capability.py` is the single source of truth for the
|
| 4 |
+
agent capability surface. Three things must stay in step with it, and each one
|
| 5 |
+
otherwise fails only at runtime (or silently, in generated docs):
|
| 6 |
+
|
| 7 |
+
* every capability needs a `HOSTED_TOOL_CONTRACTS` entry, because the tool
|
| 8 |
+
adapter looks one up for each registered capability when listing tools
|
| 9 |
+
* every capability that declares an `http_route_path` needs that route to exist,
|
| 10 |
+
since HTTP-only agents depend on the parity route
|
| 11 |
+
* declared metadata (`summary`, `response_model_name`) must be populated and the
|
| 12 |
+
response model must actually exist somewhere in the package
|
| 13 |
+
|
| 14 |
+
These tests are deliberately derived from the registry rather than hardcoded, so
|
| 15 |
+
adding a capability requires no edit here.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import ast
|
| 19 |
+
import pathlib
|
| 20 |
+
|
| 21 |
+
import pytest
|
| 22 |
+
from fakes import BaseFakeService, fake_chart_opener
|
| 23 |
+
|
| 24 |
+
from TerraFin.agent.contracts.tool_contracts import HOSTED_TOOL_CONTRACTS
|
| 25 |
+
from TerraFin.agent.runtime import build_default_capability_registry
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Hosted-only tools live outside the capability registry: the three guru
|
| 29 |
+
# consults and `current_view_context` need a live session, so they are wired
|
| 30 |
+
# straight into the tool adapter instead.
|
| 31 |
+
HOSTED_ONLY_TOOLS = frozenset(
|
| 32 |
+
{
|
| 33 |
+
"consult_warren_buffett",
|
| 34 |
+
"consult_howard_marks",
|
| 35 |
+
"consult_stanley_druckenmiller",
|
| 36 |
+
"current_view_context",
|
| 37 |
+
}
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# `open_chart` mutates hosted session state and has no stateless HTTP surface.
|
| 41 |
+
CAPABILITIES_WITHOUT_ROUTE = frozenset({"open_chart"})
|
| 42 |
+
|
| 43 |
+
# `open_chart` is handed the injected `chart_opener` callable rather than a
|
| 44 |
+
# `TerraFinAgentService` method, so its handler name never matches the
|
| 45 |
+
# capability name.
|
| 46 |
+
CAPABILITIES_WITH_INJECTED_HANDLER = frozenset({"open_chart"})
|
| 47 |
+
|
| 48 |
+
_SRC_ROOT = pathlib.Path(__file__).resolve().parents[2] / "src" / "TerraFin"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@pytest.fixture(scope="module")
|
| 52 |
+
def capabilities():
|
| 53 |
+
registry = build_default_capability_registry(BaseFakeService(), chart_opener=fake_chart_opener)
|
| 54 |
+
return registry.list()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@pytest.fixture(scope="module")
|
| 58 |
+
def live_route_paths() -> set[str | None]:
|
| 59 |
+
"""Paths served by the assembled app.
|
| 60 |
+
|
| 61 |
+
Module-scoped because `create_app()` is not side-effect free: it loads the
|
| 62 |
+
repo `.env` into `os.environ`, resets chart/calendar module state, and
|
| 63 |
+
builds process-wide singletons. Build it once here rather than per test.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
from TerraFin.interface.server import create_app
|
| 67 |
+
|
| 68 |
+
return {getattr(route, "path", None) for route in create_app().routes}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _declared_class_names() -> set[str]:
|
| 72 |
+
"""Collect every class name defined under src/TerraFin without importing it."""
|
| 73 |
+
|
| 74 |
+
names: set[str] = set()
|
| 75 |
+
for path in _SRC_ROOT.rglob("*.py"):
|
| 76 |
+
try:
|
| 77 |
+
tree = ast.parse(path.read_text(encoding="utf-8"))
|
| 78 |
+
except (SyntaxError, UnicodeDecodeError): # pragma: no cover - defensive
|
| 79 |
+
continue
|
| 80 |
+
names.update(node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef))
|
| 81 |
+
return names
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def test_every_capability_has_a_tool_contract(capabilities) -> None:
|
| 85 |
+
missing = sorted(c.name for c in capabilities if c.name not in HOSTED_TOOL_CONTRACTS)
|
| 86 |
+
assert not missing, (
|
| 87 |
+
"capabilities registered without a HOSTED_TOOL_CONTRACTS entry "
|
| 88 |
+
f"(tool listing would raise KeyError): {missing}"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def test_every_tool_contract_is_registered_or_hosted_only(capabilities) -> None:
|
| 93 |
+
registered = {c.name for c in capabilities}
|
| 94 |
+
orphans = sorted(set(HOSTED_TOOL_CONTRACTS) - registered - HOSTED_ONLY_TOOLS)
|
| 95 |
+
assert not orphans, f"tool contracts with no registered capability and no hosted-only exemption: {orphans}"
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def test_every_capability_declares_generator_metadata(capabilities) -> None:
|
| 99 |
+
missing_summary = sorted(c.name for c in capabilities if not c.summary)
|
| 100 |
+
assert not missing_summary, f"capabilities missing `summary`: {missing_summary}"
|
| 101 |
+
|
| 102 |
+
missing_model = sorted(c.name for c in capabilities if not c.response_model_name)
|
| 103 |
+
assert not missing_model, f"capabilities missing `response_model_name`: {missing_model}"
|
| 104 |
+
|
| 105 |
+
missing_route_path = sorted(
|
| 106 |
+
c.name for c in capabilities if not c.http_route_path and c.name not in CAPABILITIES_WITHOUT_ROUTE
|
| 107 |
+
)
|
| 108 |
+
assert not missing_route_path, f"capabilities missing `http_route_path`: {missing_route_path}"
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def test_declared_response_models_exist(capabilities) -> None:
|
| 112 |
+
"""Weak existence check: the name matches *some* class under src/TerraFin.
|
| 113 |
+
|
| 114 |
+
Response models are spread across agent contracts, data contracts, private
|
| 115 |
+
provider models, and page route modules, so this only catches an outright
|
| 116 |
+
typo. `test_response_model_names_match_tool_contracts` is the strict check.
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
declared = _declared_class_names()
|
| 120 |
+
unresolved = sorted(
|
| 121 |
+
f"{c.name} -> {c.response_model_name}"
|
| 122 |
+
for c in capabilities
|
| 123 |
+
if c.response_model_name and c.response_model_name not in declared
|
| 124 |
+
)
|
| 125 |
+
assert not unresolved, f"`response_model_name` values with no matching class under src/TerraFin: {unresolved}"
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def test_response_model_names_match_tool_contracts(capabilities) -> None:
|
| 129 |
+
"""The registry and the tool contract declare the same response model.
|
| 130 |
+
|
| 131 |
+
`HOSTED_TOOL_CONTRACTS[name]["response_model"]` is shipped to the model as
|
| 132 |
+
tool metadata, so a disagreement between the two declarations sends the LLM
|
| 133 |
+
a response shape that does not match the route's.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
mismatches = sorted(
|
| 137 |
+
f"{c.name}: registry={c.response_model_name!r} contract={HOSTED_TOOL_CONTRACTS[c.name].get('response_model')!r}"
|
| 138 |
+
for c in capabilities
|
| 139 |
+
if c.name in HOSTED_TOOL_CONTRACTS
|
| 140 |
+
and HOSTED_TOOL_CONTRACTS[c.name].get("response_model") != c.response_model_name
|
| 141 |
+
)
|
| 142 |
+
assert not mismatches, f"registry / tool-contract response_model disagreement: {mismatches}"
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_every_capability_handler_binds_on_the_real_service() -> None:
|
| 146 |
+
"""Guard the one thing the shared stub cannot catch.
|
| 147 |
+
|
| 148 |
+
`BaseFakeService.__getattr__` resolves any attribute, so a capability whose
|
| 149 |
+
handler names a method the real `TerraFinAgentService` does not implement
|
| 150 |
+
(a typo, or a handler added before its service method) passes every
|
| 151 |
+
stub-backed test. Building the registry against the real service is the
|
| 152 |
+
check that fails loudly, and it is cheap: no network, no env mutation.
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
from TerraFin.agent.service import TerraFinAgentService
|
| 156 |
+
|
| 157 |
+
registry = build_default_capability_registry(TerraFinAgentService(), chart_opener=fake_chart_opener)
|
| 158 |
+
|
| 159 |
+
mislabelled = sorted(
|
| 160 |
+
f"{c.name} -> {getattr(c.handler, '__name__', repr(c.handler))}"
|
| 161 |
+
for c in registry.list()
|
| 162 |
+
if c.name not in CAPABILITIES_WITH_INJECTED_HANDLER
|
| 163 |
+
and getattr(c.handler, "__name__", None) not in (c.name, None)
|
| 164 |
+
)
|
| 165 |
+
assert not mislabelled, (
|
| 166 |
+
"capability handlers bound to a differently-named service method "
|
| 167 |
+
f"(likely a copy-paste error): {mislabelled}"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def test_declared_http_routes_exist(capabilities, live_route_paths) -> None:
|
| 172 |
+
missing = sorted(
|
| 173 |
+
f"{c.name} -> {c.http_route_path}"
|
| 174 |
+
for c in capabilities
|
| 175 |
+
if c.http_route_path and c.http_route_path not in live_route_paths
|
| 176 |
+
)
|
| 177 |
+
assert not missing, f"capabilities whose declared http_route_path has no live route: {missing}"
|
|
@@ -2,6 +2,8 @@ import time
|
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
import pytest
|
|
|
|
|
|
|
| 5 |
|
| 6 |
from TerraFin.agent.definitions import (
|
| 7 |
DEFAULT_HOSTED_AGENT_NAME,
|
|
@@ -21,200 +23,6 @@ from TerraFin.agent.session_store import SQLiteHostedSessionStore
|
|
| 21 |
from TerraFin.agent.transcript_store import HostedTranscriptStore
|
| 22 |
|
| 23 |
|
| 24 |
-
def _processing() -> dict[str, object]:
|
| 25 |
-
return {
|
| 26 |
-
"requestedDepth": "auto",
|
| 27 |
-
"resolvedDepth": "full",
|
| 28 |
-
"loadedStart": "2024-01-01",
|
| 29 |
-
"loadedEnd": "2024-12-31",
|
| 30 |
-
"isComplete": True,
|
| 31 |
-
"hasOlder": False,
|
| 32 |
-
"sourceVersion": "test-source",
|
| 33 |
-
"view": "daily",
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
class _FakeService:
|
| 38 |
-
def resolve(self, query: str) -> dict[str, object]:
|
| 39 |
-
return {"type": "stock", "name": query.upper(), "path": f"/stock/{query.upper()}", "processing": _processing()}
|
| 40 |
-
|
| 41 |
-
def market_data(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 42 |
-
return {"ticker": name, "seriesType": "candlestick", "count": 1, "data": [], "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 43 |
-
|
| 44 |
-
def indicators(
|
| 45 |
-
self,
|
| 46 |
-
name: str,
|
| 47 |
-
indicators: str,
|
| 48 |
-
*,
|
| 49 |
-
depth: str = "auto",
|
| 50 |
-
view: str = "daily",
|
| 51 |
-
) -> dict[str, object]:
|
| 52 |
-
return {
|
| 53 |
-
"ticker": name,
|
| 54 |
-
"indicators": {"rsi": {"name": "rsi", "offset": 0, "values": {"value": 55.0}}},
|
| 55 |
-
"unknown": [],
|
| 56 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view, "indicatorQuery": indicators},
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
def patterns(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 60 |
-
return {"ticker": name, "signals": [], "total": 0, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 61 |
-
|
| 62 |
-
def market_snapshot(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 63 |
-
return {
|
| 64 |
-
"ticker": name,
|
| 65 |
-
"price_action": {"current": 100.0},
|
| 66 |
-
"indicators": {"rsi": 55.0},
|
| 67 |
-
"market_breadth": [],
|
| 68 |
-
"watchlist": [],
|
| 69 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
def lppl_analysis(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 73 |
-
return {"name": name, "confidence": 0.2, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 74 |
-
|
| 75 |
-
def company_info(self, ticker: str) -> dict[str, object]:
|
| 76 |
-
return {"ticker": ticker, "shortName": f"{ticker} Corp", "processing": _processing()}
|
| 77 |
-
|
| 78 |
-
def earnings(self, ticker: str) -> dict[str, object]:
|
| 79 |
-
return {"ticker": ticker, "earnings": [], "processing": _processing()}
|
| 80 |
-
|
| 81 |
-
def financials(self, ticker: str, *, statement: str = "income", period: str = "annual") -> dict[str, object]:
|
| 82 |
-
return {"ticker": ticker, "statement": statement, "period": period, "columns": [], "rows": [], "processing": _processing()}
|
| 83 |
-
|
| 84 |
-
def portfolio(self, guru: str) -> dict[str, object]:
|
| 85 |
-
return {"guru": guru, "info": {}, "holdings": [], "count": 0, "processing": _processing()}
|
| 86 |
-
|
| 87 |
-
def economic(self, indicators: str) -> dict[str, object]:
|
| 88 |
-
return {"indicators": {indicators: {"latest_value": 3.0}}, "processing": _processing()}
|
| 89 |
-
|
| 90 |
-
def macro_focus(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 91 |
-
return {
|
| 92 |
-
"name": name,
|
| 93 |
-
"info": {"name": name, "type": "index", "description": "Macro", "currentValue": 1.0, "change": 0.0, "changePercent": 0.0},
|
| 94 |
-
"seriesType": "line",
|
| 95 |
-
"count": 1,
|
| 96 |
-
"data": [],
|
| 97 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
def calendar_events(
|
| 101 |
-
self,
|
| 102 |
-
*,
|
| 103 |
-
year: int,
|
| 104 |
-
month: int,
|
| 105 |
-
categories: str | None = None,
|
| 106 |
-
limit: int | None = None,
|
| 107 |
-
) -> dict[str, object]:
|
| 108 |
-
return {"events": [], "count": 0, "month": month, "year": year, "categories": categories, "limit": limit, "processing": _processing()}
|
| 109 |
-
|
| 110 |
-
def fundamental_screen(self, ticker: str) -> dict[str, object]:
|
| 111 |
-
return {
|
| 112 |
-
"ticker": ticker,
|
| 113 |
-
"moat": {"score": "wide"},
|
| 114 |
-
"earnings_quality": {},
|
| 115 |
-
"balance_sheet": {},
|
| 116 |
-
"capital_allocation": {},
|
| 117 |
-
"pricing_power": {},
|
| 118 |
-
"warnings": [],
|
| 119 |
-
"processing": _processing(),
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
def risk_profile(self, name: str, *, depth: str = "auto") -> dict[str, object]:
|
| 123 |
-
return {
|
| 124 |
-
"ticker": name,
|
| 125 |
-
"tail_risk": {},
|
| 126 |
-
"convexity": {},
|
| 127 |
-
"volatility": {"requestedDepth": depth},
|
| 128 |
-
"drawdown": {},
|
| 129 |
-
"warnings": [],
|
| 130 |
-
"processing": _processing(),
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
def valuation(self, ticker: str) -> dict[str, object]:
|
| 134 |
-
return {
|
| 135 |
-
"ticker": ticker,
|
| 136 |
-
"dcf": {"status": "ready", "intrinsic_value": 120.0},
|
| 137 |
-
"reverse_dcf": {"status": "ready", "implied_growth_pct": 8.0},
|
| 138 |
-
"relative": {"trailing_pe": 22.0},
|
| 139 |
-
"graham_number": 100.0,
|
| 140 |
-
"margin_of_safety_pct": 12.0,
|
| 141 |
-
"current_price": 107.0,
|
| 142 |
-
"processing": _processing(),
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
def sec_filings(self, ticker: str) -> dict[str, object]:
|
| 146 |
-
return {"ticker": ticker, "cik": 1, "forms": [], "filings": [], "processing": _processing()}
|
| 147 |
-
|
| 148 |
-
def sec_filing_document(
|
| 149 |
-
self, ticker: str, accession: str, primaryDocument: str, *, form: str = "10-Q"
|
| 150 |
-
) -> dict[str, object]:
|
| 151 |
-
return {"ticker": ticker, "accession": accession, "primaryDocument": primaryDocument, "toc": [], "charCount": 0, "indexUrl": "", "documentUrl": "", "processing": _processing()}
|
| 152 |
-
|
| 153 |
-
def sec_filing_section(
|
| 154 |
-
self, ticker: str, accession: str, primaryDocument: str, sectionSlug: str, *, form: str = "10-Q"
|
| 155 |
-
) -> dict[str, object]:
|
| 156 |
-
return {"ticker": ticker, "accession": accession, "sectionSlug": sectionSlug, "sectionTitle": "stub", "markdown": "", "charCount": 0, "documentUrl": "", "processing": _processing()}
|
| 157 |
-
|
| 158 |
-
def fcf_history(self, ticker: str, years: int = 10) -> dict[str, object]:
|
| 159 |
-
return {
|
| 160 |
-
"ticker": ticker,
|
| 161 |
-
"years": years,
|
| 162 |
-
"rows": [],
|
| 163 |
-
"candidates": {"threeYearAvg": None, "latestAnnual": None, "ttm": None},
|
| 164 |
-
"autoSelectedSource": "annual",
|
| 165 |
-
"processing": _processing(),
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
def similarity_search(
|
| 169 |
-
self,
|
| 170 |
-
ticker: str,
|
| 171 |
-
universe: str = "sp500+nasdaq100+kospi200",
|
| 172 |
-
period: str = "1y",
|
| 173 |
-
top_n: int = 20,
|
| 174 |
-
) -> dict[str, object]:
|
| 175 |
-
return {"ticker": ticker, "period": period, "pool": {}, "results": [], "count": 0, "processing": _processing()}
|
| 176 |
-
|
| 177 |
-
def fear_greed(self) -> dict[str, object]:
|
| 178 |
-
return {"score": 50, "rating": "Neutral", "processing": _processing()}
|
| 179 |
-
|
| 180 |
-
def sp500_dcf(self) -> dict[str, object]:
|
| 181 |
-
return {"status": "ready", "currentIntrinsicValue": 5000.0, "processing": _processing()}
|
| 182 |
-
|
| 183 |
-
def beta_estimate(self, ticker: str) -> dict[str, object]:
|
| 184 |
-
return {"symbol": ticker, "beta": 1.0, "adjustedBeta": 1.0, "rSquared": 0.5, "processing": _processing()}
|
| 185 |
-
|
| 186 |
-
def top_companies(self) -> dict[str, object]:
|
| 187 |
-
return {"companies": [], "count": 0, "processing": _processing()}
|
| 188 |
-
|
| 189 |
-
def market_regime(self) -> dict[str, object]:
|
| 190 |
-
return {"summary": "stub", "confidence": "low", "signals": [], "processing": _processing()}
|
| 191 |
-
|
| 192 |
-
def trailing_forward_pe(self) -> dict[str, object]:
|
| 193 |
-
return {"date": "2026-04-01", "latestValue": 0.0, "history": [], "processing": _processing()}
|
| 194 |
-
|
| 195 |
-
def market_breadth(self) -> dict[str, object]:
|
| 196 |
-
return {"metrics": [], "processing": _processing()}
|
| 197 |
-
|
| 198 |
-
def watchlist(self) -> dict[str, object]:
|
| 199 |
-
return {"items": [], "count": 0, "processing": _processing()}
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def _fake_chart_opener(
|
| 203 |
-
data_or_names,
|
| 204 |
-
*,
|
| 205 |
-
session_id: str | None = None,
|
| 206 |
-
**kwargs,
|
| 207 |
-
) -> dict[str, object]:
|
| 208 |
-
_ = kwargs
|
| 209 |
-
return {
|
| 210 |
-
"ok": True,
|
| 211 |
-
"sessionId": session_id or "agent:chart",
|
| 212 |
-
"chartUrl": f"http://127.0.0.1:8001/chart?sessionId={session_id or 'agent:chart'}",
|
| 213 |
-
"processing": _processing(),
|
| 214 |
-
"inputEcho": data_or_names,
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
|
| 218 |
def _runtime(
|
| 219 |
agent_registry: TerraFinAgentDefinitionRegistry | None = None,
|
| 220 |
*,
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
import pytest
|
| 5 |
+
from fakes import BaseFakeService as _FakeService
|
| 6 |
+
from fakes import fake_chart_opener as _fake_chart_opener
|
| 7 |
|
| 8 |
from TerraFin.agent.definitions import (
|
| 9 |
DEFAULT_HOSTED_AGENT_NAME,
|
|
|
|
| 23 |
from TerraFin.agent.transcript_store import HostedTranscriptStore
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def _runtime(
|
| 27 |
agent_registry: TerraFinAgentDefinitionRegistry | None = None,
|
| 28 |
*,
|
|
@@ -1,12 +1,14 @@
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
import pytest
|
|
|
|
|
|
|
| 4 |
|
|
|
|
| 5 |
from TerraFin.agent.definitions import (
|
| 6 |
DEFAULT_HOSTED_AGENT_NAME,
|
| 7 |
build_default_agent_definition_registry,
|
| 8 |
)
|
| 9 |
-
from TerraFin.agent.conversation import is_internal_only_message
|
| 10 |
from TerraFin.agent.guru import (
|
| 11 |
GuruResearchMemo,
|
| 12 |
GuruRoutePlan,
|
|
@@ -29,193 +31,12 @@ from TerraFin.agent.session_store import SQLiteHostedSessionStore
|
|
| 29 |
from TerraFin.agent.transcript_store import HostedTranscriptStore
|
| 30 |
|
| 31 |
|
| 32 |
-
def _processing() -> dict[str, object]:
|
| 33 |
-
return {
|
| 34 |
-
"requestedDepth": "auto",
|
| 35 |
-
"resolvedDepth": "full",
|
| 36 |
-
"loadedStart": "2024-01-01",
|
| 37 |
-
"loadedEnd": "2024-12-31",
|
| 38 |
-
"isComplete": True,
|
| 39 |
-
"hasOlder": False,
|
| 40 |
-
"sourceVersion": "test-source",
|
| 41 |
-
"view": "daily",
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
|
| 45 |
def _public_roles(messages):
|
| 46 |
return [message.role for message in messages if not is_internal_only_message(message)]
|
| 47 |
|
| 48 |
|
| 49 |
-
class _FakeService:
|
| 50 |
-
def resolve(self, query: str) -> dict[str, object]:
|
| 51 |
-
return {"type": "stock", "name": query.upper(), "path": f"/stock/{query.upper()}", "processing": _processing()}
|
| 52 |
-
|
| 53 |
-
def market_data(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 54 |
-
return {"ticker": name, "seriesType": "candlestick", "count": 1, "data": [], "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 55 |
-
|
| 56 |
-
def patterns(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 57 |
-
return {"ticker": name, "signals": [], "total": 0, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 58 |
-
|
| 59 |
-
def fcf_history(self, ticker: str, years: int = 10) -> dict[str, object]:
|
| 60 |
-
return {
|
| 61 |
-
"ticker": ticker, "years": years, "rows": [],
|
| 62 |
-
"candidates": {"threeYearAvg": None, "latestAnnual": None, "ttm": None},
|
| 63 |
-
"autoSelectedSource": "annual", "processing": _processing(),
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
def similarity_search(self, ticker: str, universe: str = "sp500+nasdaq100+kospi200", period: str = "1y", top_n: int = 20) -> dict[str, object]:
|
| 67 |
-
return {"ticker": ticker, "period": period, "pool": {}, "results": [], "count": 0, "processing": _processing()}
|
| 68 |
-
|
| 69 |
-
def indicators(
|
| 70 |
-
self,
|
| 71 |
-
name: str,
|
| 72 |
-
indicators: str,
|
| 73 |
-
*,
|
| 74 |
-
depth: str = "auto",
|
| 75 |
-
view: str = "daily",
|
| 76 |
-
) -> dict[str, object]:
|
| 77 |
-
return {
|
| 78 |
-
"ticker": name,
|
| 79 |
-
"indicators": {"rsi": {"name": "rsi", "offset": 0, "values": {"value": 55.0}}},
|
| 80 |
-
"unknown": [],
|
| 81 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view, "indicatorQuery": indicators},
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
def market_snapshot(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 85 |
-
return {
|
| 86 |
-
"ticker": name,
|
| 87 |
-
"price_action": {"current": 100.0},
|
| 88 |
-
"indicators": {"rsi": 55.0},
|
| 89 |
-
"market_breadth": [],
|
| 90 |
-
"watchlist": [],
|
| 91 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
def lppl_analysis(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 95 |
-
return {"name": name, "confidence": 0.2, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 96 |
-
|
| 97 |
-
def company_info(self, ticker: str) -> dict[str, object]:
|
| 98 |
-
return {"ticker": ticker, "shortName": f"{ticker} Corp", "processing": _processing()}
|
| 99 |
-
|
| 100 |
-
def earnings(self, ticker: str) -> dict[str, object]:
|
| 101 |
-
return {"ticker": ticker, "earnings": [], "processing": _processing()}
|
| 102 |
-
|
| 103 |
-
def financials(self, ticker: str, *, statement: str = "income", period: str = "annual") -> dict[str, object]:
|
| 104 |
-
return {"ticker": ticker, "statement": statement, "period": period, "columns": [], "rows": [], "processing": _processing()}
|
| 105 |
-
|
| 106 |
-
def portfolio(self, guru: str) -> dict[str, object]:
|
| 107 |
-
return {"guru": guru, "info": {}, "holdings": [], "count": 0, "processing": _processing()}
|
| 108 |
-
|
| 109 |
-
def economic(self, indicators: str) -> dict[str, object]:
|
| 110 |
-
return {"indicators": {indicators: {"latest_value": 3.0}}, "processing": _processing()}
|
| 111 |
|
| 112 |
-
def macro_focus(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 113 |
-
return {
|
| 114 |
-
"name": name,
|
| 115 |
-
"info": {"name": name, "type": "index", "description": "Macro", "currentValue": 1.0, "change": 0.0, "changePercent": 0.0},
|
| 116 |
-
"seriesType": "line",
|
| 117 |
-
"count": 1,
|
| 118 |
-
"data": [],
|
| 119 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
def calendar_events(
|
| 123 |
-
self,
|
| 124 |
-
*,
|
| 125 |
-
year: int,
|
| 126 |
-
month: int,
|
| 127 |
-
categories: str | None = None,
|
| 128 |
-
limit: int | None = None,
|
| 129 |
-
) -> dict[str, object]:
|
| 130 |
-
return {"events": [], "count": 0, "month": month, "year": year, "categories": categories, "limit": limit, "processing": _processing()}
|
| 131 |
-
|
| 132 |
-
def fundamental_screen(self, ticker: str) -> dict[str, object]:
|
| 133 |
-
return {
|
| 134 |
-
"ticker": ticker,
|
| 135 |
-
"moat": {"score": "wide"},
|
| 136 |
-
"earnings_quality": {},
|
| 137 |
-
"balance_sheet": {},
|
| 138 |
-
"capital_allocation": {},
|
| 139 |
-
"pricing_power": {},
|
| 140 |
-
"warnings": [],
|
| 141 |
-
"processing": _processing(),
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
def risk_profile(self, name: str, *, depth: str = "auto") -> dict[str, object]:
|
| 145 |
-
return {
|
| 146 |
-
"ticker": name,
|
| 147 |
-
"tail_risk": {},
|
| 148 |
-
"convexity": {},
|
| 149 |
-
"volatility": {"requestedDepth": depth},
|
| 150 |
-
"drawdown": {},
|
| 151 |
-
"warnings": [],
|
| 152 |
-
"processing": _processing(),
|
| 153 |
-
}
|
| 154 |
-
|
| 155 |
-
def valuation(self, ticker: str) -> dict[str, object]:
|
| 156 |
-
return {
|
| 157 |
-
"ticker": ticker,
|
| 158 |
-
"dcf": {"status": "ready", "intrinsic_value": 120.0},
|
| 159 |
-
"reverse_dcf": {"status": "ready", "implied_growth_pct": 8.0},
|
| 160 |
-
"relative": {"trailing_pe": 22.0},
|
| 161 |
-
"graham_number": 100.0,
|
| 162 |
-
"margin_of_safety_pct": 12.0,
|
| 163 |
-
"current_price": 107.0,
|
| 164 |
-
"processing": _processing(),
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
def sec_filings(self, ticker: str) -> dict[str, object]:
|
| 168 |
-
return {"ticker": ticker, "cik": 1, "forms": [], "filings": [], "processing": _processing()}
|
| 169 |
-
|
| 170 |
-
def sec_filing_document(
|
| 171 |
-
self, ticker: str, accession: str, primaryDocument: str, *, form: str = "10-Q"
|
| 172 |
-
) -> dict[str, object]:
|
| 173 |
-
return {"ticker": ticker, "accession": accession, "primaryDocument": primaryDocument, "toc": [], "charCount": 0, "indexUrl": "", "documentUrl": "", "processing": _processing()}
|
| 174 |
-
|
| 175 |
-
def sec_filing_section(
|
| 176 |
-
self, ticker: str, accession: str, primaryDocument: str, sectionSlug: str, *, form: str = "10-Q"
|
| 177 |
-
) -> dict[str, object]:
|
| 178 |
-
return {"ticker": ticker, "accession": accession, "sectionSlug": sectionSlug, "sectionTitle": "stub", "markdown": "", "charCount": 0, "documentUrl": "", "processing": _processing()}
|
| 179 |
|
| 180 |
-
def fear_greed(self) -> dict[str, object]:
|
| 181 |
-
return {"score": 50, "rating": "Neutral", "processing": _processing()}
|
| 182 |
-
|
| 183 |
-
def sp500_dcf(self) -> dict[str, object]:
|
| 184 |
-
return {"status": "ready", "currentIntrinsicValue": 5000.0, "processing": _processing()}
|
| 185 |
-
|
| 186 |
-
def beta_estimate(self, ticker: str) -> dict[str, object]:
|
| 187 |
-
return {"symbol": ticker, "beta": 1.0, "adjustedBeta": 1.0, "rSquared": 0.5, "processing": _processing()}
|
| 188 |
-
|
| 189 |
-
def top_companies(self) -> dict[str, object]:
|
| 190 |
-
return {"companies": [], "count": 0, "processing": _processing()}
|
| 191 |
-
|
| 192 |
-
def market_regime(self) -> dict[str, object]:
|
| 193 |
-
return {"summary": "stub", "confidence": "low", "signals": [], "processing": _processing()}
|
| 194 |
-
|
| 195 |
-
def trailing_forward_pe(self) -> dict[str, object]:
|
| 196 |
-
return {"date": "2026-04-01", "latestValue": 0.0, "history": [], "processing": _processing()}
|
| 197 |
-
|
| 198 |
-
def market_breadth(self) -> dict[str, object]:
|
| 199 |
-
return {"metrics": [], "processing": _processing()}
|
| 200 |
-
|
| 201 |
-
def watchlist(self) -> dict[str, object]:
|
| 202 |
-
return {"items": [], "count": 0, "processing": _processing()}
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
def _fake_chart_opener(
|
| 206 |
-
data_or_names,
|
| 207 |
-
*,
|
| 208 |
-
session_id: str | None = None,
|
| 209 |
-
**kwargs,
|
| 210 |
-
) -> dict[str, object]:
|
| 211 |
-
_ = kwargs
|
| 212 |
-
return {
|
| 213 |
-
"ok": True,
|
| 214 |
-
"sessionId": session_id or "agent:chart",
|
| 215 |
-
"chartUrl": f"http://127.0.0.1:8001/chart?sessionId={session_id or 'agent:chart'}",
|
| 216 |
-
"processing": _processing(),
|
| 217 |
-
"inputEcho": data_or_names,
|
| 218 |
-
}
|
| 219 |
|
| 220 |
|
| 221 |
def _loop(model_client, *, max_steps: int = 8, service: _FakeService | None = None) -> TerraFinHostedAgentLoop:
|
|
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
+
from fakes import BaseFakeService as _FakeService
|
| 5 |
+
from fakes import fake_chart_opener as _fake_chart_opener
|
| 6 |
|
| 7 |
+
from TerraFin.agent.conversation import is_internal_only_message
|
| 8 |
from TerraFin.agent.definitions import (
|
| 9 |
DEFAULT_HOSTED_AGENT_NAME,
|
| 10 |
build_default_agent_definition_registry,
|
| 11 |
)
|
|
|
|
| 12 |
from TerraFin.agent.guru import (
|
| 13 |
GuruResearchMemo,
|
| 14 |
GuruRoutePlan,
|
|
|
|
| 31 |
from TerraFin.agent.transcript_store import HostedTranscriptStore
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def _public_roles(messages):
|
| 35 |
return [message.role for message in messages if not is_internal_only_message(message)]
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
def _loop(model_client, *, max_steps: int = 8, service: _FakeService | None = None) -> TerraFinHostedAgentLoop:
|
|
@@ -1,203 +1,22 @@
|
|
| 1 |
import pytest
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import TerraFin.agent.runtime as agent_runtime
|
| 4 |
|
| 5 |
|
| 6 |
-
def _processing() -> dict[str, object]:
|
| 7 |
-
return {
|
| 8 |
-
"requestedDepth": "auto",
|
| 9 |
-
"resolvedDepth": "full",
|
| 10 |
-
"loadedStart": "2024-01-01",
|
| 11 |
-
"loadedEnd": "2024-12-31",
|
| 12 |
-
"isComplete": True,
|
| 13 |
-
"hasOlder": False,
|
| 14 |
-
"sourceVersion": "test-source",
|
| 15 |
-
"view": "daily",
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class _FakeService:
|
| 20 |
-
def resolve(self, query: str) -> dict[str, object]:
|
| 21 |
-
return {"type": "stock", "name": query.upper(), "path": f"/stock/{query.upper()}", "processing": _processing()}
|
| 22 |
-
|
| 23 |
-
def market_data(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 24 |
-
return {"ticker": name, "seriesType": "candlestick", "count": 1, "data": [], "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 25 |
-
|
| 26 |
-
def patterns(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 27 |
-
return {"ticker": name, "signals": [], "total": 0, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 28 |
-
|
| 29 |
-
def fcf_history(self, ticker: str, years: int = 10) -> dict[str, object]:
|
| 30 |
-
return {
|
| 31 |
-
"ticker": ticker, "years": years, "rows": [],
|
| 32 |
-
"candidates": {"threeYearAvg": None, "latestAnnual": None, "ttm": None},
|
| 33 |
-
"autoSelectedSource": "annual", "processing": _processing(),
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
def similarity_search(self, ticker: str, universe: str = "sp500+nasdaq100+kospi200", period: str = "1y", top_n: int = 20) -> dict[str, object]:
|
| 37 |
-
return {"ticker": ticker, "period": period, "pool": {}, "results": [], "count": 0, "processing": _processing()}
|
| 38 |
-
|
| 39 |
-
def indicators(
|
| 40 |
-
self,
|
| 41 |
-
name: str,
|
| 42 |
-
indicators: str,
|
| 43 |
-
*,
|
| 44 |
-
depth: str = "auto",
|
| 45 |
-
view: str = "daily",
|
| 46 |
-
) -> dict[str, object]:
|
| 47 |
-
return {
|
| 48 |
-
"ticker": name,
|
| 49 |
-
"indicators": {"rsi": {"name": "rsi", "offset": 0, "values": {"value": 55.0}}},
|
| 50 |
-
"unknown": [],
|
| 51 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view, "indicatorQuery": indicators},
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
def market_snapshot(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 55 |
-
return {
|
| 56 |
-
"ticker": name,
|
| 57 |
-
"price_action": {"current": 100.0},
|
| 58 |
-
"indicators": {"rsi": 55.0},
|
| 59 |
-
"market_breadth": [],
|
| 60 |
-
"watchlist": [],
|
| 61 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
def lppl_analysis(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 65 |
-
return {"name": name, "confidence": 0.2, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 66 |
-
|
| 67 |
-
def company_info(self, ticker: str) -> dict[str, object]:
|
| 68 |
-
return {"ticker": ticker, "shortName": f"{ticker} Corp", "processing": _processing()}
|
| 69 |
-
|
| 70 |
-
def earnings(self, ticker: str) -> dict[str, object]:
|
| 71 |
-
return {"ticker": ticker, "earnings": [], "processing": _processing()}
|
| 72 |
-
|
| 73 |
-
def financials(self, ticker: str, *, statement: str = "income", period: str = "annual") -> dict[str, object]:
|
| 74 |
-
return {"ticker": ticker, "statement": statement, "period": period, "columns": [], "rows": [], "processing": _processing()}
|
| 75 |
-
|
| 76 |
-
def portfolio(self, guru: str) -> dict[str, object]:
|
| 77 |
-
return {"guru": guru, "info": {}, "holdings": [], "count": 0, "processing": _processing()}
|
| 78 |
-
|
| 79 |
-
def economic(self, indicators: str) -> dict[str, object]:
|
| 80 |
-
return {"indicators": {indicators: {"latest_value": 3.0}}, "processing": _processing()}
|
| 81 |
-
|
| 82 |
-
def macro_focus(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 83 |
-
return {
|
| 84 |
-
"name": name,
|
| 85 |
-
"info": {"name": name, "type": "index", "description": "Macro", "currentValue": 1.0, "change": 0.0, "changePercent": 0.0},
|
| 86 |
-
"seriesType": "line",
|
| 87 |
-
"count": 1,
|
| 88 |
-
"data": [],
|
| 89 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
def calendar_events(
|
| 93 |
-
self,
|
| 94 |
-
*,
|
| 95 |
-
year: int,
|
| 96 |
-
month: int,
|
| 97 |
-
categories: str | None = None,
|
| 98 |
-
limit: int | None = None,
|
| 99 |
-
) -> dict[str, object]:
|
| 100 |
-
return {"events": [], "count": 0, "month": month, "year": year, "categories": categories, "limit": limit, "processing": _processing()}
|
| 101 |
-
|
| 102 |
-
def fundamental_screen(self, ticker: str) -> dict[str, object]:
|
| 103 |
-
return {
|
| 104 |
-
"ticker": ticker,
|
| 105 |
-
"moat": {"score": "wide"},
|
| 106 |
-
"earnings_quality": {},
|
| 107 |
-
"balance_sheet": {},
|
| 108 |
-
"capital_allocation": {},
|
| 109 |
-
"pricing_power": {},
|
| 110 |
-
"warnings": [],
|
| 111 |
-
"processing": _processing(),
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
def risk_profile(self, name: str, *, depth: str = "auto") -> dict[str, object]:
|
| 115 |
-
return {
|
| 116 |
-
"ticker": name,
|
| 117 |
-
"tail_risk": {},
|
| 118 |
-
"convexity": {},
|
| 119 |
-
"volatility": {"requestedDepth": depth},
|
| 120 |
-
"drawdown": {},
|
| 121 |
-
"warnings": [],
|
| 122 |
-
"processing": _processing(),
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
def valuation(self, ticker: str) -> dict[str, object]:
|
| 126 |
-
return {
|
| 127 |
-
"ticker": ticker,
|
| 128 |
-
"dcf": {"status": "ready", "intrinsic_value": 120.0},
|
| 129 |
-
"reverse_dcf": {"status": "ready", "implied_growth_pct": 8.0},
|
| 130 |
-
"relative": {"trailing_pe": 22.0},
|
| 131 |
-
"graham_number": 100.0,
|
| 132 |
-
"margin_of_safety_pct": 12.0,
|
| 133 |
-
"current_price": 107.0,
|
| 134 |
-
"processing": _processing(),
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
def sec_filings(self, ticker: str) -> dict[str, object]:
|
| 138 |
-
return {"ticker": ticker, "cik": 1, "forms": [], "filings": [], "processing": _processing()}
|
| 139 |
-
|
| 140 |
-
def sec_filing_document(
|
| 141 |
-
self, ticker: str, accession: str, primaryDocument: str, *, form: str = "10-Q"
|
| 142 |
-
) -> dict[str, object]:
|
| 143 |
-
return {"ticker": ticker, "accession": accession, "primaryDocument": primaryDocument, "toc": [], "charCount": 0, "indexUrl": "", "documentUrl": "", "processing": _processing()}
|
| 144 |
-
|
| 145 |
-
def sec_filing_section(
|
| 146 |
-
self, ticker: str, accession: str, primaryDocument: str, sectionSlug: str, *, form: str = "10-Q"
|
| 147 |
-
) -> dict[str, object]:
|
| 148 |
-
return {"ticker": ticker, "accession": accession, "sectionSlug": sectionSlug, "sectionTitle": "stub", "markdown": "", "charCount": 0, "documentUrl": "", "processing": _processing()}
|
| 149 |
-
|
| 150 |
-
def fear_greed(self) -> dict[str, object]:
|
| 151 |
-
return {"score": 50, "rating": "Neutral", "processing": _processing()}
|
| 152 |
-
|
| 153 |
-
def sp500_dcf(self) -> dict[str, object]:
|
| 154 |
-
return {"status": "ready", "currentIntrinsicValue": 5000.0, "processing": _processing()}
|
| 155 |
-
|
| 156 |
-
def beta_estimate(self, ticker: str) -> dict[str, object]:
|
| 157 |
-
return {"symbol": ticker, "beta": 1.0, "adjustedBeta": 1.0, "rSquared": 0.5, "processing": _processing()}
|
| 158 |
-
|
| 159 |
-
def top_companies(self) -> dict[str, object]:
|
| 160 |
-
return {"companies": [], "count": 0, "processing": _processing()}
|
| 161 |
-
|
| 162 |
-
def market_regime(self) -> dict[str, object]:
|
| 163 |
-
return {"summary": "stub", "confidence": "low", "signals": [], "processing": _processing()}
|
| 164 |
-
|
| 165 |
-
def trailing_forward_pe(self) -> dict[str, object]:
|
| 166 |
-
return {"date": "2026-04-01", "latestValue": 0.0, "history": [], "processing": _processing()}
|
| 167 |
-
|
| 168 |
-
def market_breadth(self) -> dict[str, object]:
|
| 169 |
-
return {"metrics": [], "processing": _processing()}
|
| 170 |
-
|
| 171 |
-
def watchlist(self) -> dict[str, object]:
|
| 172 |
-
return {"items": [], "count": 0, "processing": _processing()}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
class _ExplodingService(_FakeService):
|
| 176 |
def market_snapshot(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 177 |
_ = name, depth, view
|
| 178 |
raise RuntimeError("snapshot failed")
|
| 179 |
|
| 180 |
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
_ = kwargs
|
| 188 |
-
return {
|
| 189 |
-
"ok": True,
|
| 190 |
-
"sessionId": session_id or "agent:chart",
|
| 191 |
-
"chartUrl": f"http://127.0.0.1:8001/chart?sessionId={session_id or 'agent:chart'}",
|
| 192 |
-
"processing": _processing(),
|
| 193 |
-
"inputEcho": data_or_names,
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def test_default_capability_registry_contains_kernel_capabilities() -> None:
|
| 198 |
-
registry = agent_runtime.build_default_capability_registry(_FakeService(), chart_opener=_fake_chart_opener)
|
| 199 |
-
|
| 200 |
-
assert registry.names() == (
|
| 201 |
"resolve",
|
| 202 |
"market_data",
|
| 203 |
"indicators",
|
|
@@ -211,9 +30,6 @@ def test_default_capability_registry_contains_kernel_capabilities() -> None:
|
|
| 211 |
"economic",
|
| 212 |
"macro_focus",
|
| 213 |
"calendar_events",
|
| 214 |
-
# Dashboard widget-parity capabilities, inserted before `open_chart` so
|
| 215 |
-
# registry ordering tracks grouping (research read-only first, then
|
| 216 |
-
# chart-opening, then SEC filings).
|
| 217 |
"fear_greed",
|
| 218 |
"sp500_dcf",
|
| 219 |
"beta_estimate",
|
|
@@ -231,7 +47,34 @@ def test_default_capability_registry_contains_kernel_capabilities() -> None:
|
|
| 231 |
"sec_filings",
|
| 232 |
"sec_filing_document",
|
| 233 |
"sec_filing_section",
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
|
| 237 |
def test_context_call_records_focus_and_capability_history() -> None:
|
|
|
|
| 1 |
import pytest
|
| 2 |
+
from fakes import BaseFakeService as _FakeService
|
| 3 |
+
from fakes import fake_chart_opener as _fake_chart_opener
|
| 4 |
|
| 5 |
import TerraFin.agent.runtime as agent_runtime
|
| 6 |
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
class _ExplodingService(_FakeService):
|
| 9 |
def market_snapshot(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 10 |
_ = name, depth, view
|
| 11 |
raise RuntimeError("snapshot failed")
|
| 12 |
|
| 13 |
|
| 14 |
+
# Capabilities that must always be present. Membership is asserted rather than
|
| 15 |
+
# an exact ordered tuple so that adding a capability does not require editing
|
| 16 |
+
# this list; `test_default_capability_registry_ordering_convention` covers the
|
| 17 |
+
# grouping rule that ordering is actually meant to express.
|
| 18 |
+
KERNEL_CAPABILITIES = frozenset(
|
| 19 |
+
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
"resolve",
|
| 21 |
"market_data",
|
| 22 |
"indicators",
|
|
|
|
| 30 |
"economic",
|
| 31 |
"macro_focus",
|
| 32 |
"calendar_events",
|
|
|
|
|
|
|
|
|
|
| 33 |
"fear_greed",
|
| 34 |
"sp500_dcf",
|
| 35 |
"beta_estimate",
|
|
|
|
| 47 |
"sec_filings",
|
| 48 |
"sec_filing_document",
|
| 49 |
"sec_filing_section",
|
| 50 |
+
}
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _registry_names() -> tuple[str, ...]:
|
| 55 |
+
registry = agent_runtime.build_default_capability_registry(_FakeService(), chart_opener=_fake_chart_opener)
|
| 56 |
+
return registry.names()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_default_capability_registry_contains_kernel_capabilities() -> None:
|
| 60 |
+
names = _registry_names()
|
| 61 |
+
|
| 62 |
+
missing = KERNEL_CAPABILITIES - set(names)
|
| 63 |
+
assert not missing, f"kernel capabilities missing from the registry: {sorted(missing)}"
|
| 64 |
+
assert len(names) == len(set(names)), "registry contains duplicate capability names"
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_default_capability_registry_ordering_convention() -> None:
|
| 68 |
+
"""Registry order groups research read-only, then chart-opening, then SEC filings."""
|
| 69 |
+
|
| 70 |
+
names = _registry_names()
|
| 71 |
+
sec_positions = [index for index, name in enumerate(names) if name.startswith("sec_")]
|
| 72 |
+
|
| 73 |
+
assert sec_positions, "expected SEC filing capabilities in the registry"
|
| 74 |
+
assert sec_positions == list(
|
| 75 |
+
range(sec_positions[0], sec_positions[-1] + 1)
|
| 76 |
+
), f"SEC filing capabilities must stay contiguous, got positions {sec_positions}"
|
| 77 |
+
assert names.index("open_chart") < sec_positions[0], "`open_chart` must precede the SEC filing group"
|
| 78 |
|
| 79 |
|
| 80 |
def test_context_call_records_focus_and_capability_history() -> None:
|
|
@@ -1,5 +1,11 @@
|
|
| 1 |
from datetime import UTC, datetime
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from TerraFin.agent.contracts.conversation_state import RUNTIME_MODEL_METADATA_KEY
|
| 4 |
from TerraFin.agent.runtime import build_default_capability_registry
|
| 5 |
from TerraFin.agent.runtime.context import create_agent_context
|
|
@@ -11,11 +17,6 @@ from TerraFin.agent.session_store import (
|
|
| 11 |
TerraFinHostedSessionRecord,
|
| 12 |
)
|
| 13 |
|
| 14 |
-
# Reuse the full-featured fake service + chart opener the hosted-runtime tests
|
| 15 |
-
# already maintain: build_default_capability_registry needs every service
|
| 16 |
-
# method, but list_light_metadata itself never invokes any capability.
|
| 17 |
-
from tests.agent.test_hosted_runtime import _FakeService, _fake_chart_opener
|
| 18 |
-
|
| 19 |
|
| 20 |
def _ts(hour: int) -> datetime:
|
| 21 |
return datetime(2026, 4, 16, hour, 0, tzinfo=UTC)
|
|
|
|
| 1 |
from datetime import UTC, datetime
|
| 2 |
|
| 3 |
+
# Reuse the shared stub service + chart opener from tests/conftest.py:
|
| 4 |
+
# build_default_capability_registry needs every service method, but
|
| 5 |
+
# list_light_metadata itself never invokes any capability.
|
| 6 |
+
from fakes import BaseFakeService as _FakeService
|
| 7 |
+
from fakes import fake_chart_opener as _fake_chart_opener
|
| 8 |
+
|
| 9 |
from TerraFin.agent.contracts.conversation_state import RUNTIME_MODEL_METADATA_KEY
|
| 10 |
from TerraFin.agent.runtime import build_default_capability_registry
|
| 11 |
from TerraFin.agent.runtime.context import create_agent_context
|
|
|
|
| 17 |
TerraFinHostedSessionRecord,
|
| 18 |
)
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
def _ts(hour: int) -> datetime:
|
| 22 |
return datetime(2026, 4, 16, hour, 0, tzinfo=UTC)
|
|
@@ -1,4 +1,6 @@
|
|
| 1 |
import pytest
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from TerraFin.agent.definitions import (
|
| 4 |
DEFAULT_HOSTED_AGENT_NAME,
|
|
@@ -10,206 +12,9 @@ from TerraFin.agent.runtime import build_default_capability_registry
|
|
| 10 |
from TerraFin.agent.tools import TerraFinHostedToolAdapter
|
| 11 |
|
| 12 |
|
| 13 |
-
|
| 14 |
-
return {
|
| 15 |
-
"requestedDepth": "auto",
|
| 16 |
-
"resolvedDepth": "full",
|
| 17 |
-
"loadedStart": "2024-01-01",
|
| 18 |
-
"loadedEnd": "2024-12-31",
|
| 19 |
-
"isComplete": True,
|
| 20 |
-
"hasOlder": False,
|
| 21 |
-
"sourceVersion": "test-source",
|
| 22 |
-
"view": "daily",
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
class _FakeService:
|
| 27 |
-
def resolve(self, query: str) -> dict[str, object]:
|
| 28 |
-
return {"type": "stock", "name": query.upper(), "path": f"/stock/{query.upper()}", "processing": _processing()}
|
| 29 |
-
|
| 30 |
-
def market_data(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 31 |
-
return {"ticker": name, "seriesType": "candlestick", "count": 1, "data": [], "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 32 |
-
|
| 33 |
-
def indicators(
|
| 34 |
-
self,
|
| 35 |
-
name: str,
|
| 36 |
-
indicators: str,
|
| 37 |
-
*,
|
| 38 |
-
depth: str = "auto",
|
| 39 |
-
view: str = "daily",
|
| 40 |
-
) -> dict[str, object]:
|
| 41 |
-
return {
|
| 42 |
-
"ticker": name,
|
| 43 |
-
"indicators": {"rsi": {"name": "rsi", "offset": 0, "values": {"value": 55.0}}},
|
| 44 |
-
"unknown": [],
|
| 45 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view, "indicatorQuery": indicators},
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
def patterns(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 49 |
-
return {"ticker": name, "signals": [], "total": 0, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 50 |
-
|
| 51 |
-
def market_snapshot(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 52 |
-
return {
|
| 53 |
-
"ticker": name,
|
| 54 |
-
"price_action": {"current": 100.0},
|
| 55 |
-
"indicators": {"rsi": 55.0},
|
| 56 |
-
"market_breadth": [],
|
| 57 |
-
"watchlist": [],
|
| 58 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
def lppl_analysis(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 62 |
-
return {"name": name, "confidence": 0.2, "processing": {**_processing(), "requestedDepth": depth, "view": view}}
|
| 63 |
-
|
| 64 |
-
def company_info(self, ticker: str) -> dict[str, object]:
|
| 65 |
-
return {"ticker": ticker, "shortName": f"{ticker} Corp", "processing": _processing()}
|
| 66 |
-
|
| 67 |
-
def earnings(self, ticker: str) -> dict[str, object]:
|
| 68 |
-
return {"ticker": ticker, "earnings": [], "processing": _processing()}
|
| 69 |
-
|
| 70 |
-
def financials(self, ticker: str, *, statement: str = "income", period: str = "annual") -> dict[str, object]:
|
| 71 |
-
return {"ticker": ticker, "statement": statement, "period": period, "columns": [], "rows": [], "processing": _processing()}
|
| 72 |
-
|
| 73 |
-
def portfolio(self, guru: str) -> dict[str, object]:
|
| 74 |
-
return {"guru": guru, "info": {}, "holdings": [], "count": 0, "processing": _processing()}
|
| 75 |
-
|
| 76 |
-
def economic(self, indicators: str) -> dict[str, object]:
|
| 77 |
-
return {"indicators": {indicators: {"latest_value": 3.0}}, "processing": _processing()}
|
| 78 |
-
|
| 79 |
-
def macro_focus(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 80 |
-
return {
|
| 81 |
-
"name": name,
|
| 82 |
-
"info": {"name": name, "type": "index", "description": "Macro", "currentValue": 1.0, "change": 0.0, "changePercent": 0.0},
|
| 83 |
-
"seriesType": "line",
|
| 84 |
-
"count": 1,
|
| 85 |
-
"data": [],
|
| 86 |
-
"processing": {**_processing(), "requestedDepth": depth, "view": view},
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
def calendar_events(
|
| 90 |
-
self,
|
| 91 |
-
*,
|
| 92 |
-
year: int,
|
| 93 |
-
month: int,
|
| 94 |
-
categories: str | None = None,
|
| 95 |
-
limit: int | None = None,
|
| 96 |
-
) -> dict[str, object]:
|
| 97 |
-
return {"events": [], "count": 0, "month": month, "year": year, "categories": categories, "limit": limit, "processing": _processing()}
|
| 98 |
-
|
| 99 |
-
def fundamental_screen(self, ticker: str) -> dict[str, object]:
|
| 100 |
-
return {
|
| 101 |
-
"ticker": ticker,
|
| 102 |
-
"moat": {"score": "wide"},
|
| 103 |
-
"earnings_quality": {},
|
| 104 |
-
"balance_sheet": {},
|
| 105 |
-
"capital_allocation": {},
|
| 106 |
-
"pricing_power": {},
|
| 107 |
-
"warnings": [],
|
| 108 |
-
"processing": _processing(),
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
def risk_profile(self, name: str, *, depth: str = "auto") -> dict[str, object]:
|
| 112 |
-
return {
|
| 113 |
-
"ticker": name,
|
| 114 |
-
"tail_risk": {},
|
| 115 |
-
"convexity": {},
|
| 116 |
-
"volatility": {"requestedDepth": depth},
|
| 117 |
-
"drawdown": {},
|
| 118 |
-
"warnings": [],
|
| 119 |
-
"processing": _processing(),
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
def valuation(self, ticker: str) -> dict[str, object]:
|
| 123 |
-
return {
|
| 124 |
-
"ticker": ticker,
|
| 125 |
-
"dcf": {"status": "ready", "intrinsic_value": 120.0},
|
| 126 |
-
"reverse_dcf": {"status": "ready", "implied_growth_pct": 8.0},
|
| 127 |
-
"relative": {"trailing_pe": 22.0},
|
| 128 |
-
"graham_number": 100.0,
|
| 129 |
-
"margin_of_safety_pct": 12.0,
|
| 130 |
-
"current_price": 107.0,
|
| 131 |
-
"processing": _processing(),
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
def sec_filings(self, ticker: str) -> dict[str, object]:
|
| 135 |
-
return {"ticker": ticker, "cik": 1, "forms": ["10-K"], "filings": [], "processing":
|
| 136 |
-
|
| 137 |
-
def sec_filing_document(
|
| 138 |
-
self, ticker: str, accession: str, primaryDocument: str, *, form: str = "10-Q"
|
| 139 |
-
) -> dict[str, object]:
|
| 140 |
-
return {
|
| 141 |
-
"ticker": ticker,
|
| 142 |
-
"accession": accession,
|
| 143 |
-
"primaryDocument": primaryDocument,
|
| 144 |
-
"toc": [],
|
| 145 |
-
"charCount": 0,
|
| 146 |
-
"indexUrl": "",
|
| 147 |
-
"documentUrl": "",
|
| 148 |
-
"processing": _processing(),
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
def sec_filing_section(
|
| 152 |
-
self,
|
| 153 |
-
ticker: str,
|
| 154 |
-
accession: str,
|
| 155 |
-
primaryDocument: str,
|
| 156 |
-
sectionSlug: str,
|
| 157 |
-
*,
|
| 158 |
-
form: str = "10-Q",
|
| 159 |
-
) -> dict[str, object]:
|
| 160 |
-
return {
|
| 161 |
-
"ticker": ticker,
|
| 162 |
-
"accession": accession,
|
| 163 |
-
"sectionSlug": sectionSlug,
|
| 164 |
-
"sectionTitle": "stub",
|
| 165 |
-
"markdown": "",
|
| 166 |
-
"charCount": 0,
|
| 167 |
-
"documentUrl": "",
|
| 168 |
-
"processing": _processing(),
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
def fcf_history(self, ticker: str, years: int = 10) -> dict[str, object]:
|
| 172 |
-
return {
|
| 173 |
-
"ticker": ticker,
|
| 174 |
-
"years": years,
|
| 175 |
-
"rows": [],
|
| 176 |
-
"candidates": {"threeYearAvg": None, "latestAnnual": None, "ttm": None},
|
| 177 |
-
"autoSelectedSource": "annual",
|
| 178 |
-
"processing": _processing(),
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
def similarity_search(
|
| 182 |
-
self,
|
| 183 |
-
ticker: str,
|
| 184 |
-
universe: str = "sp500+nasdaq100+kospi200",
|
| 185 |
-
period: str = "1y",
|
| 186 |
-
top_n: int = 20,
|
| 187 |
-
) -> dict[str, object]:
|
| 188 |
-
return {"ticker": ticker, "period": period, "pool": {}, "results": [], "count": 0, "processing": _processing()}
|
| 189 |
-
|
| 190 |
-
def fear_greed(self) -> dict[str, object]:
|
| 191 |
-
return {"score": 50, "rating": "Neutral", "processing": _processing()}
|
| 192 |
-
|
| 193 |
-
def sp500_dcf(self) -> dict[str, object]:
|
| 194 |
-
return {"status": "ready", "currentIntrinsicValue": 5000.0, "processing": _processing()}
|
| 195 |
-
|
| 196 |
-
def beta_estimate(self, ticker: str) -> dict[str, object]:
|
| 197 |
-
return {"symbol": ticker, "beta": 1.0, "adjustedBeta": 1.0, "rSquared": 0.5, "processing": _processing()}
|
| 198 |
-
|
| 199 |
-
def top_companies(self) -> dict[str, object]:
|
| 200 |
-
return {"companies": [], "count": 0, "processing": _processing()}
|
| 201 |
-
|
| 202 |
-
def market_regime(self) -> dict[str, object]:
|
| 203 |
-
return {"summary": "stub", "confidence": "low", "signals": [], "processing": _processing()}
|
| 204 |
-
|
| 205 |
-
def trailing_forward_pe(self) -> dict[str, object]:
|
| 206 |
-
return {"date": "2026-04-01", "latestValue": 0.0, "history": [], "processing": _processing()}
|
| 207 |
-
|
| 208 |
-
def market_breadth(self) -> dict[str, object]:
|
| 209 |
-
return {"metrics": [], "processing": _processing()}
|
| 210 |
-
|
| 211 |
-
def watchlist(self) -> dict[str, object]:
|
| 212 |
-
return {"items": [], "count": 0, "processing": _processing()}
|
| 213 |
|
| 214 |
|
| 215 |
class _RetryingFakeService(_FakeService):
|
|
@@ -238,20 +43,6 @@ class _MacroFocusEquityMisuseService(_FakeService):
|
|
| 238 |
raise LookupError(f"Unknown macro instrument: '{name}'")
|
| 239 |
|
| 240 |
|
| 241 |
-
def _fake_chart_opener(
|
| 242 |
-
data_or_names,
|
| 243 |
-
*,
|
| 244 |
-
session_id: str | None = None,
|
| 245 |
-
**kwargs,
|
| 246 |
-
) -> dict[str, object]:
|
| 247 |
-
_ = kwargs
|
| 248 |
-
return {
|
| 249 |
-
"ok": True,
|
| 250 |
-
"sessionId": session_id or "agent:chart",
|
| 251 |
-
"chartUrl": f"http://127.0.0.1:8001/chart?sessionId={session_id or 'agent:chart'}",
|
| 252 |
-
"processing": _processing(),
|
| 253 |
-
"inputEcho": data_or_names,
|
| 254 |
-
}
|
| 255 |
|
| 256 |
|
| 257 |
def _adapter(
|
|
|
|
| 1 |
import pytest
|
| 2 |
+
from fakes import BaseFakeService, processing
|
| 3 |
+
from fakes import fake_chart_opener as _fake_chart_opener
|
| 4 |
|
| 5 |
from TerraFin.agent.definitions import (
|
| 6 |
DEFAULT_HOSTED_AGENT_NAME,
|
|
|
|
| 12 |
from TerraFin.agent.tools import TerraFinHostedToolAdapter
|
| 13 |
|
| 14 |
|
| 15 |
+
class _FakeService(BaseFakeService):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def sec_filings(self, ticker: str) -> dict[str, object]:
|
| 17 |
+
return {"ticker": ticker, "cik": 1, "forms": ["10-K"], "filings": [], "processing": processing()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
class _RetryingFakeService(_FakeService):
|
|
|
|
| 43 |
raise LookupError(f"Unknown macro instrument: '{name}'")
|
| 44 |
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def _adapter(
|
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared stub doubles for the TerraFin test suite.
|
| 2 |
+
|
| 3 |
+
Imported as `from fakes import ...`; `tests/` is on `sys.path` via the
|
| 4 |
+
`pythonpath` setting in `pyproject.toml`.
|
| 5 |
+
|
| 6 |
+
`BaseFakeService` is the single stub service used by every test that builds the
|
| 7 |
+
default capability registry. `build_default_capability_registry` binds
|
| 8 |
+
`service.<method>` eagerly for each registered capability, so a fake that is
|
| 9 |
+
missing a method fails at registry-build time with `AttributeError`.
|
| 10 |
+
|
| 11 |
+
To keep that from turning every new capability into a mechanical edit across
|
| 12 |
+
several test modules, unknown attributes resolve through `__getattr__` to a
|
| 13 |
+
generic stub. Methods are still spelled out explicitly below when a test
|
| 14 |
+
asserts on their payload shape; anything added later works without changes
|
| 15 |
+
here. Override a single method in a subclass when a module needs a different
|
| 16 |
+
payload.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from typing import Any
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def processing() -> dict[str, object]:
|
| 23 |
+
"""Return the standard `processing` metadata block used by stub payloads."""
|
| 24 |
+
|
| 25 |
+
return {
|
| 26 |
+
"requestedDepth": "auto",
|
| 27 |
+
"resolvedDepth": "full",
|
| 28 |
+
"loadedStart": "2024-01-01",
|
| 29 |
+
"loadedEnd": "2024-12-31",
|
| 30 |
+
"isComplete": True,
|
| 31 |
+
"hasOlder": False,
|
| 32 |
+
"sourceVersion": "test-source",
|
| 33 |
+
"view": "daily",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class BaseFakeService:
|
| 38 |
+
"""Stub `TerraFinAgentService` covering the default capability registry."""
|
| 39 |
+
|
| 40 |
+
def __getattr__(self, name: str) -> Any:
|
| 41 |
+
"""Resolve capabilities with no explicit stub to a generic payload.
|
| 42 |
+
|
| 43 |
+
Private and dunder lookups must still raise so that `copy`, `pickle`,
|
| 44 |
+
and pytest introspection keep working.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
if name.startswith("_"):
|
| 48 |
+
raise AttributeError(name)
|
| 49 |
+
|
| 50 |
+
def _auto_stub(*args: Any, **kwargs: Any) -> dict[str, object]:
|
| 51 |
+
return {
|
| 52 |
+
"capability": name,
|
| 53 |
+
"args": list(args),
|
| 54 |
+
"kwargs": kwargs,
|
| 55 |
+
"processing": processing(),
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return _auto_stub
|
| 59 |
+
|
| 60 |
+
def resolve(self, query: str) -> dict[str, object]:
|
| 61 |
+
return {"type": "stock", "name": query.upper(), "path": f"/stock/{query.upper()}", "processing": processing()}
|
| 62 |
+
|
| 63 |
+
def market_data(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 64 |
+
return {"ticker": name, "seriesType": "candlestick", "count": 1, "data": [], "processing": {**processing(), "requestedDepth": depth, "view": view}}
|
| 65 |
+
|
| 66 |
+
def patterns(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 67 |
+
return {"ticker": name, "signals": [], "total": 0, "processing": {**processing(), "requestedDepth": depth, "view": view}}
|
| 68 |
+
|
| 69 |
+
def fcf_history(self, ticker: str, years: int = 10) -> dict[str, object]:
|
| 70 |
+
return {
|
| 71 |
+
"ticker": ticker, "years": years, "rows": [],
|
| 72 |
+
"candidates": {"threeYearAvg": None, "latestAnnual": None, "ttm": None},
|
| 73 |
+
"autoSelectedSource": "annual", "processing": processing(),
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
def similarity_search(self, ticker: str, universe: str = "sp500+nasdaq100+kospi200", period: str = "1y", top_n: int = 20) -> dict[str, object]:
|
| 77 |
+
return {"ticker": ticker, "period": period, "pool": {}, "results": [], "count": 0, "processing": processing()}
|
| 78 |
+
|
| 79 |
+
def indicators(
|
| 80 |
+
self,
|
| 81 |
+
name: str,
|
| 82 |
+
indicators: str,
|
| 83 |
+
*,
|
| 84 |
+
depth: str = "auto",
|
| 85 |
+
view: str = "daily",
|
| 86 |
+
) -> dict[str, object]:
|
| 87 |
+
return {
|
| 88 |
+
"ticker": name,
|
| 89 |
+
"indicators": {"rsi": {"name": "rsi", "offset": 0, "values": {"value": 55.0}}},
|
| 90 |
+
"unknown": [],
|
| 91 |
+
"processing": {**processing(), "requestedDepth": depth, "view": view, "indicatorQuery": indicators},
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
def market_snapshot(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 95 |
+
return {
|
| 96 |
+
"ticker": name,
|
| 97 |
+
"price_action": {"current": 100.0},
|
| 98 |
+
"indicators": {"rsi": 55.0},
|
| 99 |
+
"market_breadth": [],
|
| 100 |
+
"watchlist": [],
|
| 101 |
+
"processing": {**processing(), "requestedDepth": depth, "view": view},
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def lppl_analysis(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 105 |
+
return {"name": name, "confidence": 0.2, "processing": {**processing(), "requestedDepth": depth, "view": view}}
|
| 106 |
+
|
| 107 |
+
def company_info(self, ticker: str) -> dict[str, object]:
|
| 108 |
+
return {"ticker": ticker, "shortName": f"{ticker} Corp", "processing": processing()}
|
| 109 |
+
|
| 110 |
+
def earnings(self, ticker: str) -> dict[str, object]:
|
| 111 |
+
return {"ticker": ticker, "earnings": [], "processing": processing()}
|
| 112 |
+
|
| 113 |
+
def financials(self, ticker: str, *, statement: str = "income", period: str = "annual") -> dict[str, object]:
|
| 114 |
+
return {"ticker": ticker, "statement": statement, "period": period, "columns": [], "rows": [], "processing": processing()}
|
| 115 |
+
|
| 116 |
+
def portfolio(self, guru: str) -> dict[str, object]:
|
| 117 |
+
return {"guru": guru, "info": {}, "holdings": [], "count": 0, "processing": processing()}
|
| 118 |
+
|
| 119 |
+
def economic(self, indicators: str) -> dict[str, object]:
|
| 120 |
+
return {"indicators": {indicators: {"latest_value": 3.0}}, "processing": processing()}
|
| 121 |
+
|
| 122 |
+
def macro_focus(self, name: str, *, depth: str = "auto", view: str = "daily") -> dict[str, object]:
|
| 123 |
+
return {
|
| 124 |
+
"name": name,
|
| 125 |
+
"info": {"name": name, "type": "index", "description": "Macro", "currentValue": 1.0, "change": 0.0, "changePercent": 0.0},
|
| 126 |
+
"seriesType": "line",
|
| 127 |
+
"count": 1,
|
| 128 |
+
"data": [],
|
| 129 |
+
"processing": {**processing(), "requestedDepth": depth, "view": view},
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
def calendar_events(
|
| 133 |
+
self,
|
| 134 |
+
*,
|
| 135 |
+
year: int,
|
| 136 |
+
month: int,
|
| 137 |
+
categories: str | None = None,
|
| 138 |
+
limit: int | None = None,
|
| 139 |
+
) -> dict[str, object]:
|
| 140 |
+
return {"events": [], "count": 0, "month": month, "year": year, "categories": categories, "limit": limit, "processing": processing()}
|
| 141 |
+
|
| 142 |
+
def fundamental_screen(self, ticker: str) -> dict[str, object]:
|
| 143 |
+
return {
|
| 144 |
+
"ticker": ticker,
|
| 145 |
+
"moat": {"score": "wide"},
|
| 146 |
+
"earnings_quality": {},
|
| 147 |
+
"balance_sheet": {},
|
| 148 |
+
"capital_allocation": {},
|
| 149 |
+
"pricing_power": {},
|
| 150 |
+
"warnings": [],
|
| 151 |
+
"processing": processing(),
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
def risk_profile(self, name: str, *, depth: str = "auto") -> dict[str, object]:
|
| 155 |
+
return {
|
| 156 |
+
"ticker": name,
|
| 157 |
+
"tail_risk": {},
|
| 158 |
+
"convexity": {},
|
| 159 |
+
"volatility": {"requestedDepth": depth},
|
| 160 |
+
"drawdown": {},
|
| 161 |
+
"warnings": [],
|
| 162 |
+
"processing": processing(),
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
def valuation(self, ticker: str) -> dict[str, object]:
|
| 166 |
+
return {
|
| 167 |
+
"ticker": ticker,
|
| 168 |
+
"dcf": {"status": "ready", "intrinsic_value": 120.0},
|
| 169 |
+
"reverse_dcf": {"status": "ready", "implied_growth_pct": 8.0},
|
| 170 |
+
"relative": {"trailing_pe": 22.0},
|
| 171 |
+
"graham_number": 100.0,
|
| 172 |
+
"margin_of_safety_pct": 12.0,
|
| 173 |
+
"current_price": 107.0,
|
| 174 |
+
"processing": processing(),
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
def sec_filings(self, ticker: str) -> dict[str, object]:
|
| 178 |
+
return {"ticker": ticker, "cik": 1, "forms": [], "filings": [], "processing": processing()}
|
| 179 |
+
|
| 180 |
+
def sec_filing_document(
|
| 181 |
+
self, ticker: str, accession: str, primaryDocument: str, *, form: str = "10-Q"
|
| 182 |
+
) -> dict[str, object]:
|
| 183 |
+
return {"ticker": ticker, "accession": accession, "primaryDocument": primaryDocument, "toc": [], "charCount": 0, "indexUrl": "", "documentUrl": "", "processing": processing()}
|
| 184 |
+
|
| 185 |
+
def sec_filing_section(
|
| 186 |
+
self, ticker: str, accession: str, primaryDocument: str, sectionSlug: str, *, form: str = "10-Q"
|
| 187 |
+
) -> dict[str, object]:
|
| 188 |
+
return {"ticker": ticker, "accession": accession, "sectionSlug": sectionSlug, "sectionTitle": "stub", "markdown": "", "charCount": 0, "documentUrl": "", "processing": processing()}
|
| 189 |
+
|
| 190 |
+
def fear_greed(self) -> dict[str, object]:
|
| 191 |
+
return {"score": 50, "rating": "Neutral", "processing": processing()}
|
| 192 |
+
|
| 193 |
+
def sp500_dcf(self) -> dict[str, object]:
|
| 194 |
+
return {"status": "ready", "currentIntrinsicValue": 5000.0, "processing": processing()}
|
| 195 |
+
|
| 196 |
+
def beta_estimate(self, ticker: str) -> dict[str, object]:
|
| 197 |
+
return {"symbol": ticker, "beta": 1.0, "adjustedBeta": 1.0, "rSquared": 0.5, "processing": processing()}
|
| 198 |
+
|
| 199 |
+
def top_companies(self) -> dict[str, object]:
|
| 200 |
+
return {"companies": [], "count": 0, "processing": processing()}
|
| 201 |
+
|
| 202 |
+
def market_regime(self) -> dict[str, object]:
|
| 203 |
+
return {"summary": "stub", "confidence": "low", "signals": [], "processing": processing()}
|
| 204 |
+
|
| 205 |
+
def trailing_forward_pe(self) -> dict[str, object]:
|
| 206 |
+
return {"date": "2026-04-01", "latestValue": 0.0, "history": [], "processing": processing()}
|
| 207 |
+
|
| 208 |
+
def market_breadth(self) -> dict[str, object]:
|
| 209 |
+
return {"metrics": [], "processing": processing()}
|
| 210 |
+
|
| 211 |
+
def watchlist(self) -> dict[str, object]:
|
| 212 |
+
return {"items": [], "count": 0, "processing": processing()}
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def fake_chart_opener(
|
| 216 |
+
data_or_names,
|
| 217 |
+
*,
|
| 218 |
+
session_id: str | None = None,
|
| 219 |
+
**kwargs,
|
| 220 |
+
) -> dict[str, object]:
|
| 221 |
+
_ = kwargs
|
| 222 |
+
return {
|
| 223 |
+
"ok": True,
|
| 224 |
+
"sessionId": session_id or "agent:chart",
|
| 225 |
+
"chartUrl": f"http://127.0.0.1:8001/chart?sessionId={session_id or 'agent:chart'}",
|
| 226 |
+
"processing": processing(),
|
| 227 |
+
"inputEcho": data_or_names,
|
| 228 |
+
}
|