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.
Files changed (254) hide show
  1. apps/__init__.py +1 -0
  2. apps/audit/__init__.py +1 -0
  3. apps/audit/admin.py +6 -0
  4. apps/audit/apps.py +8 -0
  5. apps/audit/migrations/0001_initial.py +35 -0
  6. apps/audit/migrations/__init__.py +0 -0
  7. apps/audit/models.py +22 -0
  8. apps/harness/__init__.py +1 -0
  9. apps/harness/admin.py +6 -0
  10. apps/harness/apps.py +8 -0
  11. apps/harness/migrations/0001_initial.py +35 -0
  12. apps/harness/migrations/__init__.py +0 -0
  13. apps/harness/models.py +22 -0
  14. apps/harness/templatetags/__init__.py +1 -0
  15. apps/integrations/__init__.py +1 -0
  16. apps/integrations/admin.py +6 -0
  17. apps/integrations/apps.py +8 -0
  18. apps/integrations/migrations/0001_initial.py +29 -0
  19. apps/integrations/migrations/__init__.py +0 -0
  20. apps/integrations/models.py +16 -0
  21. apps/integrations/services.py +31 -0
  22. apps/mcp/__init__.py +1 -0
  23. apps/mcp/admin.py +20 -0
  24. apps/mcp/apps.py +8 -0
  25. apps/mcp/migrations/0001_initial.py +168 -0
  26. apps/mcp/migrations/__init__.py +0 -0
  27. apps/mcp/models.py +154 -0
  28. apps/mcp/services.py +327 -0
  29. apps/orders/__init__.py +1 -0
  30. apps/orders/admin.py +6 -0
  31. apps/orders/apps.py +8 -0
  32. apps/orders/migrations/0001_initial.py +79 -0
  33. apps/orders/migrations/__init__.py +0 -0
  34. apps/orders/models.py +66 -0
  35. apps/orders/services.py +107 -0
  36. apps/policy/__init__.py +1 -0
  37. apps/policy/admin.py +6 -0
  38. apps/policy/apps.py +8 -0
  39. apps/policy/migrations/0001_initial.py +75 -0
  40. apps/policy/migrations/__init__.py +0 -0
  41. apps/policy/models.py +61 -0
  42. apps/policy/services.py +110 -0
  43. apps/portfolio/__init__.py +1 -0
  44. apps/portfolio/admin.py +6 -0
  45. apps/portfolio/apps.py +8 -0
  46. apps/portfolio/migrations/0001_initial.py +67 -0
  47. apps/portfolio/migrations/__init__.py +0 -0
  48. apps/portfolio/models.py +53 -0
  49. apps/research/__init__.py +1 -0
  50. apps/research/admin.py +1 -0
  51. apps/research/apps.py +8 -0
  52. apps/research/migrations/__init__.py +0 -0
  53. apps/research/models.py +1 -0
  54. apps/workflows/__init__.py +1 -0
  55. apps/workflows/admin.py +6 -0
  56. apps/workflows/apps.py +8 -0
  57. apps/workflows/migrations/0001_initial.py +51 -0
  58. apps/workflows/migrations/__init__.py +0 -0
  59. apps/workflows/models.py +44 -0
  60. tradingcodex-0.1.0.dist-info/METADATA +337 -0
  61. tradingcodex-0.1.0.dist-info/RECORD +254 -0
  62. tradingcodex-0.1.0.dist-info/WHEEL +5 -0
  63. tradingcodex-0.1.0.dist-info/entry_points.txt +2 -0
  64. tradingcodex-0.1.0.dist-info/licenses/LICENSE +202 -0
  65. tradingcodex-0.1.0.dist-info/licenses/NOTICE +24 -0
  66. tradingcodex-0.1.0.dist-info/top_level.txt +4 -0
  67. tradingcodex_cli/__init__.py +1 -0
  68. tradingcodex_cli/__main__.py +124 -0
  69. tradingcodex_cli/commands/__init__.py +2 -0
  70. tradingcodex_cli/commands/bootstrap.py +157 -0
  71. tradingcodex_cli/commands/db.py +36 -0
  72. tradingcodex_cli/commands/doctor.py +230 -0
  73. tradingcodex_cli/commands/mcp.py +186 -0
  74. tradingcodex_cli/commands/orders.py +89 -0
  75. tradingcodex_cli/commands/policy.py +21 -0
  76. tradingcodex_cli/commands/profile.py +110 -0
  77. tradingcodex_cli/commands/research.py +76 -0
  78. tradingcodex_cli/commands/skills.py +93 -0
  79. tradingcodex_cli/commands/strategies.py +67 -0
  80. tradingcodex_cli/commands/subagents.py +106 -0
  81. tradingcodex_cli/commands/utils.py +134 -0
  82. tradingcodex_cli/commands/workspaces.py +53 -0
  83. tradingcodex_cli/generator.py +234 -0
  84. tradingcodex_cli/mcp_stdio.py +26 -0
  85. tradingcodex_cli/service_autostart.py +127 -0
  86. tradingcodex_service/__init__.py +5 -0
  87. tradingcodex_service/admin.py +1 -0
  88. tradingcodex_service/api.py +486 -0
  89. tradingcodex_service/application/__init__.py +2 -0
  90. tradingcodex_service/application/agents.py +1470 -0
  91. tradingcodex_service/application/audit.py +71 -0
  92. tradingcodex_service/application/common.py +88 -0
  93. tradingcodex_service/application/components.py +299 -0
  94. tradingcodex_service/application/harness.py +747 -0
  95. tradingcodex_service/application/markdown_preview.py +179 -0
  96. tradingcodex_service/application/orders.py +404 -0
  97. tradingcodex_service/application/policy.py +150 -0
  98. tradingcodex_service/application/portfolio.py +166 -0
  99. tradingcodex_service/application/research.py +356 -0
  100. tradingcodex_service/application/runtime.py +321 -0
  101. tradingcodex_service/asgi.py +6 -0
  102. tradingcodex_service/mcp_http.py +59 -0
  103. tradingcodex_service/mcp_runtime.py +565 -0
  104. tradingcodex_service/settings.py +91 -0
  105. tradingcodex_service/templates/web/activity.html +24 -0
  106. tradingcodex_service/templates/web/agent_skills.html +111 -0
  107. tradingcodex_service/templates/web/agents.html +121 -0
  108. tradingcodex_service/templates/web/base.html +150 -0
  109. tradingcodex_service/templates/web/dashboard.html +109 -0
  110. tradingcodex_service/templates/web/fragments/role_inspector.html +81 -0
  111. tradingcodex_service/templates/web/fragments/starter_prompt.html +24 -0
  112. tradingcodex_service/templates/web/fragments/topology_canvas.html +85 -0
  113. tradingcodex_service/templates/web/harness.html +87 -0
  114. tradingcodex_service/templates/web/mcp_router.html +250 -0
  115. tradingcodex_service/templates/web/orders.html +68 -0
  116. tradingcodex_service/templates/web/policy.html +81 -0
  117. tradingcodex_service/templates/web/portfolio.html +52 -0
  118. tradingcodex_service/templates/web/research.html +73 -0
  119. tradingcodex_service/templates/web/starter_prompt.html +40 -0
  120. tradingcodex_service/templates/web/strategies.html +74 -0
  121. tradingcodex_service/urls.py +42 -0
  122. tradingcodex_service/version.py +1 -0
  123. tradingcodex_service/web.py +885 -0
  124. tradingcodex_service/wsgi.py +6 -0
  125. workspace_templates/__init__.py +1 -0
  126. workspace_templates/modules/audit/files/.tradingcodex/audit/README.md +5 -0
  127. workspace_templates/modules/audit/files/trading/audit/.gitkeep +1 -0
  128. workspace_templates/modules/audit/module.json +16 -0
  129. workspace_templates/modules/codex-base/files/.codex/config.toml +272 -0
  130. workspace_templates/modules/codex-base/files/.codex/hooks/tradingcodex_hook.py +173 -0
  131. workspace_templates/modules/codex-base/files/.codex/hooks.json +105 -0
  132. workspace_templates/modules/codex-base/files/.codex/prompts/base_instructions/head-manager.md +129 -0
  133. workspace_templates/modules/codex-base/files/.codex/rules/tradingcodex.rules +50 -0
  134. workspace_templates/modules/codex-base/files/.tradingcodex/capabilities.yaml +56 -0
  135. workspace_templates/modules/codex-base/files/.tradingcodex/cli.py +16 -0
  136. workspace_templates/modules/codex-base/files/.tradingcodex/config.yaml +53 -0
  137. workspace_templates/modules/codex-base/files/.tradingcodex/policies/policy-bindings.yaml +16 -0
  138. workspace_templates/modules/codex-base/files/.tradingcodex/policies/principals.yaml +17 -0
  139. workspace_templates/modules/codex-base/files/.tradingcodex/policies/roles.yaml +27 -0
  140. workspace_templates/modules/codex-base/files/AGENTS.md +56 -0
  141. workspace_templates/modules/codex-base/files/pyproject.toml +13 -0
  142. workspace_templates/modules/codex-base/files/tcx +35 -0
  143. workspace_templates/modules/codex-base/module.json +17 -0
  144. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/access-policies.yaml +39 -0
  145. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/policies/restricted-list.yaml +6 -0
  146. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/approval_receipt.schema.json +14 -0
  147. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/audit_event.schema.json +11 -0
  148. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/evidence_pack.schema.json +16 -0
  149. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/execution_result.schema.json +12 -0
  150. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/fundamental_report.schema.json +15 -0
  151. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/news_report.schema.json +15 -0
  152. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/order_intent.schema.json +30 -0
  153. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/portfolio_review.schema.json +15 -0
  154. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/postmortem_report.schema.json +14 -0
  155. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/risk_report.schema.json +15 -0
  156. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/technical_report.schema.json +15 -0
  157. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/thesis.schema.json +15 -0
  158. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/schemas/valuation.schema.json +15 -0
  159. workspace_templates/modules/enforcement-guardrails/files/.tradingcodex/scripts/validate-order-intent.py +15 -0
  160. workspace_templates/modules/enforcement-guardrails/module.json +19 -0
  161. workspace_templates/modules/fixed-subagents/files/.codex/agents/execution-operator.toml +71 -0
  162. workspace_templates/modules/fixed-subagents/files/.codex/agents/fundamental-analyst.toml +62 -0
  163. workspace_templates/modules/fixed-subagents/files/.codex/agents/instrument-analyst.toml +64 -0
  164. workspace_templates/modules/fixed-subagents/files/.codex/agents/macro-analyst.toml +64 -0
  165. workspace_templates/modules/fixed-subagents/files/.codex/agents/news-analyst.toml +63 -0
  166. workspace_templates/modules/fixed-subagents/files/.codex/agents/portfolio-manager.toml +62 -0
  167. workspace_templates/modules/fixed-subagents/files/.codex/agents/risk-manager.toml +65 -0
  168. workspace_templates/modules/fixed-subagents/files/.codex/agents/technical-analyst.toml +63 -0
  169. workspace_templates/modules/fixed-subagents/files/.codex/agents/valuation-analyst.toml +59 -0
  170. workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/head-manager.yaml +66 -0
  171. workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/skill-change-proposals/.gitkeep +1 -0
  172. workspace_templates/modules/fixed-subagents/files/.tradingcodex/mainagent/subagent-registry.yaml +56 -0
  173. workspace_templates/modules/fixed-subagents/module.json +23 -0
  174. workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/guardrails.md +17 -0
  175. workspace_templates/modules/guidance-guardrails/files/.tradingcodex/guidance/task-quality-checklist.md +37 -0
  176. workspace_templates/modules/guidance-guardrails/module.json +18 -0
  177. workspace_templates/modules/information-barriers/files/.tradingcodex/policies/information-barriers.yaml +211 -0
  178. workspace_templates/modules/information-barriers/files/.tradingcodex/secrets.md +9 -0
  179. workspace_templates/modules/information-barriers/files/trading/approvals/.gitkeep +1 -0
  180. workspace_templates/modules/information-barriers/files/trading/market-data/.gitkeep +1 -0
  181. workspace_templates/modules/information-barriers/files/trading/orders/approved/.gitkeep +1 -0
  182. workspace_templates/modules/information-barriers/files/trading/orders/draft/.gitkeep +1 -0
  183. workspace_templates/modules/information-barriers/files/trading/orders/executed/.gitkeep +1 -0
  184. workspace_templates/modules/information-barriers/files/trading/orders/rejected/.gitkeep +1 -0
  185. workspace_templates/modules/information-barriers/files/trading/portfolio/.gitkeep +1 -0
  186. workspace_templates/modules/information-barriers/files/trading/reports/fundamental/.gitkeep +1 -0
  187. workspace_templates/modules/information-barriers/files/trading/reports/instrument/.gitkeep +1 -0
  188. workspace_templates/modules/information-barriers/files/trading/reports/macro/.gitkeep +1 -0
  189. workspace_templates/modules/information-barriers/files/trading/reports/news/.gitkeep +1 -0
  190. workspace_templates/modules/information-barriers/files/trading/reports/policy/.gitkeep +1 -0
  191. workspace_templates/modules/information-barriers/files/trading/reports/portfolio/.gitkeep +1 -0
  192. workspace_templates/modules/information-barriers/files/trading/reports/postmortem/.gitkeep +1 -0
  193. workspace_templates/modules/information-barriers/files/trading/reports/risk/.gitkeep +1 -0
  194. workspace_templates/modules/information-barriers/files/trading/reports/technical/.gitkeep +1 -0
  195. workspace_templates/modules/information-barriers/files/trading/reports/valuation/.gitkeep +1 -0
  196. workspace_templates/modules/information-barriers/files/trading/research/.gitkeep +1 -0
  197. workspace_templates/modules/information-barriers/module.json +19 -0
  198. workspace_templates/modules/paper-trading/files/.tradingcodex/mcp/adapters/paper-trading.py +4 -0
  199. workspace_templates/modules/paper-trading/module.json +16 -0
  200. workspace_templates/modules/postmortem/files/.tradingcodex/workflows/postmortem.yaml +12 -0
  201. workspace_templates/modules/postmortem/module.json +16 -0
  202. workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/SKILL.md +106 -0
  203. workspace_templates/modules/repo-skills/files/.agents/skills/investment-workflow-map/agents/openai.yaml +6 -0
  204. workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/SKILL.md +101 -0
  205. workspace_templates/modules/repo-skills/files/.agents/skills/manage-optional-skills/agents/openai.yaml +6 -0
  206. workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/SKILL.md +140 -0
  207. workspace_templates/modules/repo-skills/files/.agents/skills/manage-subagents/agents/openai.yaml +6 -0
  208. workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/SKILL.md +140 -0
  209. workspace_templates/modules/repo-skills/files/.agents/skills/orchestrate-workflow/agents/openai.yaml +6 -0
  210. workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/SKILL.md +31 -0
  211. workspace_templates/modules/repo-skills/files/.agents/skills/postmortem/agents/openai.yaml +6 -0
  212. workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/SKILL.md +138 -0
  213. workspace_templates/modules/repo-skills/files/.agents/skills/scenario-quality-gates/agents/openai.yaml +6 -0
  214. workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/SKILL.md +109 -0
  215. workspace_templates/modules/repo-skills/files/.agents/skills/strategy-creator/agents/openai.yaml +6 -0
  216. workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/SKILL.md +54 -0
  217. workspace_templates/modules/repo-skills/files/.agents/skills/synthesize-decision/agents/openai.yaml +6 -0
  218. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/SKILL.md +35 -0
  219. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/execution-operator/execute-paper-order/agents/openai.yaml +6 -0
  220. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/SKILL.md +46 -0
  221. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/fundamental-analyst/fundamental-analysis/agents/openai.yaml +6 -0
  222. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/SKILL.md +40 -0
  223. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/instrument-analyst/instrument-analysis/agents/openai.yaml +6 -0
  224. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/SKILL.md +40 -0
  225. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/macro-analyst/macro-analysis/agents/openai.yaml +6 -0
  226. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/SKILL.md +43 -0
  227. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/news-analyst/news-analysis/agents/openai.yaml +6 -0
  228. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/SKILL.md +46 -0
  229. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/create-order-intent/agents/openai.yaml +6 -0
  230. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/SKILL.md +44 -0
  231. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/portfolio-manager/portfolio-review/agents/openai.yaml +6 -0
  232. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/SKILL.md +38 -0
  233. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/approve-order/agents/openai.yaml +6 -0
  234. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/SKILL.md +43 -0
  235. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/policy-review/agents/openai.yaml +6 -0
  236. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/SKILL.md +45 -0
  237. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/risk-manager/review-risk/agents/openai.yaml +6 -0
  238. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/SKILL.md +46 -0
  239. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/collect-evidence/agents/openai.yaml +6 -0
  240. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/SKILL.md +66 -0
  241. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/shared/external-data-source-gate/agents/openai.yaml +6 -0
  242. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/SKILL.md +43 -0
  243. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/technical-analyst/technical-analysis/agents/openai.yaml +6 -0
  244. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/SKILL.md +47 -0
  245. workspace_templates/modules/repo-skills/files/.tradingcodex/subagents/skills/valuation-analyst/valuation-review/agents/openai.yaml +6 -0
  246. workspace_templates/modules/repo-skills/module.json +37 -0
  247. workspace_templates/modules/stub-execution/files/.tradingcodex/mcp/adapters/stub-execution.py +4 -0
  248. workspace_templates/modules/stub-execution/module.json +15 -0
  249. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/adapters/live-adapter.contract.md +25 -0
  250. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/enforcer/README.md +5 -0
  251. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/gateway/README.md +8 -0
  252. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/server.py +27 -0
  253. workspace_templates/modules/tradingcodex-mcp/files/.tradingcodex/mcp/smoke-call.py +18 -0
  254. workspace_templates/modules/tradingcodex-mcp/module.json +24 -0
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import nh3
9
+ from markdown_it import MarkdownIt
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class MarkdownPreview:
14
+ markdown: str
15
+ html: str
16
+ source_file: str
17
+ source_label: str
18
+ frontmatter: dict[str, Any]
19
+ metadata_items: list[dict[str, str]]
20
+ heading: str
21
+
22
+
23
+ _MARKDOWN = MarkdownIt("default", {"html": False, "linkify": False, "typographer": False})
24
+
25
+ _ALLOWED_TAGS = {
26
+ "a",
27
+ "blockquote",
28
+ "br",
29
+ "code",
30
+ "del",
31
+ "em",
32
+ "h1",
33
+ "h2",
34
+ "h3",
35
+ "h4",
36
+ "h5",
37
+ "h6",
38
+ "hr",
39
+ "li",
40
+ "ol",
41
+ "p",
42
+ "pre",
43
+ "strong",
44
+ "table",
45
+ "tbody",
46
+ "td",
47
+ "th",
48
+ "thead",
49
+ "tr",
50
+ "ul",
51
+ }
52
+
53
+ _ALLOWED_ATTRIBUTES = {
54
+ "a": {"href", "title"},
55
+ "th": {"align"},
56
+ "td": {"align"},
57
+ }
58
+
59
+
60
+ def render_markdown_preview(
61
+ markdown: str,
62
+ *,
63
+ source_file: str = "",
64
+ source_label: str = "",
65
+ strip_frontmatter: bool = True,
66
+ ) -> MarkdownPreview:
67
+ document = split_markdown_frontmatter(markdown or "") if strip_frontmatter else MarkdownDocument({}, "", markdown or "")
68
+ body = document.body
69
+ raw_html = _MARKDOWN.render(body or "")
70
+ clean_html = nh3.clean(
71
+ raw_html,
72
+ tags=_ALLOWED_TAGS,
73
+ attributes=_ALLOWED_ATTRIBUTES,
74
+ url_schemes={"http", "https", "mailto"},
75
+ )
76
+ return MarkdownPreview(
77
+ markdown=body,
78
+ html=clean_html,
79
+ source_file=source_file,
80
+ source_label=source_label or "markdown",
81
+ frontmatter=document.frontmatter,
82
+ metadata_items=frontmatter_items(document.frontmatter),
83
+ heading=document.heading,
84
+ )
85
+
86
+
87
+ def read_markdown_preview(path: Path, *, source_file: str = "", source_label: str = "") -> MarkdownPreview:
88
+ try:
89
+ markdown = path.read_text(encoding="utf-8")
90
+ except Exception:
91
+ markdown = "_No markdown available._"
92
+ return render_markdown_preview(markdown, source_file=source_file or str(path), source_label=source_label)
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class MarkdownDocument:
97
+ frontmatter: dict[str, Any]
98
+ heading: str
99
+ body: str
100
+
101
+
102
+ def split_markdown_frontmatter(markdown: str) -> MarkdownDocument:
103
+ lines = (markdown or "").splitlines()
104
+ frontmatter: dict[str, Any] = {}
105
+ body_lines = lines
106
+ if lines and lines[0].strip() == "---":
107
+ for index, line in enumerate(lines[1:], start=1):
108
+ if line.strip() == "---":
109
+ raw_frontmatter = "\n".join(lines[1:index])
110
+ frontmatter = parse_simple_frontmatter(raw_frontmatter)
111
+ body_lines = lines[index + 1 :]
112
+ break
113
+ heading = ""
114
+ for line in body_lines:
115
+ stripped = line.strip()
116
+ if stripped.startswith("#"):
117
+ heading = stripped.lstrip("#").strip()
118
+ break
119
+ body = "\n".join(body_lines).strip()
120
+ return MarkdownDocument(frontmatter=frontmatter, heading=heading, body=body + ("\n" if body else ""))
121
+
122
+
123
+ def parse_simple_frontmatter(text: str) -> dict[str, Any]:
124
+ data: dict[str, Any] = {}
125
+ current_key = ""
126
+ current_list: list[str] | None = None
127
+ for raw_line in (text or "").splitlines():
128
+ line = raw_line.rstrip()
129
+ if not line.strip() or line.lstrip().startswith("#"):
130
+ continue
131
+ stripped = line.strip()
132
+ if current_key and current_list is not None and stripped.startswith("- "):
133
+ current_list.append(str(_parse_frontmatter_scalar(stripped[2:].strip())))
134
+ data[current_key] = current_list
135
+ continue
136
+ current_key = ""
137
+ current_list = None
138
+ if ":" not in line:
139
+ continue
140
+ key, raw_value = line.split(":", 1)
141
+ key = key.strip()
142
+ if not key:
143
+ continue
144
+ raw_value = raw_value.strip()
145
+ if raw_value == "":
146
+ current_key = key
147
+ current_list = []
148
+ data[key] = current_list
149
+ continue
150
+ data[key] = _parse_frontmatter_scalar(raw_value)
151
+ return data
152
+
153
+
154
+ def frontmatter_items(frontmatter: dict[str, Any]) -> list[dict[str, str]]:
155
+ items: list[dict[str, str]] = []
156
+ for key, value in frontmatter.items():
157
+ if value is None or value == "":
158
+ continue
159
+ if isinstance(value, bool):
160
+ rendered = "true" if value else "false"
161
+ elif isinstance(value, (list, tuple)):
162
+ rendered = ", ".join(str(item) for item in value)
163
+ else:
164
+ rendered = str(value)
165
+ items.append({"key": str(key), "label": str(key).replace("_", " ").title(), "value": rendered})
166
+ return items
167
+
168
+
169
+ def _parse_frontmatter_scalar(raw: str) -> Any:
170
+ if raw in {"true", "True"}:
171
+ return True
172
+ if raw in {"false", "False"}:
173
+ return False
174
+ if raw in {"null", "Null", "None", "~"}:
175
+ return None
176
+ try:
177
+ return json.loads(raw)
178
+ except Exception:
179
+ return raw.strip("'\"")
@@ -0,0 +1,404 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from tradingcodex_service.application.audit import write_audit_event
8
+ from tradingcodex_service.application.common import (
9
+ _parse_datetime,
10
+ _resolve_path,
11
+ _validate_positive,
12
+ now_iso,
13
+ read_json,
14
+ sanitize_id,
15
+ stable_hash,
16
+ write_json,
17
+ )
18
+ from tradingcodex_service.application.policy import evaluate_policy
19
+ from tradingcodex_service.application.portfolio import portfolio_keys, submit_paper_order
20
+ from tradingcodex_service.application.runtime import ensure_runtime_database, workspace_context_payload
21
+
22
+ def validate_order_intent(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
23
+ order = resolve_order_intent(Path(workspace_root), args)
24
+ reasons: list[str] = []
25
+ for field in ["id", "symbol", "side", "quantity", "limit_price", "currency", "broker", "estimated_notional_krw", "created_by", "created_at"]:
26
+ if order.get(field) in (None, ""):
27
+ reasons.append(f"missing {field}")
28
+ if order.get("side") not in ("buy", "sell"):
29
+ reasons.append("side must be buy or sell")
30
+ _validate_positive(order.get("quantity"), "quantity", reasons)
31
+ _validate_positive(order.get("limit_price"), "limit_price", reasons)
32
+ _validate_positive(order.get("estimated_notional_krw"), "estimated_notional_krw", reasons)
33
+ conflict = order_intent_payload_conflict(Path(workspace_root), order)
34
+ if conflict:
35
+ reasons.append(conflict)
36
+ policy = evaluate_policy(workspace_root, {**args, "action": args.get("action") or "order_intent.validate", "order_intent": order})
37
+ all_reasons = list(dict.fromkeys(reasons + policy["reasons"]))
38
+ result = {"valid": not all_reasons and policy["decision"] == "allow", "reasons": all_reasons, "policy": policy, "db_canonical": True, "workspace_context": workspace_context_payload(workspace_root)}
39
+ persist_order_intent_if_available(Path(workspace_root), order, result)
40
+ return result
41
+
42
+
43
+ def validate_approval_receipt(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
44
+ root = Path(workspace_root)
45
+ order = resolve_order_intent(root, args)
46
+ receipt = resolve_approval_receipt(root, args, order)
47
+ reasons: list[str] = []
48
+ for field in ["id", "order_intent_id", "approved_by", "valid", "expires_at"]:
49
+ if receipt.get(field) in (None, ""):
50
+ reasons.append(f"missing {field}")
51
+ if receipt.get("valid") is not True:
52
+ reasons.append("approval_receipt.valid must be true")
53
+ if order.get("id") and receipt.get("order_intent_id") != order["id"]:
54
+ reasons.append("approval_receipt.order_intent_id does not match order_intent.id")
55
+ if order.get("created_by") and receipt.get("approved_by") == order["created_by"]:
56
+ reasons.append("order creator cannot approve the same order")
57
+ expires_at = _parse_datetime(receipt.get("expires_at"))
58
+ if receipt.get("expires_at") and expires_at is None:
59
+ reasons.append("approval_receipt.expires_at is not a valid date")
60
+ if expires_at and expires_at <= datetime.now(timezone.utc):
61
+ reasons.append("approval receipt is expired")
62
+ return {"valid": not reasons, "reasons": reasons, "db_canonical": True, "workspace_context": workspace_context_payload(workspace_root)}
63
+
64
+
65
+ def create_approval_receipt(workspace_root: Path | str, order: dict[str, Any], approved_by: str = "risk-manager", expires_hours: int = 24) -> dict[str, Any]:
66
+ root = Path(workspace_root)
67
+ validation = validate_order_intent(root, {"principal_id": approved_by, "order_intent": order})
68
+ if not validation["valid"]:
69
+ rejected = {"status": "rejected", "order_intent_id": order.get("id"), "reasons": validation["reasons"], "db_canonical": True, "workspace_context": workspace_context_payload(root)}
70
+ write_rejected_order(root, order, validation["reasons"])
71
+ write_audit_event(root, {"type": "approval.rejected", "payload": rejected}, principal_id=approved_by, source="service")
72
+ return rejected
73
+ approval_policy = evaluate_policy(root, {"principal_id": approved_by, "action": "approval_receipt.create", "order_intent": order})
74
+ if approval_policy["decision"] != "allow":
75
+ rejected = {"status": "rejected", "order_intent_id": order.get("id"), "reasons": approval_policy["reasons"], "db_canonical": True, "workspace_context": workspace_context_payload(root)}
76
+ write_rejected_order(root, order, approval_policy["reasons"])
77
+ write_audit_event(root, {"type": "approval.rejected", "payload": rejected}, principal_id=approved_by, source="service")
78
+ return rejected
79
+ if approved_by == order.get("created_by"):
80
+ raise ValueError("order creator cannot approve the same order")
81
+ created = datetime.now(timezone.utc)
82
+ receipt = {
83
+ "id": f"approval-{sanitize_id(order['id'])}-{created.strftime('%Y%m%dT%H%M%S%fZ')}",
84
+ "order_intent_id": order["id"],
85
+ "approved_by": approved_by,
86
+ "valid": True,
87
+ "created_at": created.isoformat().replace("+00:00", "Z"),
88
+ "expires_at": (created + timedelta(hours=expires_hours)).isoformat().replace("+00:00", "Z"),
89
+ "policy_decision": validation["policy"],
90
+ }
91
+ receipt_validation = validate_approval_receipt(root, {"order_intent": order, "approval_receipt": receipt})
92
+ if not receipt_validation["valid"]:
93
+ return {"status": "rejected", "order_intent_id": order.get("id"), "reasons": receipt_validation["reasons"], "db_canonical": True, "workspace_context": workspace_context_payload(root)}
94
+ persist_order_intent_if_available(root, order, validation)
95
+ persist_approval_receipt_if_available(root, receipt)
96
+ write_json(root / "trading" / "orders" / "approved" / f"{sanitize_id(order['id'])}.order_intent.json", order)
97
+ write_json(root / "trading" / "approvals" / f"{sanitize_id(order['id'])}.approval_receipt.json", receipt)
98
+ result = {
99
+ "status": "approved",
100
+ "order_intent_id": order["id"],
101
+ "approved_order_path": f"trading/orders/approved/{sanitize_id(order['id'])}.order_intent.json",
102
+ "approval_receipt_path": f"trading/approvals/{sanitize_id(order['id'])}.approval_receipt.json",
103
+ "db_canonical": True,
104
+ "workspace_context": workspace_context_payload(root),
105
+ }
106
+ write_audit_event(root, {"type": "approval.accepted", "payload": result}, principal_id=approved_by, source="service")
107
+ return result
108
+
109
+
110
+ def submit_approved_order(workspace_root: Path | str, args: dict[str, Any]) -> dict[str, Any]:
111
+ root = Path(workspace_root)
112
+ order = resolve_order_intent(root, args)
113
+ receipt = resolve_approval_receipt(root, args, order)
114
+ principal_id = args.get("principal_id") or "execution-operator"
115
+ order_validation = validate_order_intent(root, {"principal_id": principal_id, "order_intent": order})
116
+ receipt_validation = validate_approval_receipt(root, {"order_intent": order, "approval_receipt": receipt})
117
+ policy = evaluate_policy(root, {
118
+ "principal_id": principal_id,
119
+ "action": "mcp.tradingcodex.submit_approved_order",
120
+ "order_intent": order,
121
+ "approval_receipt": receipt,
122
+ "require_approval_check": True,
123
+ })
124
+ if not order_validation["valid"] or not receipt_validation["valid"] or policy["decision"] != "allow":
125
+ rejected = {
126
+ "status": "rejected",
127
+ "order_intent_id": order.get("id"),
128
+ "reasons": order_validation["reasons"] + receipt_validation["reasons"] + policy["reasons"],
129
+ "db_canonical": True,
130
+ "workspace_context": workspace_context_payload(root),
131
+ }
132
+ write_audit_event(root, {"type": "submit_approved_order.rejected", "payload": rejected}, principal_id=principal_id, source="mcp")
133
+ return rejected
134
+ ensure_runtime_database(root)
135
+ from apps.orders.services import finalize_execution_reservation, reserve_execution
136
+
137
+ portfolio_id, account_id, strategy_id = portfolio_keys(order, root)
138
+ reservation = reserve_execution(
139
+ order=order,
140
+ receipt=receipt,
141
+ adapter=order.get("broker", ""),
142
+ portfolio_id=portfolio_id,
143
+ account_id=account_id,
144
+ strategy_id=strategy_id,
145
+ workspace_context=workspace_context_payload(root),
146
+ principal_id=principal_id,
147
+ )
148
+ if not reservation.created:
149
+ rejected = {
150
+ "status": "rejected",
151
+ "order_intent_id": order.get("id"),
152
+ "idempotency_key": reservation.idempotency_key,
153
+ "reasons": [f"order already has an execution result: {reservation.execution.status}"],
154
+ "db_canonical": True,
155
+ "workspace_context": workspace_context_payload(root),
156
+ }
157
+ write_audit_event(root, {"type": "submit_approved_order.duplicate", "payload": rejected}, principal_id=principal_id, source="mcp")
158
+ return rejected
159
+ try:
160
+ adapter_result = submit_with_adapter(root, order)
161
+ except Exception as exc:
162
+ rejected = {
163
+ "status": "rejected",
164
+ "order_intent_id": order.get("id"),
165
+ "adapter": order.get("broker"),
166
+ "reasons": [f"adapter error: {exc}"],
167
+ "db_canonical": True,
168
+ "workspace_context": workspace_context_payload(root),
169
+ }
170
+ finalize_execution_reservation(reservation.execution, rejected)
171
+ write_audit_event(root, {"type": "submit_approved_order.adapter_error", "payload": rejected}, principal_id=principal_id, source="mcp")
172
+ return rejected
173
+ accepted = {"status": "accepted", "order_intent_id": order["id"], "adapter": order["broker"], "idempotency_key": reservation.idempotency_key, "result": adapter_result, "db_canonical": True, "workspace_context": workspace_context_payload(root)}
174
+ persist_order_intent_if_available(root, order)
175
+ persist_approval_receipt_if_available(root, receipt)
176
+ finalize_execution_reservation(reservation.execution, accepted)
177
+ write_json(root / "trading" / "orders" / "executed" / f"{sanitize_id(order['id'])}.execution_result.json", {
178
+ "order_intent_id": order["id"],
179
+ "approval_receipt_id": receipt.get("id"),
180
+ "idempotency_key": reservation.idempotency_key,
181
+ "result": accepted,
182
+ })
183
+ write_audit_event(root, {"type": "submit_approved_order.accepted", "payload": accepted}, principal_id=principal_id, source="mcp")
184
+ return accepted
185
+
186
+
187
+ def submit_with_adapter(root: Path, order: dict[str, Any]) -> dict[str, Any]:
188
+ broker = order.get("broker")
189
+ if broker == "stub-execution":
190
+ return {
191
+ "adapter": "stub-execution",
192
+ "broker_order_id": f"stub-{order['id']}",
193
+ "status": "stubbed",
194
+ "submitted_at": now_iso(),
195
+ "order": order,
196
+ }
197
+ if broker == "paper-trading":
198
+ return submit_paper_order(root, order)
199
+ raise ValueError(f"Adapter is not enabled: {broker}")
200
+
201
+ def resolve_order_intent(root: Path, args: dict[str, Any]) -> dict[str, Any]:
202
+ if isinstance(args.get("order_intent"), dict):
203
+ return args["order_intent"]
204
+ if isinstance(args.get("order"), dict):
205
+ return args["order"]
206
+ if args.get("order_intent_path"):
207
+ return read_json(_resolve_path(root, args["order_intent_path"]), {})
208
+ if args.get("order_intent_id"):
209
+ return find_order_intent_by_id(root, args["order_intent_id"]) or {}
210
+ return {}
211
+
212
+
213
+ def resolve_approval_receipt(root: Path, args: dict[str, Any], order: dict[str, Any] | None = None) -> dict[str, Any]:
214
+ if isinstance(args.get("approval_receipt"), dict):
215
+ return args["approval_receipt"]
216
+ if args.get("approval_receipt_path"):
217
+ return read_json(_resolve_path(root, args["approval_receipt_path"]), {})
218
+ if args.get("approval_receipt_id"):
219
+ return find_approval_receipt_by_id(root, args["approval_receipt_id"]) or {}
220
+ if order and order.get("id"):
221
+ return find_approval_receipt_by_order_id(root, order["id"]) or {}
222
+ return {}
223
+
224
+
225
+ def find_order_intent_by_id(root: Path, order_id: str) -> dict[str, Any] | None:
226
+ try:
227
+ ensure_runtime_database(root)
228
+ from apps.orders.models import OrderIntent
229
+
230
+ stored = OrderIntent.objects.filter(intent_id=order_id).first()
231
+ if stored:
232
+ payload = stored.payload or {}
233
+ if isinstance(payload.get("order_intent"), dict):
234
+ return payload["order_intent"]
235
+ return {
236
+ "id": stored.intent_id,
237
+ "symbol": stored.symbol,
238
+ "side": stored.side,
239
+ "quantity": float(stored.quantity),
240
+ "limit_price": float(stored.limit_price),
241
+ "currency": stored.currency,
242
+ "broker": stored.broker,
243
+ "estimated_notional_krw": float(stored.estimated_notional_krw),
244
+ "created_by": stored.created_by,
245
+ "created_at": stored.created_at.isoformat(),
246
+ "portfolio_id": stored.portfolio_id,
247
+ "account_id": stored.account_id,
248
+ "strategy_id": stored.strategy_id,
249
+ }
250
+ except Exception:
251
+ pass
252
+ for folder in ["approved", "draft", "rejected", "executed"]:
253
+ for path in (root / "trading" / "orders" / folder).glob("*.json"):
254
+ data = read_json(path, {})
255
+ if data.get("id") == order_id or data.get("order_intent", {}).get("id") == order_id or data.get("order_intent_id") == order_id:
256
+ return data.get("order_intent") or data
257
+ return None
258
+
259
+
260
+ def order_intent_payload_conflict(root: Path, order: dict[str, Any]) -> str:
261
+ order_id = order.get("id")
262
+ if not order_id:
263
+ return ""
264
+ try:
265
+ ensure_runtime_database(root)
266
+ from apps.orders.models import OrderIntent
267
+
268
+ stored = OrderIntent.objects.filter(intent_id=order_id).first()
269
+ if stored is None:
270
+ return ""
271
+ payload = stored.payload or {}
272
+ stored_order = payload.get("order_intent") if isinstance(payload.get("order_intent"), dict) else {}
273
+ if stored_order and stable_hash(stored_order) != stable_hash(order):
274
+ return "order_intent.id already exists with a different payload"
275
+ except Exception:
276
+ return ""
277
+ return ""
278
+
279
+
280
+ def find_approval_receipt_by_id(root: Path, receipt_id: str) -> dict[str, Any] | None:
281
+ try:
282
+ ensure_runtime_database(root)
283
+ from apps.orders.models import ApprovalReceipt
284
+
285
+ stored = ApprovalReceipt.objects.filter(receipt_id=receipt_id).first()
286
+ if stored:
287
+ return stored.payload or {}
288
+ except Exception:
289
+ pass
290
+ for path in (root / "trading" / "approvals").glob("*.json"):
291
+ data = read_json(path, {})
292
+ if data.get("id") == receipt_id:
293
+ return data
294
+ return None
295
+
296
+
297
+ def find_approval_receipt_by_order_id(root: Path, order_id: str) -> dict[str, Any] | None:
298
+ try:
299
+ ensure_runtime_database(root)
300
+ from apps.orders.models import ApprovalReceipt
301
+
302
+ stored = ApprovalReceipt.objects.filter(order_intent_id=order_id, valid=True).order_by("-created_at", "-id").first()
303
+ if stored:
304
+ return stored.payload or {}
305
+ except Exception:
306
+ pass
307
+ for path in (root / "trading" / "approvals").glob("*.json"):
308
+ data = read_json(path, {})
309
+ if data.get("order_intent_id") == order_id:
310
+ return data
311
+ return None
312
+
313
+
314
+ def write_rejected_order(root: Path, order: dict[str, Any], reasons: list[str]) -> None:
315
+ write_json(root / "trading" / "orders" / "rejected" / f"{sanitize_id(order.get('id', 'unknown'))}.rejected.json", {
316
+ "order_intent": order,
317
+ "rejected_at": now_iso(),
318
+ "reasons": reasons,
319
+ })
320
+
321
+
322
+ def persist_order_intent_if_available(root: Path, order: dict[str, Any], validation: dict[str, Any] | None = None) -> None:
323
+ required = ["id", "symbol", "side", "quantity", "limit_price", "currency", "broker", "estimated_notional_krw", "created_by", "created_at"]
324
+ if any(order.get(field) in (None, "") for field in required):
325
+ return
326
+ try:
327
+ ensure_runtime_database(root)
328
+ from apps.orders.models import OrderIntent
329
+
330
+ if order_intent_payload_conflict(root, order):
331
+ return
332
+ portfolio_id, account_id, strategy_id = portfolio_keys(order, root)
333
+ OrderIntent.objects.update_or_create(
334
+ intent_id=order["id"],
335
+ defaults={
336
+ "symbol": str(order["symbol"]).upper(),
337
+ "side": order["side"],
338
+ "quantity": order["quantity"],
339
+ "limit_price": order["limit_price"],
340
+ "currency": order["currency"],
341
+ "broker": order["broker"],
342
+ "estimated_notional_krw": order["estimated_notional_krw"],
343
+ "created_by": order["created_by"],
344
+ "created_at": _parse_datetime(order["created_at"]) or datetime.now(timezone.utc),
345
+ "portfolio_id": portfolio_id,
346
+ "account_id": account_id,
347
+ "strategy_id": strategy_id,
348
+ "workspace_context": workspace_context_payload(root),
349
+ "payload": {"order_intent": order, "validation": validation or {}},
350
+ },
351
+ )
352
+ except Exception:
353
+ return
354
+
355
+
356
+ def persist_approval_receipt_if_available(root: Path, receipt: dict[str, Any]) -> None:
357
+ required = ["id", "order_intent_id", "approved_by", "valid", "expires_at"]
358
+ if any(receipt.get(field) in (None, "") for field in required):
359
+ return
360
+ try:
361
+ ensure_runtime_database(root)
362
+ from apps.orders.models import ApprovalReceipt
363
+
364
+ ApprovalReceipt.objects.update_or_create(
365
+ receipt_id=receipt["id"],
366
+ defaults={
367
+ "order_intent_id": receipt["order_intent_id"],
368
+ "approved_by": receipt["approved_by"],
369
+ "valid": bool(receipt["valid"]),
370
+ "expires_at": _parse_datetime(receipt["expires_at"]) or datetime.now(timezone.utc),
371
+ "workspace_context": workspace_context_payload(root),
372
+ "payload": receipt,
373
+ },
374
+ )
375
+ except Exception:
376
+ return
377
+
378
+
379
+ def persist_execution_result_if_available(root: Path, order: dict[str, Any], receipt: dict[str, Any], result: dict[str, Any]) -> None:
380
+ if not order.get("id"):
381
+ return
382
+ try:
383
+ ensure_runtime_database(root)
384
+ from apps.orders.services import execution_idempotency_key
385
+ from apps.orders.models import ExecutionResult
386
+
387
+ portfolio_id, account_id, strategy_id = portfolio_keys(order, root)
388
+ key = str(result.get("idempotency_key") or execution_idempotency_key(order, receipt, portfolio_id, account_id, strategy_id))
389
+ ExecutionResult.objects.update_or_create(
390
+ idempotency_key=key,
391
+ defaults={
392
+ "order_intent_id": order["id"],
393
+ "approval_receipt_id": receipt.get("id", ""),
394
+ "adapter": result.get("adapter") or order.get("broker", ""),
395
+ "status": result.get("status", "recorded"),
396
+ "portfolio_id": portfolio_id,
397
+ "account_id": account_id,
398
+ "strategy_id": strategy_id,
399
+ "workspace_context": workspace_context_payload(root),
400
+ "payload": result,
401
+ },
402
+ )
403
+ except Exception:
404
+ return