tradingcodex 0.1.0__py3-none-any.whl
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.
- apps/__init__.py +1 -0
- apps/audit/__init__.py +1 -0
- apps/audit/admin.py +6 -0
- apps/audit/apps.py +8 -0
- apps/audit/migrations/0001_initial.py +35 -0
- apps/audit/migrations/__init__.py +0 -0
- apps/audit/models.py +22 -0
- apps/harness/__init__.py +1 -0
- apps/harness/admin.py +6 -0
- apps/harness/apps.py +8 -0
- apps/harness/migrations/0001_initial.py +35 -0
- apps/harness/migrations/__init__.py +0 -0
- apps/harness/models.py +22 -0
- apps/harness/templatetags/__init__.py +1 -0
- apps/integrations/__init__.py +1 -0
- apps/integrations/admin.py +6 -0
- apps/integrations/apps.py +8 -0
- apps/integrations/migrations/0001_initial.py +29 -0
- apps/integrations/migrations/__init__.py +0 -0
- apps/integrations/models.py +16 -0
- apps/integrations/services.py +31 -0
- apps/mcp/__init__.py +1 -0
- apps/mcp/admin.py +20 -0
- apps/mcp/apps.py +8 -0
- apps/mcp/migrations/0001_initial.py +168 -0
- apps/mcp/migrations/__init__.py +0 -0
- apps/mcp/models.py +154 -0
- apps/mcp/services.py +327 -0
- apps/orders/__init__.py +1 -0
- apps/orders/admin.py +6 -0
- apps/orders/apps.py +8 -0
- apps/orders/migrations/0001_initial.py +79 -0
- apps/orders/migrations/__init__.py +0 -0
- apps/orders/models.py +66 -0
- apps/orders/services.py +107 -0
- apps/policy/__init__.py +1 -0
- apps/policy/admin.py +6 -0
- apps/policy/apps.py +8 -0
- apps/policy/migrations/0001_initial.py +75 -0
- apps/policy/migrations/__init__.py +0 -0
- apps/policy/models.py +61 -0
- apps/policy/services.py +110 -0
- apps/portfolio/__init__.py +1 -0
- apps/portfolio/admin.py +6 -0
- apps/portfolio/apps.py +8 -0
- apps/portfolio/migrations/0001_initial.py +67 -0
- apps/portfolio/migrations/__init__.py +0 -0
- apps/portfolio/models.py +53 -0
- apps/research/__init__.py +1 -0
- apps/research/admin.py +1 -0
- apps/research/apps.py +8 -0
- apps/research/migrations/__init__.py +0 -0
- apps/research/models.py +1 -0
- apps/workflows/__init__.py +1 -0
- apps/workflows/admin.py +6 -0
- apps/workflows/apps.py +8 -0
- apps/workflows/migrations/0001_initial.py +51 -0
- apps/workflows/migrations/__init__.py +0 -0
- apps/workflows/models.py +44 -0
- tradingcodex-0.1.0.dist-info/METADATA +337 -0
- tradingcodex-0.1.0.dist-info/RECORD +254 -0
- tradingcodex-0.1.0.dist-info/WHEEL +5 -0
- tradingcodex-0.1.0.dist-info/entry_points.txt +2 -0
- tradingcodex-0.1.0.dist-info/licenses/LICENSE +202 -0
- tradingcodex-0.1.0.dist-info/licenses/NOTICE +24 -0
- tradingcodex-0.1.0.dist-info/top_level.txt +4 -0
- tradingcodex_cli/__init__.py +1 -0
- tradingcodex_cli/__main__.py +124 -0
- tradingcodex_cli/commands/__init__.py +2 -0
- tradingcodex_cli/commands/bootstrap.py +157 -0
- tradingcodex_cli/commands/db.py +36 -0
- tradingcodex_cli/commands/doctor.py +230 -0
- tradingcodex_cli/commands/mcp.py +186 -0
- tradingcodex_cli/commands/orders.py +89 -0
- tradingcodex_cli/commands/policy.py +21 -0
- tradingcodex_cli/commands/profile.py +110 -0
- tradingcodex_cli/commands/research.py +76 -0
- tradingcodex_cli/commands/skills.py +93 -0
- tradingcodex_cli/commands/strategies.py +67 -0
- tradingcodex_cli/commands/subagents.py +106 -0
- tradingcodex_cli/commands/utils.py +134 -0
- tradingcodex_cli/commands/workspaces.py +53 -0
- tradingcodex_cli/generator.py +234 -0
- tradingcodex_cli/mcp_stdio.py +26 -0
- tradingcodex_cli/service_autostart.py +127 -0
- tradingcodex_service/__init__.py +5 -0
- tradingcodex_service/admin.py +1 -0
- tradingcodex_service/api.py +486 -0
- tradingcodex_service/application/__init__.py +2 -0
- tradingcodex_service/application/agents.py +1470 -0
- tradingcodex_service/application/audit.py +71 -0
- tradingcodex_service/application/common.py +88 -0
- tradingcodex_service/application/components.py +299 -0
- tradingcodex_service/application/harness.py +747 -0
- tradingcodex_service/application/markdown_preview.py +179 -0
- tradingcodex_service/application/orders.py +404 -0
- tradingcodex_service/application/policy.py +150 -0
- tradingcodex_service/application/portfolio.py +166 -0
- tradingcodex_service/application/research.py +356 -0
- tradingcodex_service/application/runtime.py +321 -0
- tradingcodex_service/asgi.py +6 -0
- tradingcodex_service/mcp_http.py +59 -0
- tradingcodex_service/mcp_runtime.py +565 -0
- tradingcodex_service/settings.py +91 -0
- tradingcodex_service/templates/web/activity.html +24 -0
- tradingcodex_service/templates/web/agent_skills.html +111 -0
- tradingcodex_service/templates/web/agents.html +121 -0
- tradingcodex_service/templates/web/base.html +150 -0
- tradingcodex_service/templates/web/dashboard.html +109 -0
- tradingcodex_service/templates/web/fragments/role_inspector.html +81 -0
- tradingcodex_service/templates/web/fragments/starter_prompt.html +24 -0
- tradingcodex_service/templates/web/fragments/topology_canvas.html +85 -0
- tradingcodex_service/templates/web/harness.html +87 -0
- tradingcodex_service/templates/web/mcp_router.html +250 -0
- tradingcodex_service/templates/web/orders.html +68 -0
- tradingcodex_service/templates/web/policy.html +81 -0
- tradingcodex_service/templates/web/portfolio.html +52 -0
- tradingcodex_service/templates/web/research.html +73 -0
- tradingcodex_service/templates/web/starter_prompt.html +40 -0
- tradingcodex_service/templates/web/strategies.html +74 -0
- tradingcodex_service/urls.py +42 -0
- tradingcodex_service/version.py +1 -0
- tradingcodex_service/web.py +885 -0
- tradingcodex_service/wsgi.py +6 -0
- workspace_templates/__init__.py +1 -0
- workspace_templates/modules/audit/files/.tradingcodex/audit/README.md +5 -0
- workspace_templates/modules/audit/files/trading/audit/.gitkeep +1 -0
- workspace_templates/modules/audit/module.json +16 -0
- workspace_templates/modules/codex-base/files/.codex/config.toml +272 -0
- workspace_templates/modules/codex-base/files/.codex/hooks/tradingcodex_hook.py +173 -0
- workspace_templates/modules/codex-base/files/.codex/hooks.json +105 -0
- workspace_templates/modules/codex-base/files/.codex/prompts/base_instructions/head-manager.md +129 -0
- workspace_templates/modules/codex-base/files/.codex/rules/tradingcodex.rules +50 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/capabilities.yaml +56 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/cli.py +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/config.yaml +53 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/policy-bindings.yaml +16 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/principals.yaml +17 -0
- workspace_templates/modules/codex-base/files/.tradingcodex/policies/roles.yaml +27 -0
- workspace_templates/modules/codex-base/files/AGENTS.md +56 -0
- workspace_templates/modules/codex-base/files/pyproject.toml +13 -0
- workspace_templates/modules/codex-base/files/tcx +35 -0
- workspace_templates/modules/codex-base/module.json +17 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/access-policies.yaml +39 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/restricted-list.yaml +6 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/approval_receipt.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/audit_event.schema.json +11 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/evidence_pack.schema.json +16 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/execution_result.schema.json +12 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/fundamental_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/news_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/order_intent.schema.json +30 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/portfolio_review.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/postmortem_report.schema.json +14 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/risk_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/technical_report.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/thesis.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/valuation.schema.json +15 -0
- workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/scripts/validate-order-intent.py +15 -0
- workspace_templates/modules/enforcement-guardrails/module.json +19 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/execution-operator.toml +71 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/fundamental-analyst.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/instrument-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/macro-analyst.toml +64 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/news-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/portfolio-manager.toml +62 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/risk-manager.toml +65 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/technical-analyst.toml +63 -0
- workspace_templates/modules/fixed-subagents/files/.codex/agents/valuation-analyst.toml +59 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/head-manager.yaml +66 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/skill-change-proposals/.gitkeep +1 -0
- workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/subagent-registry.yaml +56 -0
- workspace_templates/modules/fixed-subagents/module.json +23 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/guardrails.md +17 -0
- workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/task-quality-checklist.md +37 -0
- workspace_templates/modules/guidance-guardrails/module.json +18 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/policies/information-barriers.yaml +211 -0
- workspace_templates/modules/information-barriers/files/.tradingcodex/secrets.md +9 -0
- workspace_templates/modules/information-barriers/files/trading/approvals/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/market-data/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/approved/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/draft/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/executed/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/orders/rejected/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/fundamental/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/instrument/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/macro/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/news/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/policy/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/portfolio/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/postmortem/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/risk/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/technical/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/reports/valuation/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/files/trading/research/.gitkeep +1 -0
- workspace_templates/modules/information-barriers/module.json +19 -0
- workspace_templates/modules/paper-trading/files/.tradingcodex/mcp/adapters/paper-trading.py +4 -0
- workspace_templates/modules/paper-trading/module.json +16 -0
- workspace_templates/modules/postmortem/files/.tradingcodex/workflows/postmortem.yaml +12 -0
- workspace_templates/modules/postmortem/module.json +16 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/SKILL.md +106 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/SKILL.md +101 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/SKILL.md +140 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/SKILL.md +31 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/SKILL.md +138 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/SKILL.md +109 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/SKILL.md +54 -0
- workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/SKILL.md +35 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/SKILL.md +40 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/SKILL.md +44 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/SKILL.md +38 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/SKILL.md +45 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/SKILL.md +46 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/SKILL.md +66 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/SKILL.md +43 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/SKILL.md +47 -0
- workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/agents/openai.yaml +6 -0
- workspace_templates/modules/repo-skills/module.json +37 -0
- workspace_templates/modules/stub-execution/files/.tradingcodex/mcp/adapters/stub-execution.py +4 -0
- workspace_templates/modules/stub-execution/module.json +15 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/adapters/live-adapter.contract.md +25 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/enforcer/README.md +5 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/gateway/README.md +8 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/server.py +27 -0
- workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/smoke-call.py +18 -0
- workspace_templates/modules/tradingcodex-mcp/module.json +24 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tradingcodex_service.application.audit import write_policy_decision_if_available
|
|
9
|
+
from tradingcodex_service.application.common import _number, _safe_read
|
|
10
|
+
from tradingcodex_service.application.runtime import ensure_runtime_database, workspace_context_payload
|
|
11
|
+
|
|
12
|
+
DEFAULT_MAX_SINGLE_ORDER_KRW = 100_000_000
|
|
13
|
+
DEFAULT_ALLOWED_ADAPTERS = {"stub-execution", "paper-trading"}
|
|
14
|
+
EXPLICIT_DENY_ACTIONS = {
|
|
15
|
+
"api_key.read",
|
|
16
|
+
"api_key.rotate",
|
|
17
|
+
"secret.read",
|
|
18
|
+
"broker.raw_api",
|
|
19
|
+
"broker_api.direct_call",
|
|
20
|
+
"approval.self_issue",
|
|
21
|
+
"approval_receipt.self_issue",
|
|
22
|
+
"execute_order",
|
|
23
|
+
"order.execute",
|
|
24
|
+
"trade.execute",
|
|
25
|
+
"trading.execute",
|
|
26
|
+
"cash.withdraw",
|
|
27
|
+
"cash.transfer",
|
|
28
|
+
"permissions.write",
|
|
29
|
+
"policy.write",
|
|
30
|
+
"mcp.tradingcodex.write_policy_and_execute",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class RuntimePolicy:
|
|
35
|
+
max_single_order_krw: int = DEFAULT_MAX_SINGLE_ORDER_KRW
|
|
36
|
+
allowed_adapters: frozenset[str] = frozenset(DEFAULT_ALLOWED_ADAPTERS)
|
|
37
|
+
source: tuple[str, ...] = ("default-runtime-policy",)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_runtime_policy(workspace_root: Path | str) -> RuntimePolicy:
|
|
41
|
+
root = Path(workspace_root)
|
|
42
|
+
max_single_order = DEFAULT_MAX_SINGLE_ORDER_KRW
|
|
43
|
+
allowed_adapters = set(DEFAULT_ALLOWED_ADAPTERS)
|
|
44
|
+
source = [".tradingcodex/policies/access-policies.yaml", ".tradingcodex/config.yaml"]
|
|
45
|
+
|
|
46
|
+
access_text = _safe_read(root / ".tradingcodex" / "policies" / "access-policies.yaml")
|
|
47
|
+
max_match = re.search(r"order\.estimated_notional_krw\s*<=\s*(\d+)", access_text)
|
|
48
|
+
if max_match:
|
|
49
|
+
max_single_order = int(max_match.group(1))
|
|
50
|
+
brokers_match = re.search(r"order\.broker\s+in\s+\[([^\]]+)\]", access_text)
|
|
51
|
+
if brokers_match:
|
|
52
|
+
parsed = re.findall(r'"([^"]+)"', brokers_match.group(1))
|
|
53
|
+
if parsed:
|
|
54
|
+
allowed_adapters = set(parsed)
|
|
55
|
+
|
|
56
|
+
config_text = _safe_read(root / ".tradingcodex" / "config.yaml")
|
|
57
|
+
section = re.search(r"enabled_adapters:[ \t]*\n((?:[ \t]*-[ \t]*[A-Za-z0-9._-]+[ \t]*(?:\n|$))+)", config_text)
|
|
58
|
+
if section:
|
|
59
|
+
configured = set(re.findall(r"^[ \t]*-[ \t]*([A-Za-z0-9._-]+)[ \t]*$", section.group(1), flags=re.M))
|
|
60
|
+
if configured:
|
|
61
|
+
allowed_adapters &= configured
|
|
62
|
+
|
|
63
|
+
return RuntimePolicy(max_single_order, frozenset(allowed_adapters), tuple(source))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_restricted_symbols(workspace_root: Path | str) -> set[str]:
|
|
67
|
+
text = _safe_read(Path(workspace_root) / ".tradingcodex" / "policies" / "restricted-list.yaml")
|
|
68
|
+
symbols: set[str] = set()
|
|
69
|
+
try:
|
|
70
|
+
ensure_runtime_database(workspace_root)
|
|
71
|
+
from apps.policy.models import RestrictedSymbol
|
|
72
|
+
|
|
73
|
+
symbols.update(symbol.upper() for symbol in RestrictedSymbol.objects.filter(active=True).values_list("symbol", flat=True))
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
inline = re.search(r"restricted_symbols\s*:\s*\[([^\]]*)\]", text)
|
|
77
|
+
if inline:
|
|
78
|
+
for raw in inline.group(1).split(","):
|
|
79
|
+
symbol = raw.strip().strip("'\"")
|
|
80
|
+
if symbol:
|
|
81
|
+
symbols.add(symbol.upper())
|
|
82
|
+
block = re.search(r"restricted_symbols\s*:\s*\n((?:[ \t]*-[ \t]*[A-Za-z0-9_.:-]+[ \t]*(?:\n|$))+)", text)
|
|
83
|
+
if block:
|
|
84
|
+
symbols.update(symbol.upper() for symbol in re.findall(r"^[ \t]*-[ \t]*([A-Za-z0-9_.:-]+)[ \t]*$", block.group(1), flags=re.M))
|
|
85
|
+
symbols.update(symbol.upper() for symbol in re.findall(r"\bsymbol\s*:\s*['\"]?([A-Za-z0-9_.:-]+)['\"]?", text))
|
|
86
|
+
return symbols
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def evaluate_policy(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
90
|
+
ensure_runtime_database(workspace_root)
|
|
91
|
+
from apps.policy.services import capability_check, sync_builtin_principals_and_capabilities
|
|
92
|
+
from tradingcodex_service.application.orders import resolve_approval_receipt, resolve_order_intent
|
|
93
|
+
|
|
94
|
+
sync_builtin_principals_and_capabilities()
|
|
95
|
+
policy = read_runtime_policy(workspace_root)
|
|
96
|
+
order = resolve_order_intent(Path(workspace_root), args)
|
|
97
|
+
receipt = resolve_approval_receipt(Path(workspace_root), args, order)
|
|
98
|
+
principal_id = args.get("principal_id") or "unknown"
|
|
99
|
+
action = args.get("action") or "unknown"
|
|
100
|
+
reasons: list[str] = []
|
|
101
|
+
capability_allowed, capability_reasons = capability_check(principal_id, action, args.get("resource"))
|
|
102
|
+
if not capability_allowed:
|
|
103
|
+
reasons.extend(capability_reasons)
|
|
104
|
+
|
|
105
|
+
if action in EXPLICIT_DENY_ACTIONS:
|
|
106
|
+
reasons.append(f"explicit deny action: {action}")
|
|
107
|
+
if action.startswith(("broker_api.", "broker.")):
|
|
108
|
+
reasons.append("direct broker API actions are explicitly denied")
|
|
109
|
+
if "live" in action.lower() and re.search(r"order|execution|submit|broker", action.lower()):
|
|
110
|
+
reasons.append("live execution actions are disabled in the initial core")
|
|
111
|
+
if "live" in str(args.get("resource") or "").lower() and re.search(r"order|execution|submit|broker", action.lower()):
|
|
112
|
+
reasons.append("live execution resources are disabled in the initial core")
|
|
113
|
+
if action in {"approval.create", "approval_receipt.create"} and principal_id != "risk-manager":
|
|
114
|
+
reasons.append("only risk-manager can create approval receipts")
|
|
115
|
+
if action == "mcp.tradingcodex.submit_approved_order" and principal_id != "execution-operator":
|
|
116
|
+
reasons.append("only execution-operator can submit approved orders")
|
|
117
|
+
if order.get("broker") == "live" and not (Path(workspace_root) / ".tradingcodex" / "mcp" / "adapters" / "live.py").exists():
|
|
118
|
+
reasons.append("live broker adapter is not installed in this workspace")
|
|
119
|
+
if order.get("broker") and order["broker"] not in policy.allowed_adapters:
|
|
120
|
+
reasons.append(f"adapter not enabled: {order['broker']}")
|
|
121
|
+
|
|
122
|
+
notional = _number(order.get("estimated_notional_krw"))
|
|
123
|
+
if order.get("estimated_notional_krw") not in (None, "") and (notional is None or notional <= 0):
|
|
124
|
+
reasons.append("estimated_notional_krw must be a positive number")
|
|
125
|
+
elif notional is not None and notional > policy.max_single_order_krw:
|
|
126
|
+
reasons.append(f"estimated_notional_krw exceeds {policy.max_single_order_krw}")
|
|
127
|
+
|
|
128
|
+
if order.get("symbol") and str(order["symbol"]).upper() in read_restricted_symbols(workspace_root):
|
|
129
|
+
reasons.append(f"symbol is restricted: {order['symbol']}")
|
|
130
|
+
if args.get("require_approval_check") and receipt.get("valid") is not True:
|
|
131
|
+
reasons.append("approval_receipt.valid == false")
|
|
132
|
+
|
|
133
|
+
decision = "allow" if not reasons else "deny"
|
|
134
|
+
result = {
|
|
135
|
+
"decision": decision,
|
|
136
|
+
"reasons": reasons,
|
|
137
|
+
"enforced_by": ["TradingCodex MCP"],
|
|
138
|
+
"policy_source": list(policy.source),
|
|
139
|
+
"principal_id": principal_id,
|
|
140
|
+
"action": action,
|
|
141
|
+
"resource": args.get("resource"),
|
|
142
|
+
"db_canonical": True,
|
|
143
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
144
|
+
}
|
|
145
|
+
write_policy_decision_if_available(workspace_root, result)
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def simulate_policy(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
return evaluate_policy(workspace_root, args)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from tradingcodex_service.application.common import now_iso
|
|
7
|
+
from tradingcodex_service.application.runtime import active_profile_for_workspace, ensure_runtime_database, workspace_context_payload
|
|
8
|
+
|
|
9
|
+
DEFAULT_PAPER_CASH_KRW = 100_000_000
|
|
10
|
+
DEFAULT_PORTFOLIO_ID = "default-paper"
|
|
11
|
+
DEFAULT_ACCOUNT_ID = "local-paper"
|
|
12
|
+
DEFAULT_STRATEGY_ID = "default-strategy"
|
|
13
|
+
|
|
14
|
+
def submit_paper_order(root: Path, order: dict[str, Any]) -> dict[str, Any]:
|
|
15
|
+
portfolio_id, account_id, strategy_id = portfolio_keys(order, root)
|
|
16
|
+
state = load_paper_portfolio_state(root, portfolio_id, account_id, strategy_id)
|
|
17
|
+
symbol = str(order["symbol"]).upper()
|
|
18
|
+
quantity = float(order["quantity"])
|
|
19
|
+
price = float(order["limit_price"])
|
|
20
|
+
notional = quantity * price
|
|
21
|
+
current = state.setdefault("positions", {}).get(symbol, {"quantity": 0, "average_price": 0, "currency": order.get("currency", "KRW")})
|
|
22
|
+
if order["side"] == "buy":
|
|
23
|
+
if float(state.get("cash_krw", 0)) < notional:
|
|
24
|
+
raise ValueError(f"insufficient paper cash: required {notional}, available {state.get('cash_krw', 0)}")
|
|
25
|
+
next_quantity = float(current.get("quantity", 0)) + quantity
|
|
26
|
+
current["average_price"] = 0 if next_quantity == 0 else ((float(current.get("quantity", 0)) * float(current.get("average_price", 0))) + notional) / next_quantity
|
|
27
|
+
current["quantity"] = next_quantity
|
|
28
|
+
state["cash_krw"] = float(state.get("cash_krw", 0)) - notional
|
|
29
|
+
else:
|
|
30
|
+
if float(current.get("quantity", 0)) < quantity:
|
|
31
|
+
raise ValueError(f"insufficient paper position: required {quantity}, available {current.get('quantity', 0)}")
|
|
32
|
+
current["quantity"] = float(current.get("quantity", 0)) - quantity
|
|
33
|
+
state["cash_krw"] = float(state.get("cash_krw", 0)) + notional
|
|
34
|
+
if current["quantity"] == 0:
|
|
35
|
+
current["average_price"] = 0
|
|
36
|
+
state["positions"][symbol] = current
|
|
37
|
+
state["updated_at"] = now_iso()
|
|
38
|
+
persist_paper_portfolio_state(root, state, portfolio_id, account_id, strategy_id, source="paper-trading")
|
|
39
|
+
return {
|
|
40
|
+
"adapter": "paper-trading",
|
|
41
|
+
"broker_order_id": f"paper-{order['id']}",
|
|
42
|
+
"status": "filled",
|
|
43
|
+
"filled_quantity": quantity,
|
|
44
|
+
"average_price": price,
|
|
45
|
+
"submitted_at": state["updated_at"],
|
|
46
|
+
"portfolio_id": portfolio_id,
|
|
47
|
+
"account_id": account_id,
|
|
48
|
+
"strategy_id": strategy_id,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def portfolio_keys(args: dict[str, Any], workspace_root: Path | str | None = None) -> tuple[str, str, str]:
|
|
53
|
+
profile = active_profile_for_workspace(workspace_root)
|
|
54
|
+
return (
|
|
55
|
+
str(args.get("portfolio_id") or profile.get("portfolio_id") or DEFAULT_PORTFOLIO_ID),
|
|
56
|
+
str(args.get("account_id") or profile.get("account_id") or DEFAULT_ACCOUNT_ID),
|
|
57
|
+
str(args.get("strategy_id") or profile.get("strategy_id") or DEFAULT_STRATEGY_ID),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def default_paper_portfolio_state(portfolio_id: str = DEFAULT_PORTFOLIO_ID, account_id: str = DEFAULT_ACCOUNT_ID, strategy_id: str = DEFAULT_STRATEGY_ID) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"cash_krw": DEFAULT_PAPER_CASH_KRW,
|
|
64
|
+
"positions": {},
|
|
65
|
+
"updated_at": now_iso(),
|
|
66
|
+
"portfolio_id": portfolio_id,
|
|
67
|
+
"account_id": account_id,
|
|
68
|
+
"strategy_id": strategy_id,
|
|
69
|
+
"source": "central-db",
|
|
70
|
+
"db_canonical": True,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_paper_portfolio_state(
|
|
75
|
+
workspace_root: Path | str | None = None,
|
|
76
|
+
portfolio_id: str = DEFAULT_PORTFOLIO_ID,
|
|
77
|
+
account_id: str = DEFAULT_ACCOUNT_ID,
|
|
78
|
+
strategy_id: str = DEFAULT_STRATEGY_ID,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
ensure_runtime_database(workspace_root)
|
|
81
|
+
from apps.portfolio.models import PortfolioSnapshot
|
|
82
|
+
|
|
83
|
+
snapshot = (
|
|
84
|
+
PortfolioSnapshot.objects.filter(
|
|
85
|
+
portfolio_id=portfolio_id,
|
|
86
|
+
account_id=account_id,
|
|
87
|
+
strategy_id=strategy_id,
|
|
88
|
+
source="paper-trading",
|
|
89
|
+
)
|
|
90
|
+
.order_by("-created_at", "-id")
|
|
91
|
+
.first()
|
|
92
|
+
)
|
|
93
|
+
if snapshot is None:
|
|
94
|
+
state = default_paper_portfolio_state(portfolio_id, account_id, strategy_id)
|
|
95
|
+
state["workspace_context"] = workspace_context_payload(workspace_root)
|
|
96
|
+
persist_paper_portfolio_state(workspace_root, state, portfolio_id, account_id, strategy_id, source="paper-trading")
|
|
97
|
+
return state
|
|
98
|
+
state = dict(snapshot.payload or {})
|
|
99
|
+
state.setdefault("cash_krw", 0)
|
|
100
|
+
state.setdefault("positions", {})
|
|
101
|
+
state.setdefault("updated_at", snapshot.created_at.isoformat())
|
|
102
|
+
state.update({
|
|
103
|
+
"portfolio_id": portfolio_id,
|
|
104
|
+
"account_id": account_id,
|
|
105
|
+
"strategy_id": strategy_id,
|
|
106
|
+
"source": "central-db",
|
|
107
|
+
"db_canonical": True,
|
|
108
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
109
|
+
})
|
|
110
|
+
return state
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def persist_paper_portfolio_state(
|
|
114
|
+
workspace_root: Path | str | None,
|
|
115
|
+
state: dict[str, Any],
|
|
116
|
+
portfolio_id: str,
|
|
117
|
+
account_id: str,
|
|
118
|
+
strategy_id: str,
|
|
119
|
+
source: str = "paper-trading",
|
|
120
|
+
) -> None:
|
|
121
|
+
ensure_runtime_database(workspace_root)
|
|
122
|
+
from apps.portfolio.models import CashBalance, PortfolioSnapshot, Position
|
|
123
|
+
|
|
124
|
+
state = dict(state)
|
|
125
|
+
state.update({
|
|
126
|
+
"portfolio_id": portfolio_id,
|
|
127
|
+
"account_id": account_id,
|
|
128
|
+
"strategy_id": strategy_id,
|
|
129
|
+
"source": "central-db",
|
|
130
|
+
"db_canonical": True,
|
|
131
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
132
|
+
})
|
|
133
|
+
snapshot = PortfolioSnapshot.objects.create(
|
|
134
|
+
source=source,
|
|
135
|
+
portfolio_id=portfolio_id,
|
|
136
|
+
account_id=account_id,
|
|
137
|
+
strategy_id=strategy_id,
|
|
138
|
+
workspace_context=workspace_context_payload(workspace_root),
|
|
139
|
+
payload=state,
|
|
140
|
+
)
|
|
141
|
+
CashBalance.objects.create(
|
|
142
|
+
snapshot=snapshot,
|
|
143
|
+
currency="KRW",
|
|
144
|
+
amount=state.get("cash_krw", 0),
|
|
145
|
+
portfolio_id=portfolio_id,
|
|
146
|
+
account_id=account_id,
|
|
147
|
+
strategy_id=strategy_id,
|
|
148
|
+
)
|
|
149
|
+
for symbol, position in sorted((state.get("positions") or {}).items()):
|
|
150
|
+
if float(position.get("quantity", 0)) == 0:
|
|
151
|
+
continue
|
|
152
|
+
Position.objects.create(
|
|
153
|
+
snapshot=snapshot,
|
|
154
|
+
symbol=str(symbol).upper(),
|
|
155
|
+
quantity=position.get("quantity", 0),
|
|
156
|
+
average_price=position.get("average_price", 0),
|
|
157
|
+
currency=position.get("currency") or "KRW",
|
|
158
|
+
portfolio_id=portfolio_id,
|
|
159
|
+
account_id=account_id,
|
|
160
|
+
strategy_id=strategy_id,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def list_positions(workspace_root: Path | str) -> dict[str, Any]:
|
|
165
|
+
portfolio_id, account_id, strategy_id = portfolio_keys({}, workspace_root)
|
|
166
|
+
return load_paper_portfolio_state(Path(workspace_root), portfolio_id, account_id, strategy_id)
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from tradingcodex_service.application.common import _resolve_path, sanitize_id
|
|
10
|
+
from tradingcodex_service.application.markdown_preview import split_markdown_frontmatter
|
|
11
|
+
from tradingcodex_service.application.runtime import workspace_context_payload
|
|
12
|
+
|
|
13
|
+
RESEARCH_FILE_ROOTS = (Path("trading/research"), Path("trading/reports"))
|
|
14
|
+
SOURCE_SNAPSHOT_ROOT = Path("trading/research/source-snapshots")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_workflow_artifacts(workspace_root: Path | str) -> dict[str, Any]:
|
|
18
|
+
root = Path(workspace_root)
|
|
19
|
+
files = []
|
|
20
|
+
for prefix in ["trading/research", "trading/reports", "trading/orders", "trading/approvals"]:
|
|
21
|
+
base = root / prefix
|
|
22
|
+
if base.exists():
|
|
23
|
+
files.extend(str(path.relative_to(root)) for path in base.rglob("*") if path.is_file() and path.name != ".gitkeep")
|
|
24
|
+
return {
|
|
25
|
+
"artifacts": sorted(files),
|
|
26
|
+
"research_artifacts": list_research_artifacts(root, {"include_markdown": False}).get("artifacts", []),
|
|
27
|
+
"workspace_native": True,
|
|
28
|
+
"workspace_context": workspace_context_payload(root),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_research_artifact(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
33
|
+
root = Path(workspace_root)
|
|
34
|
+
markdown = args.get("markdown")
|
|
35
|
+
markdown_path = args.get("markdown_path") or args.get("markdown_file")
|
|
36
|
+
if not markdown and markdown_path:
|
|
37
|
+
markdown = _resolve_path(root, markdown_path).read_text(encoding="utf-8")
|
|
38
|
+
if not markdown:
|
|
39
|
+
raise ValueError("research artifact markdown is required")
|
|
40
|
+
|
|
41
|
+
source_document = split_markdown_frontmatter(str(markdown))
|
|
42
|
+
source_frontmatter = source_document.frontmatter
|
|
43
|
+
markdown_body = source_document.body or str(markdown)
|
|
44
|
+
artifact_type = str(args.get("artifact_type") or args.get("type") or source_frontmatter.get("artifact_type") or source_frontmatter.get("type") or "research_memo")
|
|
45
|
+
title = str(args.get("title") or source_frontmatter.get("title") or source_document.heading or args.get("artifact_id") or "Untitled research artifact")
|
|
46
|
+
symbol = str(args.get("symbol") or source_frontmatter.get("symbol") or "").upper()
|
|
47
|
+
content_hash = hashlib.sha256(markdown_body.encode("utf-8")).hexdigest()
|
|
48
|
+
artifact_id = str(args.get("artifact_id") or source_frontmatter.get("artifact_id") or f"{sanitize_id(artifact_type)}-{sanitize_id(symbol or title)}-{content_hash[:12]}")
|
|
49
|
+
metadata = args.get("metadata") if isinstance(args.get("metadata"), dict) else {}
|
|
50
|
+
created_by = str(args.get("created_by") or args.get("principal_id") or source_frontmatter.get("created_by") or "system")
|
|
51
|
+
existing = find_workspace_research_artifact(root, artifact_id)
|
|
52
|
+
if existing and existing.get("content_hash") != content_hash and not args.get("_append_version"):
|
|
53
|
+
raise ValueError("research artifact already exists in this workspace; use append_research_artifact_version to create a new version")
|
|
54
|
+
|
|
55
|
+
existing_version = _int_value(existing.get("version") if existing else None, default=0)
|
|
56
|
+
version = existing_version + 1 if args.get("_append_version") else existing_version or 1
|
|
57
|
+
export_path = str(args.get("export_path") or (existing.get("path") if existing else "") or default_research_export_path_from_values(artifact_id, artifact_type, metadata))
|
|
58
|
+
frontmatter = {
|
|
59
|
+
**source_frontmatter,
|
|
60
|
+
"artifact_id": artifact_id,
|
|
61
|
+
"artifact_type": artifact_type,
|
|
62
|
+
"universe": args.get("universe") or source_frontmatter.get("universe") or "public_equity",
|
|
63
|
+
"workflow_type": args.get("workflow_type") or source_frontmatter.get("workflow_type") or "",
|
|
64
|
+
"role": args.get("role") or metadata.get("role") or source_frontmatter.get("role") or _role_alias_from_actor(created_by),
|
|
65
|
+
"symbol": symbol,
|
|
66
|
+
"title": title,
|
|
67
|
+
"source_as_of": args.get("source_as_of") or source_frontmatter.get("source_as_of") or "",
|
|
68
|
+
"readiness_label": args.get("readiness_label") or source_frontmatter.get("readiness_label") or "",
|
|
69
|
+
"version": version,
|
|
70
|
+
"content_hash": content_hash,
|
|
71
|
+
"workspace_native": True,
|
|
72
|
+
"created_by": created_by,
|
|
73
|
+
}
|
|
74
|
+
path = _resolve_path(root, export_path)
|
|
75
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
path.write_text(_render_research_markdown(frontmatter, markdown_body), encoding="utf-8")
|
|
77
|
+
|
|
78
|
+
result = {
|
|
79
|
+
"status": "updated" if existing else "stored",
|
|
80
|
+
"db_canonical": False,
|
|
81
|
+
"file_sot": True,
|
|
82
|
+
"workspace_native": True,
|
|
83
|
+
"artifact_id": artifact_id,
|
|
84
|
+
"version": version,
|
|
85
|
+
"content_hash": content_hash,
|
|
86
|
+
"export_path": path.relative_to(root).as_posix(),
|
|
87
|
+
"workspace_context": workspace_context_payload(root),
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def append_research_artifact_version(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
93
|
+
if not args.get("artifact_id"):
|
|
94
|
+
raise ValueError("artifact_id is required")
|
|
95
|
+
current = get_research_artifact(workspace_root, {"artifact_id": args["artifact_id"]})
|
|
96
|
+
payload = {
|
|
97
|
+
**current,
|
|
98
|
+
**args,
|
|
99
|
+
"_append_version": True,
|
|
100
|
+
"metadata": args.get("metadata") or current.get("metadata") or {},
|
|
101
|
+
"export_path": args.get("export_path") or current.get("path") or current.get("export_path"),
|
|
102
|
+
}
|
|
103
|
+
if args.get("markdown"):
|
|
104
|
+
payload["markdown"] = args["markdown"]
|
|
105
|
+
elif args.get("markdown_path") or args.get("markdown_file"):
|
|
106
|
+
payload.pop("markdown", None)
|
|
107
|
+
else:
|
|
108
|
+
payload["markdown"] = current.get("markdown")
|
|
109
|
+
return create_research_artifact(workspace_root, payload)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_research_artifact(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
artifact_id = args.get("artifact_id") or args.get("id")
|
|
114
|
+
if not artifact_id:
|
|
115
|
+
raise ValueError("artifact_id is required")
|
|
116
|
+
artifact = find_workspace_research_artifact(Path(workspace_root), str(artifact_id))
|
|
117
|
+
if not artifact:
|
|
118
|
+
raise ValueError(f"research artifact not found in workspace: {artifact_id}")
|
|
119
|
+
if args.get("include_markdown", True) is not False:
|
|
120
|
+
artifact["markdown"] = _read_research_markdown_body(Path(workspace_root) / artifact["path"])
|
|
121
|
+
return artifact
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def list_research_artifacts(workspace_root: Path | str, args: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
125
|
+
args = args or {}
|
|
126
|
+
artifacts = list_workspace_research_artifacts(Path(workspace_root), include_markdown=args.get("include_markdown") is True)
|
|
127
|
+
for field in ["artifact_type", "universe", "workflow_type", "symbol", "readiness_label", "created_by"]:
|
|
128
|
+
value = args.get(field)
|
|
129
|
+
if value:
|
|
130
|
+
artifacts = [artifact for artifact in artifacts if str(artifact.get(field) or "").lower() == str(value).lower()]
|
|
131
|
+
limit = max(1, min(int(args.get("limit") or 50), 200))
|
|
132
|
+
return {
|
|
133
|
+
"db_canonical": False,
|
|
134
|
+
"file_sot": True,
|
|
135
|
+
"workspace_native": True,
|
|
136
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
137
|
+
"artifacts": artifacts[:limit],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def search_research_artifacts(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
query = str(args.get("query") or args.get("q") or "").strip()
|
|
143
|
+
if not query:
|
|
144
|
+
raise ValueError("query is required")
|
|
145
|
+
artifacts = list_workspace_research_artifacts(Path(workspace_root), include_markdown=True)
|
|
146
|
+
query_lower = query.lower()
|
|
147
|
+
artifacts = [
|
|
148
|
+
artifact
|
|
149
|
+
for artifact in artifacts
|
|
150
|
+
if query_lower in str(artifact.get("title") or "").lower()
|
|
151
|
+
or query_lower in str(artifact.get("symbol") or "").lower()
|
|
152
|
+
or query_lower in str(artifact.get("markdown") or "").lower()
|
|
153
|
+
]
|
|
154
|
+
for field in ["universe", "artifact_type"]:
|
|
155
|
+
if args.get(field):
|
|
156
|
+
artifacts = [artifact for artifact in artifacts if str(artifact.get(field) or "").lower() == str(args[field]).lower()]
|
|
157
|
+
limit = max(1, min(int(args.get("limit") or 20), 100))
|
|
158
|
+
for artifact in artifacts:
|
|
159
|
+
artifact.pop("markdown", None)
|
|
160
|
+
return {
|
|
161
|
+
"query": query,
|
|
162
|
+
"db_canonical": False,
|
|
163
|
+
"file_sot": True,
|
|
164
|
+
"workspace_native": True,
|
|
165
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
166
|
+
"artifacts": artifacts[:limit],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def export_research_artifact_md(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
171
|
+
root = Path(workspace_root)
|
|
172
|
+
artifact_id = args.get("artifact_id") or args.get("id")
|
|
173
|
+
if not artifact_id:
|
|
174
|
+
raise ValueError("artifact_id is required")
|
|
175
|
+
artifact = get_research_artifact(root, {"artifact_id": artifact_id, "include_markdown": True})
|
|
176
|
+
target_rel = str(args.get("export_path") or artifact["path"])
|
|
177
|
+
target = _resolve_path(root, target_rel)
|
|
178
|
+
source = root / artifact["path"]
|
|
179
|
+
if target.resolve() != source.resolve():
|
|
180
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
|
|
182
|
+
return {
|
|
183
|
+
"status": "exported",
|
|
184
|
+
"artifact_id": artifact["artifact_id"],
|
|
185
|
+
"export_path": target.relative_to(root).as_posix(),
|
|
186
|
+
"db_canonical": False,
|
|
187
|
+
"file_sot": True,
|
|
188
|
+
"workspace_native": True,
|
|
189
|
+
"workspace_context": workspace_context_payload(root),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def record_source_snapshot(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
|
|
194
|
+
root = Path(workspace_root)
|
|
195
|
+
recorded_at = datetime.now(timezone.utc).isoformat()
|
|
196
|
+
payload = {
|
|
197
|
+
"provider": args.get("provider") or "unknown",
|
|
198
|
+
"source_category": args.get("source_category") or args.get("category") or "unknown",
|
|
199
|
+
"as_of": args.get("as_of") or "",
|
|
200
|
+
"artifact_id": args.get("artifact_id") or "",
|
|
201
|
+
"warnings": args.get("warnings") if isinstance(args.get("warnings"), list) else [],
|
|
202
|
+
"payload": args.get("payload") if isinstance(args.get("payload"), dict) else {},
|
|
203
|
+
"created_by": args.get("principal_id") or args.get("created_by") or "system",
|
|
204
|
+
"recorded_at": recorded_at,
|
|
205
|
+
"workspace_native": True,
|
|
206
|
+
}
|
|
207
|
+
snapshot_id = _source_snapshot_id(payload)
|
|
208
|
+
rel_path = SOURCE_SNAPSHOT_ROOT / f"{snapshot_id}.json"
|
|
209
|
+
path = _resolve_path(root, rel_path)
|
|
210
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
path.write_text(json.dumps({**payload, "snapshot_id": snapshot_id}, indent=2, ensure_ascii=False, sort_keys=True) + "\n", encoding="utf-8")
|
|
212
|
+
result = {
|
|
213
|
+
"status": "recorded",
|
|
214
|
+
"snapshot_id": snapshot_id,
|
|
215
|
+
"artifact_id": payload["artifact_id"],
|
|
216
|
+
"provider": payload["provider"],
|
|
217
|
+
"source_category": payload["source_category"],
|
|
218
|
+
"export_path": path.relative_to(root).as_posix(),
|
|
219
|
+
"db_canonical": False,
|
|
220
|
+
"file_sot": True,
|
|
221
|
+
"workspace_native": True,
|
|
222
|
+
"workspace_context": workspace_context_payload(root),
|
|
223
|
+
}
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def list_workspace_research_artifacts(root: Path, *, include_markdown: bool = False) -> list[dict[str, Any]]:
|
|
228
|
+
records: list[dict[str, Any]] = []
|
|
229
|
+
for rel_root in RESEARCH_FILE_ROOTS:
|
|
230
|
+
base = root / rel_root
|
|
231
|
+
if not base.exists():
|
|
232
|
+
continue
|
|
233
|
+
for path in sorted(base.rglob("*.md")):
|
|
234
|
+
if path.name == ".gitkeep":
|
|
235
|
+
continue
|
|
236
|
+
records.append(_research_file_payload(root, path, include_markdown=include_markdown))
|
|
237
|
+
return sorted(records, key=lambda item: item["updated_at"], reverse=True)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def find_workspace_research_artifact(root: Path, artifact_id: str) -> dict[str, Any] | None:
|
|
241
|
+
direct = root / artifact_id
|
|
242
|
+
if direct.exists() and direct.is_file():
|
|
243
|
+
return _research_file_payload(root, direct, include_markdown=False)
|
|
244
|
+
for artifact in list_workspace_research_artifacts(root, include_markdown=False):
|
|
245
|
+
if artifact["artifact_id"] == artifact_id or artifact["path"] == artifact_id:
|
|
246
|
+
return artifact
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _research_file_payload(root: Path, path: Path, *, include_markdown: bool = False) -> dict[str, Any]:
|
|
251
|
+
rel = path.relative_to(root).as_posix()
|
|
252
|
+
frontmatter, heading, body = _research_file_parts(path)
|
|
253
|
+
content_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
|
254
|
+
artifact_id = str(frontmatter.get("artifact_id") or rel)
|
|
255
|
+
payload = {
|
|
256
|
+
"artifact_id": artifact_id,
|
|
257
|
+
"path": rel,
|
|
258
|
+
"export_path": rel,
|
|
259
|
+
"artifact_type": str(frontmatter.get("artifact_type") or _infer_research_artifact_type(path)),
|
|
260
|
+
"universe": str(frontmatter.get("universe") or _infer_research_universe(path)),
|
|
261
|
+
"workflow_type": str(frontmatter.get("workflow_type") or ""),
|
|
262
|
+
"role": str(frontmatter.get("role") or ""),
|
|
263
|
+
"symbol": str(frontmatter.get("symbol") or ""),
|
|
264
|
+
"title": str(frontmatter.get("title") or heading or path.stem.replace("-", " ").title()),
|
|
265
|
+
"metadata": {},
|
|
266
|
+
"workspace_context": workspace_context_payload(root),
|
|
267
|
+
"source_as_of": str(frontmatter.get("source_as_of") or ""),
|
|
268
|
+
"readiness_label": str(frontmatter.get("readiness_label") or frontmatter.get("handoff_state") or "workspace-file"),
|
|
269
|
+
"created_by": str(frontmatter.get("created_by") or "workspace"),
|
|
270
|
+
"content_hash": str(frontmatter.get("content_hash") or content_hash),
|
|
271
|
+
"version": _int_value(frontmatter.get("version"), default=1),
|
|
272
|
+
"parent_artifact_id": str(frontmatter.get("parent_artifact_id") or ""),
|
|
273
|
+
"updated_at": datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc),
|
|
274
|
+
"created_at": datetime.fromtimestamp(path.stat().st_ctime, tz=timezone.utc),
|
|
275
|
+
"db_canonical": False,
|
|
276
|
+
"file_sot": True,
|
|
277
|
+
"workspace_native": True,
|
|
278
|
+
}
|
|
279
|
+
if include_markdown:
|
|
280
|
+
payload["markdown"] = body
|
|
281
|
+
return payload
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _research_file_parts(path: Path) -> tuple[dict[str, Any], str, str]:
|
|
285
|
+
try:
|
|
286
|
+
text = path.read_text(encoding="utf-8")
|
|
287
|
+
except Exception:
|
|
288
|
+
return {}, "", ""
|
|
289
|
+
document = split_markdown_frontmatter(text)
|
|
290
|
+
return document.frontmatter, document.heading, document.body
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _read_research_markdown_body(path: Path) -> str:
|
|
294
|
+
return _research_file_parts(path)[2]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _render_research_markdown(frontmatter: dict[str, Any], markdown: str) -> str:
|
|
298
|
+
header = "---\n" + "\n".join(f"{key}: {json.dumps(value, ensure_ascii=False)}" for key, value in frontmatter.items()) + "\n---\n\n"
|
|
299
|
+
return header + markdown.rstrip() + "\n"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _int_value(value: Any, *, default: int) -> int:
|
|
303
|
+
try:
|
|
304
|
+
return int(value)
|
|
305
|
+
except Exception:
|
|
306
|
+
return default
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _infer_research_universe(path: Path) -> str:
|
|
310
|
+
text = path.as_posix().lower()
|
|
311
|
+
if "crypto" in text:
|
|
312
|
+
return "public_crypto"
|
|
313
|
+
if "macro" in text:
|
|
314
|
+
return "macro"
|
|
315
|
+
return "workspace"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _infer_research_artifact_type(path: Path) -> str:
|
|
319
|
+
parts = path.as_posix().split("/")
|
|
320
|
+
if "reports" in parts:
|
|
321
|
+
index = parts.index("reports")
|
|
322
|
+
if len(parts) > index + 1:
|
|
323
|
+
return f"{parts[index + 1]}_report"
|
|
324
|
+
return "role_report"
|
|
325
|
+
if path.name.endswith(".evidence.md"):
|
|
326
|
+
return "evidence_pack"
|
|
327
|
+
return "research_handoff"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _role_alias_from_actor(actor: str) -> str:
|
|
331
|
+
return actor.replace("-analyst", "").replace("-manager", "").replace("-operator", "")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _source_snapshot_id(payload: dict[str, Any]) -> str:
|
|
335
|
+
base = "-".join(
|
|
336
|
+
filter(
|
|
337
|
+
None,
|
|
338
|
+
[
|
|
339
|
+
sanitize_id(str(payload.get("provider") or "unknown")),
|
|
340
|
+
sanitize_id(str(payload.get("source_category") or "unknown")),
|
|
341
|
+
sanitize_id(str(payload.get("artifact_id") or "")),
|
|
342
|
+
],
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
digest = hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()[:12]
|
|
346
|
+
return f"{base or 'source-snapshot'}-{digest}"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def default_research_export_path_from_values(artifact_id: str, artifact_type: str, metadata: dict[str, Any]) -> str:
|
|
350
|
+
stem = sanitize_id(artifact_id)
|
|
351
|
+
role = metadata.get("role") if isinstance(metadata, dict) else ""
|
|
352
|
+
if artifact_type == "evidence_pack":
|
|
353
|
+
return f"trading/research/{stem}.evidence.md"
|
|
354
|
+
if role in {"fundamental", "technical", "news", "macro", "instrument", "valuation", "portfolio", "risk", "policy"}:
|
|
355
|
+
return f"trading/reports/{role}/{stem}.md"
|
|
356
|
+
return f"trading/research/{stem}.md"
|