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
apps/mcp/services.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from django.utils import timezone
|
|
8
|
+
from django.db.models import QuerySet
|
|
9
|
+
|
|
10
|
+
from apps.mcp.models import (
|
|
11
|
+
McpExternalTool,
|
|
12
|
+
McpExternalToolCall,
|
|
13
|
+
McpExternalToolPermission,
|
|
14
|
+
McpRouter,
|
|
15
|
+
McpToolDefinition,
|
|
16
|
+
)
|
|
17
|
+
from apps.policy.services import role_for_principal_id
|
|
18
|
+
from tradingcodex_service.application.common import stable_hash
|
|
19
|
+
from tradingcodex_service.application.runtime import workspace_context_payload
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
READ_ONLY_PROXY_MODES = {"read_only", "summary_only"}
|
|
23
|
+
SERVICE_PROXY_MODES = {"service_adapter", "service_path"}
|
|
24
|
+
RESEARCH_ROLES = {
|
|
25
|
+
"head-manager",
|
|
26
|
+
"fundamental-analyst",
|
|
27
|
+
"technical-analyst",
|
|
28
|
+
"news-analyst",
|
|
29
|
+
"macro-analyst",
|
|
30
|
+
"instrument-analyst",
|
|
31
|
+
"valuation-analyst",
|
|
32
|
+
}
|
|
33
|
+
ACCOUNT_READ_ROLES = {"head-manager", "portfolio-manager", "risk-manager", "execution-operator"}
|
|
34
|
+
PORTFOLIO_STATE_ROLES = {"portfolio-manager", "risk-manager"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def set_mcp_tools_enabled(queryset: QuerySet[McpToolDefinition], enabled: bool, actor: str = "admin") -> int:
|
|
38
|
+
count = queryset.update(enabled=enabled)
|
|
39
|
+
_audit("mcp_tool.enabled" if enabled else "mcp_tool.disabled", {"count": count}, actor)
|
|
40
|
+
return count
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def sync_builtin_mcp_registry(actor: str = "admin") -> None:
|
|
44
|
+
from tradingcodex_service.mcp_runtime import sync_mcp_tool_definitions
|
|
45
|
+
|
|
46
|
+
sync_mcp_tool_definitions()
|
|
47
|
+
_audit("mcp_tool_registry.synced", {"source": "builtin"}, actor)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_or_update_router(
|
|
51
|
+
*,
|
|
52
|
+
name: str,
|
|
53
|
+
label: str = "",
|
|
54
|
+
transport: str = "stdio",
|
|
55
|
+
command: str = "",
|
|
56
|
+
url: str = "",
|
|
57
|
+
credential_ref: str = "",
|
|
58
|
+
enabled: bool = False,
|
|
59
|
+
actor: str = "web",
|
|
60
|
+
) -> McpRouter:
|
|
61
|
+
if not name:
|
|
62
|
+
raise ValueError("router name is required")
|
|
63
|
+
router, created = McpRouter.objects.update_or_create(
|
|
64
|
+
name=name,
|
|
65
|
+
defaults={
|
|
66
|
+
"label": label,
|
|
67
|
+
"transport": transport or "stdio",
|
|
68
|
+
"command": command,
|
|
69
|
+
"url": url,
|
|
70
|
+
"credential_ref": credential_ref,
|
|
71
|
+
"enabled": bool(enabled),
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
_audit("external_mcp_router.created" if created else "external_mcp_router.updated", {"router": router.name, "enabled": router.enabled}, actor)
|
|
75
|
+
return router
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def import_external_mcp_discovery(router: McpRouter, discovery_payload: str | dict[str, Any], actor: str = "web") -> dict[str, Any]:
|
|
79
|
+
payload = _coerce_payload(discovery_payload)
|
|
80
|
+
imported: list[McpExternalTool] = []
|
|
81
|
+
for primitive, item in _iter_discovered_primitives(payload):
|
|
82
|
+
imported.append(upsert_external_mcp_tool(router, primitive, item))
|
|
83
|
+
router.last_status = "ok"
|
|
84
|
+
router.last_error = ""
|
|
85
|
+
router.last_checked_at = timezone.now()
|
|
86
|
+
router.save(update_fields=["last_status", "last_error", "last_checked_at", "updated_at"])
|
|
87
|
+
_audit("external_mcp.discovery_imported", {"router": router.name, "count": len(imported)}, actor)
|
|
88
|
+
return {"router": router.name, "imported": len(imported), "tool_ids": [tool.id for tool in imported]}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def upsert_external_mcp_tool(router: McpRouter, primitive: str, item: dict[str, Any]) -> McpExternalTool:
|
|
92
|
+
external_name = str(item.get("name") or item.get("uri") or item.get("id") or "").strip()
|
|
93
|
+
if not external_name:
|
|
94
|
+
raise ValueError("external MCP item is missing name, uri, or id")
|
|
95
|
+
description = str(item.get("description") or item.get("title") or "")
|
|
96
|
+
input_schema = item.get("inputSchema") or item.get("input_schema") or item.get("schema") or {}
|
|
97
|
+
output_schema = item.get("outputSchema") or item.get("output_schema") or {}
|
|
98
|
+
schema_hash = stable_hash({"primitive": primitive, "name": external_name, "description": description, "input_schema": input_schema, "output_schema": output_schema})
|
|
99
|
+
classification = classify_external_mcp_item(external_name, description, input_schema, primitive=primitive)
|
|
100
|
+
tool, created = McpExternalTool.objects.get_or_create(
|
|
101
|
+
router=router,
|
|
102
|
+
primitive=primitive,
|
|
103
|
+
external_name=external_name,
|
|
104
|
+
defaults={
|
|
105
|
+
"description": description,
|
|
106
|
+
"input_schema": input_schema if isinstance(input_schema, dict) else {},
|
|
107
|
+
"output_schema": output_schema if isinstance(output_schema, dict) else {},
|
|
108
|
+
"schema_hash": schema_hash,
|
|
109
|
+
**classification,
|
|
110
|
+
"last_seen_at": timezone.now(),
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
if created:
|
|
114
|
+
return tool
|
|
115
|
+
changed = bool(tool.schema_hash and tool.schema_hash != schema_hash)
|
|
116
|
+
tool.description = description
|
|
117
|
+
tool.input_schema = input_schema if isinstance(input_schema, dict) else {}
|
|
118
|
+
tool.output_schema = output_schema if isinstance(output_schema, dict) else {}
|
|
119
|
+
tool.schema_hash = schema_hash
|
|
120
|
+
tool.last_seen_at = timezone.now()
|
|
121
|
+
if changed:
|
|
122
|
+
tool.enabled = False
|
|
123
|
+
tool.drift_detected = True
|
|
124
|
+
tool.review_status = "schema_changed"
|
|
125
|
+
else:
|
|
126
|
+
for field, value in classification.items():
|
|
127
|
+
if tool.review_status in {"review_required", "auto_classified"} or field in {"category", "risk_level", "sensitivity", "canonical_capability"}:
|
|
128
|
+
setattr(tool, field, value)
|
|
129
|
+
tool.save()
|
|
130
|
+
return tool
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def classify_external_mcp_item(name: str, description: str = "", schema: dict[str, Any] | None = None, *, primitive: str = "tool") -> dict[str, Any]:
|
|
134
|
+
text = " ".join([name, description, json.dumps(schema or {}, sort_keys=True, default=str)]).lower()
|
|
135
|
+
if primitive != "tool":
|
|
136
|
+
return {
|
|
137
|
+
"category": "market_data" if primitive == "resource" else "workflow_prompt",
|
|
138
|
+
"risk_level": "read",
|
|
139
|
+
"sensitivity": "public",
|
|
140
|
+
"canonical_capability": "market_data.read" if primitive == "resource" else "workflow.prompt.read",
|
|
141
|
+
"proxy_mode": "read_only",
|
|
142
|
+
"allowed_roles": sorted(RESEARCH_ROLES),
|
|
143
|
+
"conditions": {"as_of_required": primitive == "resource"},
|
|
144
|
+
"review_status": "auto_classified",
|
|
145
|
+
}
|
|
146
|
+
if _matches(text, r"secret|credential|password|api[_\s-]?key|token|\.env"):
|
|
147
|
+
return _classification("secret", "blocked", "secret", "secret.read", "blocked", [])
|
|
148
|
+
if _matches(text, r"transfer|withdraw|wire|ach|deposit"):
|
|
149
|
+
return _classification("execution", "execution", "private", "cash.transfer", "service_path", [])
|
|
150
|
+
if _matches(text, r"place[_\s-]?order|submit[_\s-]?order|create[_\s-]?order|replace[_\s-]?order|cancel[_\s-]?order|trade|execute"):
|
|
151
|
+
capability = "order.cancel" if "cancel" in text else "order.submit"
|
|
152
|
+
return _classification("execution", "execution", "private", capability, "service_adapter", [])
|
|
153
|
+
if _matches(text, r"policy|permission|principal|capability|allowlist|admin|enable[_\s-]?tool|disable[_\s-]?tool"):
|
|
154
|
+
return _classification("policy_admin", "write", "canonical_state", "policy.config.write", "blocked", [])
|
|
155
|
+
if _matches(text, r"position|positions|balance|balances|account|buying[_\s-]?power|portfolio|orders|fills|holdings"):
|
|
156
|
+
return _classification("account_read", "read", "private", "account.positions.read", "summary_only", sorted(ACCOUNT_READ_ROLES))
|
|
157
|
+
if _matches(text, r"quote|quotes|candles|bars|ohlcv|price|market[_\s-]?data|ticker|tickers|news|filing|fundamental|financial|earnings"):
|
|
158
|
+
return _classification("market_data", "read", "public", "market_data.read", "read_only", sorted(RESEARCH_ROLES | PORTFOLIO_STATE_ROLES))
|
|
159
|
+
if _matches(text, r"snapshot|source|artifact|research|dataset|import"):
|
|
160
|
+
return _classification("research_write", "write", "research", "research.snapshot.write", "service_path", sorted(RESEARCH_ROLES))
|
|
161
|
+
return _classification("unknown", "unknown", "unknown", "mcp.external.unknown", "blocked", [])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def set_external_tool_policy(
|
|
165
|
+
tool: McpExternalTool,
|
|
166
|
+
*,
|
|
167
|
+
category: str | None = None,
|
|
168
|
+
risk_level: str | None = None,
|
|
169
|
+
sensitivity: str | None = None,
|
|
170
|
+
canonical_capability: str | None = None,
|
|
171
|
+
proxy_mode: str | None = None,
|
|
172
|
+
allowed_roles: list[str] | None = None,
|
|
173
|
+
enabled: bool | None = None,
|
|
174
|
+
review_status: str = "reviewed",
|
|
175
|
+
actor: str = "web",
|
|
176
|
+
) -> McpExternalTool:
|
|
177
|
+
if category is not None:
|
|
178
|
+
tool.category = category or "unknown"
|
|
179
|
+
if risk_level is not None:
|
|
180
|
+
tool.risk_level = risk_level or "unknown"
|
|
181
|
+
if sensitivity is not None:
|
|
182
|
+
tool.sensitivity = sensitivity or "unknown"
|
|
183
|
+
if canonical_capability is not None:
|
|
184
|
+
tool.canonical_capability = canonical_capability
|
|
185
|
+
if proxy_mode is not None:
|
|
186
|
+
tool.proxy_mode = proxy_mode or "blocked"
|
|
187
|
+
if allowed_roles is not None:
|
|
188
|
+
tool.allowed_roles = [role for role in allowed_roles if role]
|
|
189
|
+
if enabled is not None:
|
|
190
|
+
if enabled:
|
|
191
|
+
_validate_external_tool_can_enable(tool)
|
|
192
|
+
tool.enabled = bool(enabled)
|
|
193
|
+
tool.review_status = review_status or "reviewed"
|
|
194
|
+
if tool.review_status == "reviewed":
|
|
195
|
+
tool.drift_detected = False
|
|
196
|
+
tool.save()
|
|
197
|
+
_audit("external_mcp_tool.policy_updated", {"tool": str(tool), "enabled": tool.enabled, "proxy_mode": tool.proxy_mode}, actor)
|
|
198
|
+
return tool
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def evaluate_external_mcp_proxy_call(
|
|
202
|
+
workspace_root: Any,
|
|
203
|
+
tool: McpExternalTool,
|
|
204
|
+
*,
|
|
205
|
+
principal_id: str,
|
|
206
|
+
arguments: dict[str, Any] | None = None,
|
|
207
|
+
actor: str = "mcp-proxy",
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
reasons = external_tool_denial_reasons(tool, principal_id)
|
|
210
|
+
decision = "allow" if not reasons else "deny"
|
|
211
|
+
result = {
|
|
212
|
+
"decision": decision,
|
|
213
|
+
"reasons": reasons,
|
|
214
|
+
"router": tool.router.name,
|
|
215
|
+
"external_name": tool.external_name,
|
|
216
|
+
"proxy_mode": tool.proxy_mode,
|
|
217
|
+
"category": tool.category,
|
|
218
|
+
"risk_level": tool.risk_level,
|
|
219
|
+
"canonical_capability": tool.canonical_capability,
|
|
220
|
+
"adapter_call_allowed": decision == "allow" and tool.proxy_mode in SERVICE_PROXY_MODES,
|
|
221
|
+
"direct_proxy_allowed": decision == "allow" and tool.proxy_mode in READ_ONLY_PROXY_MODES,
|
|
222
|
+
"db_canonical": True,
|
|
223
|
+
"workspace_context": workspace_context_payload(workspace_root),
|
|
224
|
+
}
|
|
225
|
+
McpExternalToolCall.objects.create(
|
|
226
|
+
external_tool=tool,
|
|
227
|
+
router_name=tool.router.name,
|
|
228
|
+
external_name=tool.external_name,
|
|
229
|
+
principal_id=principal_id,
|
|
230
|
+
proxy_mode=tool.proxy_mode,
|
|
231
|
+
decision=decision,
|
|
232
|
+
reasons=reasons,
|
|
233
|
+
request=arguments or {},
|
|
234
|
+
response=result,
|
|
235
|
+
request_hash=stable_hash(arguments or {}),
|
|
236
|
+
result_hash=stable_hash(result),
|
|
237
|
+
workspace_context=result["workspace_context"],
|
|
238
|
+
)
|
|
239
|
+
_audit("external_mcp.proxy_allowed" if decision == "allow" else "external_mcp.proxy_denied", {"tool": str(tool), "reasons": reasons}, actor)
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def external_tool_denial_reasons(tool: McpExternalTool, principal_id: str) -> list[str]:
|
|
244
|
+
reasons: list[str] = []
|
|
245
|
+
role = role_for_principal_id(principal_id)
|
|
246
|
+
if not tool.router.enabled:
|
|
247
|
+
reasons.append(f"router is disabled: {tool.router.name}")
|
|
248
|
+
if not tool.enabled:
|
|
249
|
+
reasons.append(f"external tool is disabled: {tool.external_name}")
|
|
250
|
+
if tool.drift_detected:
|
|
251
|
+
reasons.append("schema drift requires review")
|
|
252
|
+
if tool.review_status not in {"reviewed", "approved"}:
|
|
253
|
+
reasons.append(f"tool review is not complete: {tool.review_status}")
|
|
254
|
+
if tool.category in {"secret", "policy_admin"}:
|
|
255
|
+
reasons.append(f"category is not proxyable: {tool.category}")
|
|
256
|
+
if tool.category == "execution" and tool.proxy_mode not in SERVICE_PROXY_MODES:
|
|
257
|
+
reasons.append("execution tools must map to a TradingCodex service adapter path")
|
|
258
|
+
if tool.category == "unknown":
|
|
259
|
+
reasons.append("unknown tools require classification before proxy")
|
|
260
|
+
allowed = set(tool.allowed_roles or [])
|
|
261
|
+
if not _permission_allows(tool, principal_id, role, allowed):
|
|
262
|
+
reasons.append(f"principal is not allowed for external tool: {principal_id}")
|
|
263
|
+
return list(dict.fromkeys(reasons))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _validate_external_tool_can_enable(tool: McpExternalTool) -> None:
|
|
267
|
+
if tool.drift_detected:
|
|
268
|
+
raise ValueError("schema drift requires review before enabling")
|
|
269
|
+
if tool.proxy_mode == "direct":
|
|
270
|
+
raise ValueError("direct raw proxy mode is not allowed")
|
|
271
|
+
if tool.category in {"secret", "policy_admin", "unknown"}:
|
|
272
|
+
raise ValueError(f"{tool.category} tools cannot be enabled for proxy")
|
|
273
|
+
if tool.category == "execution" and tool.proxy_mode not in SERVICE_PROXY_MODES:
|
|
274
|
+
raise ValueError("execution tools must use service_adapter or service_path proxy mode")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _permission_allows(tool: McpExternalTool, principal_id: str, role: str, allowed_roles: set[str]) -> bool:
|
|
278
|
+
if principal_id in allowed_roles or role in allowed_roles:
|
|
279
|
+
return True
|
|
280
|
+
permissions = McpExternalToolPermission.objects.filter(external_tool=tool, enabled=True)
|
|
281
|
+
if permissions.filter(decision="deny", principal_or_role__in={principal_id, role}).exists():
|
|
282
|
+
return False
|
|
283
|
+
return permissions.filter(decision="allow", principal_or_role__in={principal_id, role}).exists()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _classification(category: str, risk_level: str, sensitivity: str, capability: str, proxy_mode: str, roles: list[str]) -> dict[str, Any]:
|
|
287
|
+
return {
|
|
288
|
+
"category": category,
|
|
289
|
+
"risk_level": risk_level,
|
|
290
|
+
"sensitivity": sensitivity,
|
|
291
|
+
"canonical_capability": capability,
|
|
292
|
+
"proxy_mode": proxy_mode,
|
|
293
|
+
"allowed_roles": roles,
|
|
294
|
+
"conditions": {},
|
|
295
|
+
"review_status": "auto_classified",
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _matches(text: str, pattern: str) -> bool:
|
|
300
|
+
return bool(re.search(pattern, text, flags=re.I))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _coerce_payload(payload: str | dict[str, Any]) -> dict[str, Any]:
|
|
304
|
+
if isinstance(payload, dict):
|
|
305
|
+
return payload
|
|
306
|
+
if not str(payload).strip():
|
|
307
|
+
return {}
|
|
308
|
+
parsed = json.loads(payload)
|
|
309
|
+
if not isinstance(parsed, dict):
|
|
310
|
+
raise ValueError("MCP discovery payload must be a JSON object")
|
|
311
|
+
return parsed
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _iter_discovered_primitives(payload: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
|
|
315
|
+
body = payload.get("result") if isinstance(payload.get("result"), dict) else payload
|
|
316
|
+
primitives: list[tuple[str, dict[str, Any]]] = []
|
|
317
|
+
for key, primitive in [("tools", "tool"), ("resources", "resource"), ("prompts", "prompt")]:
|
|
318
|
+
items = body.get(key) if isinstance(body, dict) else None
|
|
319
|
+
if isinstance(items, list):
|
|
320
|
+
primitives.extend((primitive, item) for item in items if isinstance(item, dict))
|
|
321
|
+
return primitives
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _audit(action: str, payload: dict[str, Any], actor: str) -> None:
|
|
325
|
+
from tradingcodex_service.application.audit import write_audit_event_if_available
|
|
326
|
+
|
|
327
|
+
write_audit_event_if_available(None, actor, "admin", {"type": action, "payload": payload})
|
apps/orders/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
apps/orders/admin.py
ADDED
apps/orders/apps.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Generated by Django 5.2.5 on 2026-06-12 16:32
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
initial = True
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name='ApprovalReceipt',
|
|
16
|
+
fields=[
|
|
17
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
18
|
+
('receipt_id', models.CharField(max_length=160, unique=True)),
|
|
19
|
+
('order_intent_id', models.CharField(max_length=160)),
|
|
20
|
+
('approved_by', models.CharField(max_length=128)),
|
|
21
|
+
('valid', models.BooleanField(default=True)),
|
|
22
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
23
|
+
('expires_at', models.DateTimeField()),
|
|
24
|
+
('workspace_context', models.JSONField(blank=True, default=dict)),
|
|
25
|
+
('payload', models.JSONField(blank=True, default=dict)),
|
|
26
|
+
],
|
|
27
|
+
options={
|
|
28
|
+
'verbose_name': 'Approval receipt',
|
|
29
|
+
'verbose_name_plural': 'Approval receipts',
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
migrations.CreateModel(
|
|
33
|
+
name='ExecutionResult',
|
|
34
|
+
fields=[
|
|
35
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
36
|
+
('order_intent_id', models.CharField(max_length=160)),
|
|
37
|
+
('approval_receipt_id', models.CharField(blank=True, max_length=160)),
|
|
38
|
+
('idempotency_key', models.CharField(blank=True, max_length=220, null=True, unique=True)),
|
|
39
|
+
('adapter', models.CharField(max_length=64)),
|
|
40
|
+
('status', models.CharField(max_length=32)),
|
|
41
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
42
|
+
('portfolio_id', models.CharField(default='default-paper', max_length=120)),
|
|
43
|
+
('account_id', models.CharField(default='local-paper', max_length=120)),
|
|
44
|
+
('strategy_id', models.CharField(default='default-strategy', max_length=120)),
|
|
45
|
+
('workspace_context', models.JSONField(blank=True, default=dict)),
|
|
46
|
+
('payload', models.JSONField(blank=True, default=dict)),
|
|
47
|
+
],
|
|
48
|
+
options={
|
|
49
|
+
'verbose_name': 'Execution result',
|
|
50
|
+
'verbose_name_plural': 'Execution results',
|
|
51
|
+
'ordering': ['-created_at', '-id'],
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
migrations.CreateModel(
|
|
55
|
+
name='OrderIntent',
|
|
56
|
+
fields=[
|
|
57
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
58
|
+
('intent_id', models.CharField(max_length=160, unique=True)),
|
|
59
|
+
('symbol', models.CharField(max_length=64)),
|
|
60
|
+
('side', models.CharField(max_length=8)),
|
|
61
|
+
('quantity', models.DecimalField(decimal_places=6, max_digits=20)),
|
|
62
|
+
('limit_price', models.DecimalField(decimal_places=6, max_digits=20)),
|
|
63
|
+
('currency', models.CharField(default='KRW', max_length=16)),
|
|
64
|
+
('broker', models.CharField(max_length=64)),
|
|
65
|
+
('estimated_notional_krw', models.DecimalField(decimal_places=2, max_digits=24)),
|
|
66
|
+
('created_by', models.CharField(max_length=128)),
|
|
67
|
+
('created_at', models.DateTimeField()),
|
|
68
|
+
('portfolio_id', models.CharField(default='default-paper', max_length=120)),
|
|
69
|
+
('account_id', models.CharField(default='local-paper', max_length=120)),
|
|
70
|
+
('strategy_id', models.CharField(default='default-strategy', max_length=120)),
|
|
71
|
+
('workspace_context', models.JSONField(blank=True, default=dict)),
|
|
72
|
+
('payload', models.JSONField(blank=True, default=dict)),
|
|
73
|
+
],
|
|
74
|
+
options={
|
|
75
|
+
'verbose_name': 'Order intent',
|
|
76
|
+
'verbose_name_plural': 'Order intents',
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
]
|
|
File without changes
|
apps/orders/models.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OrderIntent(models.Model):
|
|
5
|
+
intent_id = models.CharField(max_length=160, unique=True)
|
|
6
|
+
symbol = models.CharField(max_length=64)
|
|
7
|
+
side = models.CharField(max_length=8)
|
|
8
|
+
quantity = models.DecimalField(max_digits=20, decimal_places=6)
|
|
9
|
+
limit_price = models.DecimalField(max_digits=20, decimal_places=6)
|
|
10
|
+
currency = models.CharField(max_length=16, default="KRW")
|
|
11
|
+
broker = models.CharField(max_length=64)
|
|
12
|
+
estimated_notional_krw = models.DecimalField(max_digits=24, decimal_places=2)
|
|
13
|
+
created_by = models.CharField(max_length=128)
|
|
14
|
+
created_at = models.DateTimeField()
|
|
15
|
+
portfolio_id = models.CharField(max_length=120, default="default-paper")
|
|
16
|
+
account_id = models.CharField(max_length=120, default="local-paper")
|
|
17
|
+
strategy_id = models.CharField(max_length=120, default="default-strategy")
|
|
18
|
+
workspace_context = models.JSONField(default=dict, blank=True)
|
|
19
|
+
payload = models.JSONField(default=dict, blank=True)
|
|
20
|
+
|
|
21
|
+
class Meta:
|
|
22
|
+
verbose_name = "Order intent"
|
|
23
|
+
verbose_name_plural = "Order intents"
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return self.intent_id
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ApprovalReceipt(models.Model):
|
|
30
|
+
receipt_id = models.CharField(max_length=160, unique=True)
|
|
31
|
+
order_intent_id = models.CharField(max_length=160)
|
|
32
|
+
approved_by = models.CharField(max_length=128)
|
|
33
|
+
valid = models.BooleanField(default=True)
|
|
34
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
35
|
+
expires_at = models.DateTimeField()
|
|
36
|
+
workspace_context = models.JSONField(default=dict, blank=True)
|
|
37
|
+
payload = models.JSONField(default=dict, blank=True)
|
|
38
|
+
|
|
39
|
+
class Meta:
|
|
40
|
+
verbose_name = "Approval receipt"
|
|
41
|
+
verbose_name_plural = "Approval receipts"
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
return self.receipt_id
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExecutionResult(models.Model):
|
|
48
|
+
order_intent_id = models.CharField(max_length=160)
|
|
49
|
+
approval_receipt_id = models.CharField(max_length=160, blank=True)
|
|
50
|
+
idempotency_key = models.CharField(max_length=220, unique=True, null=True, blank=True)
|
|
51
|
+
adapter = models.CharField(max_length=64)
|
|
52
|
+
status = models.CharField(max_length=32)
|
|
53
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
54
|
+
portfolio_id = models.CharField(max_length=120, default="default-paper")
|
|
55
|
+
account_id = models.CharField(max_length=120, default="local-paper")
|
|
56
|
+
strategy_id = models.CharField(max_length=120, default="default-strategy")
|
|
57
|
+
workspace_context = models.JSONField(default=dict, blank=True)
|
|
58
|
+
payload = models.JSONField(default=dict, blank=True)
|
|
59
|
+
|
|
60
|
+
class Meta:
|
|
61
|
+
ordering = ["-created_at", "-id"]
|
|
62
|
+
verbose_name = "Execution result"
|
|
63
|
+
verbose_name_plural = "Execution results"
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
return f"{self.status}: {self.order_intent_id}"
|
apps/orders/services.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from django.db import IntegrityError, transaction
|
|
9
|
+
|
|
10
|
+
from apps.orders.models import ExecutionResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ExecutionReservation:
|
|
15
|
+
created: bool
|
|
16
|
+
execution: ExecutionResult
|
|
17
|
+
idempotency_key: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def execution_idempotency_key(
|
|
21
|
+
order: dict[str, Any],
|
|
22
|
+
receipt: dict[str, Any] | None = None,
|
|
23
|
+
portfolio_id: str = "",
|
|
24
|
+
account_id: str = "",
|
|
25
|
+
strategy_id: str = "",
|
|
26
|
+
) -> str:
|
|
27
|
+
explicit = order.get("idempotency_key") or (receipt or {}).get("idempotency_key")
|
|
28
|
+
if explicit:
|
|
29
|
+
return str(explicit)
|
|
30
|
+
payload = {
|
|
31
|
+
"order_intent_id": order.get("id"),
|
|
32
|
+
"portfolio_id": portfolio_id or order.get("portfolio_id", ""),
|
|
33
|
+
"account_id": account_id or order.get("account_id", ""),
|
|
34
|
+
"strategy_id": strategy_id or order.get("strategy_id", ""),
|
|
35
|
+
"execution_boundary": "submit_approved_order",
|
|
36
|
+
}
|
|
37
|
+
return "submit:" + hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def existing_execution_for_order(
|
|
41
|
+
order_id: str,
|
|
42
|
+
idempotency_key: str,
|
|
43
|
+
portfolio_id: str,
|
|
44
|
+
account_id: str,
|
|
45
|
+
strategy_id: str,
|
|
46
|
+
) -> ExecutionResult | None:
|
|
47
|
+
return (
|
|
48
|
+
ExecutionResult.objects.filter(idempotency_key=idempotency_key).order_by("-created_at", "-id").first()
|
|
49
|
+
or ExecutionResult.objects.filter(
|
|
50
|
+
order_intent_id=order_id,
|
|
51
|
+
portfolio_id=portfolio_id,
|
|
52
|
+
account_id=account_id,
|
|
53
|
+
strategy_id=strategy_id,
|
|
54
|
+
).order_by("-created_at", "-id").first()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def reserve_execution(
|
|
59
|
+
*,
|
|
60
|
+
order: dict[str, Any],
|
|
61
|
+
receipt: dict[str, Any],
|
|
62
|
+
adapter: str,
|
|
63
|
+
portfolio_id: str,
|
|
64
|
+
account_id: str,
|
|
65
|
+
strategy_id: str,
|
|
66
|
+
workspace_context: dict[str, Any],
|
|
67
|
+
principal_id: str,
|
|
68
|
+
) -> ExecutionReservation:
|
|
69
|
+
key = execution_idempotency_key(order, receipt, portfolio_id, account_id, strategy_id)
|
|
70
|
+
existing = existing_execution_for_order(str(order.get("id", "")), key, portfolio_id, account_id, strategy_id)
|
|
71
|
+
if existing is not None:
|
|
72
|
+
return ExecutionReservation(False, existing, key)
|
|
73
|
+
|
|
74
|
+
payload = {
|
|
75
|
+
"status": "pending",
|
|
76
|
+
"order_intent_id": order.get("id"),
|
|
77
|
+
"approval_receipt_id": receipt.get("id", ""),
|
|
78
|
+
"principal_id": principal_id,
|
|
79
|
+
"idempotency_key": key,
|
|
80
|
+
}
|
|
81
|
+
try:
|
|
82
|
+
with transaction.atomic():
|
|
83
|
+
execution = ExecutionResult.objects.create(
|
|
84
|
+
order_intent_id=order["id"],
|
|
85
|
+
approval_receipt_id=receipt.get("id", ""),
|
|
86
|
+
adapter=adapter,
|
|
87
|
+
status="pending",
|
|
88
|
+
portfolio_id=portfolio_id,
|
|
89
|
+
account_id=account_id,
|
|
90
|
+
strategy_id=strategy_id,
|
|
91
|
+
workspace_context=workspace_context,
|
|
92
|
+
payload=payload,
|
|
93
|
+
idempotency_key=key,
|
|
94
|
+
)
|
|
95
|
+
except IntegrityError:
|
|
96
|
+
execution = existing_execution_for_order(str(order.get("id", "")), key, portfolio_id, account_id, strategy_id)
|
|
97
|
+
if execution is None:
|
|
98
|
+
raise
|
|
99
|
+
return ExecutionReservation(False, execution, key)
|
|
100
|
+
return ExecutionReservation(True, execution, key)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def finalize_execution_reservation(execution: ExecutionResult, result: dict[str, Any]) -> None:
|
|
104
|
+
execution.status = str(result.get("status") or "recorded")
|
|
105
|
+
execution.adapter = str(result.get("adapter") or execution.adapter)
|
|
106
|
+
execution.payload = result
|
|
107
|
+
execution.save(update_fields=["status", "adapter", "payload"])
|
apps/policy/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
apps/policy/admin.py
ADDED