Skip to content

Commit 15ace9c

Browse files
committed
Release TradingCodex 0.2.0
1 parent 5da416a commit 15ace9c

106 files changed

Lines changed: 5076 additions & 891 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ TradingCodex is a Python/Django local-first trading harness. CLI code lives in `
1616

1717
Target Python `>=3.14,<3.15` and Django `5.2.x`. Use four-space indentation, clear module-level service functions, and type hints where they clarify contracts. Admin, Django Ninja, MCP, generated hooks, and CLI code should call shared application services rather than duplicating policy, approval, research, order, portfolio, audit, or harness logic. Research artifacts and source snapshots are workspace-file-native, not Django DB models. Prefer direct canonical imports over pre-release compatibility facades. Keep generated workspace template bodies as ordinary files under `workspace_templates/modules/*/files`; use Python for registry loading, dependency resolution, rendering, validation, and generated indexes, not to hide durable prompts, skills, policies, hooks, or workspace-contract content inside string constants.
1818

19+
## Agent Context & Harness Review
20+
21+
When changing or reviewing agent, workflow, MCP, policy, template, or harness behavior, do not infer behavior from Python code alone. Read the relevant harness flow and instruction surfaces first: `docs/harness.md`, `docs/roles-skills-and-workflows.md`, `tradingcodex_service/application/components.py`, and generated workspace files under `workspace_templates/modules/*/files`, especially `.agents/skills/*/SKILL.md`, `.codex/agents/*.toml`, `.codex/prompts/*`, `.codex/hooks/*`, `.tradingcodex/policies/*`, and `.tradingcodex/workflows/*`. Treat skill bodies, role TOML, hooks, policies, docs, and service-layer code as one product contract; keep them aligned when routing, permissions, role boundaries, quality gates, or workflow behavior changes.
22+
1923
## Testing Guidelines
2024

2125
Use pytest; test files and functions should follow `test_*.py` and `test_*`. Run focused tests while iterating, then `python -m pytest` before handoff. Template or bootstrap changes must regenerate a clean workspace and run `./tcx doctor`. Research-memory changes should verify file-native create, search, source-snapshot, and export flows. MCP changes should verify `tools/list`.

README.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ execution-sensitive state lives in the central local runtime DB:
6060

6161
## What TradingCodex Does
6262

63-
TradingCodex `0.1.0` provides:
63+
TradingCodex `0.2.0` provides:
6464

6565
- A generated Codex workspace with fixed role topology, project-scoped MCP,
6666
local wrappers, workspace manifest, generated policy/config files, and
@@ -79,14 +79,20 @@ TradingCodex `0.1.0` provides:
7979
role boundaries.
8080
- Policy, restricted-symbol, approval, order, execution, portfolio, MCP, and
8181
audit services behind a shared Django application layer.
82-
- Experimental paper/stub order lifecycle support: validate intent, create
83-
approval receipt, submit approved order, cancel approved order, and record
82+
- Broker Center foundations: broker connections, broker accounts, read-only
83+
paper sync, external MCP broker discovery import, and reconciliation
84+
summaries behind the service layer.
85+
- Canonical Order Ticket foundations: natural-language or structured draft
86+
tickets, schema/policy/cash-position/broker validation checks, state-machine
87+
events, exact approval-scope binding, broker order timeline, and paper fills.
88+
- Experimental paper/stub order lifecycle support: check order tickets, request
89+
approval receipts, submit approved orders, cancel approved orders, and record
8490
outcomes.
8591
- A local product web surface for browsing agents, skills, research markdown,
86-
external MCP router metadata, starter prompts, and operational state.
92+
external MCP Gate metadata, starter prompts, and operational state.
8793
- Django Admin, Django Ninja API, CLI, and MCP surfaces that call the same
8894
service-layer logic instead of creating parallel execution paths.
89-
- A managed external MCP router gate that imports discovery metadata,
95+
- A managed External MCP Gate that imports discovery metadata,
9096
classifies tool risk, scopes role access, and blocks unsafe direct proxy
9197
paths by default.
9298

@@ -161,9 +167,10 @@ http://127.0.0.1:48267/
161167
```
162168

163169
The dashboard is a review and control surface for the agent roster, required
164-
and optional skills, strategy skills, research markdown, external MCP router
165-
metadata, starter prompts, policy/order/portfolio/activity status, and local
166-
workspace state. It does not spawn Codex agents, approve orders, submit
170+
and optional skills, strategy skills, research markdown, Broker Center,
171+
External MCP Gate metadata, portfolio sync/reconciliation, order-ticket
172+
drafts/checks, starter prompts, policy/order/portfolio/activity status, and
173+
local workspace state. It does not spawn Codex agents, approve orders, submit
167174
executions, or provide investment recommendations.
168175

169176
For CLI-only sessions, start it manually:
@@ -185,11 +192,11 @@ TradingCodex is designed around handoffs rather than one giant answer:
185192
3. Analysts create evidence-backed artifacts with source/as-of posture.
186193
4. Downstream roles consume accepted upstream artifacts instead of silently
187194
filling missing work outside their role.
188-
5. Portfolio and risk roles review fit, sizing, limits, restricted symbols,
189-
and approval readiness.
190-
6. If an executable paper/stub action is requested, `risk-manager` creates the
191-
approval receipt and `execution-operator` submits only through TradingCodex
192-
MCP.
195+
5. Portfolio and risk roles review fit, sizing, broker sync/reconciliation,
196+
limits, restricted symbols, order-ticket checks, and approval readiness.
197+
6. If an executable paper/stub action is requested, `risk-manager` creates an
198+
approval receipt bound to the exact order payload hash and
199+
`execution-operator` submits only through TradingCodex MCP.
193200
7. Policy decisions, MCP calls, approvals, execution results, and audit events
194201
remain inspectable through local service surfaces.
195202

@@ -219,7 +226,7 @@ application services:
219226

220227
| Surface | Role |
221228
| --- | --- |
222-
| Product web | Agents-first review dashboard for roles, skills, research markdown, external MCP router review, starter prompts, and local status. |
229+
| Product web | Agents-first review dashboard for roles, skills, research markdown, External MCP Gate lifecycle/review, starter prompts, and local status. |
223230
| Django Admin | Local/staff DB inspection for policy, orders, portfolio, MCP registry, workflows, integrations, and audit rows. |
224231
| Django Ninja API | Typed local/staff REST and control endpoints. |
225232
| MCP | Agent/tool boundary with typed tools, role scopes, policy checks, and audit. |
@@ -276,7 +283,7 @@ labels such as `research-only`, `screen-grade`, `not-decision-ready`, or
276283

277284
## Release Status
278285

279-
`0.1.0` is the first stable public contract for the generated workspace, Python
286+
`0.2.0` is the OrderTicket rewrite release for the generated workspace, Python
280287
CLI, Django service plane, product web, MCP boundary, and documentation set.
281288
The package still uses an alpha development classifier because live broker
282289
adapters and hosted service modes are intentionally outside the initial core.

apps/integrations/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.contrib import admin
22

3-
from apps.integrations.models import AdapterDefinition
3+
from apps.integrations.models import AdapterDefinition, BrokerAccount, BrokerConnection, InstrumentMap
44

55

6-
admin.site.register(AdapterDefinition)
6+
admin.site.register([AdapterDefinition, BrokerAccount, BrokerConnection, InstrumentMap])
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Generated by Django 5.2.5 on 2026-06-14 07:14
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('integrations', '0001_initial'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='BrokerConnection',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('broker_id', models.CharField(max_length=120, unique=True)),
19+
('display_name', models.CharField(max_length=160)),
20+
('transport', models.CharField(default='paper', max_length=32)),
21+
('adapter_type', models.CharField(default='paper', max_length=64)),
22+
('status', models.CharField(default='read_only', max_length=32)),
23+
('credential_ref', models.CharField(blank=True, max_length=255)),
24+
('capabilities', models.JSONField(blank=True, default=list)),
25+
('enabled_read_scopes', models.JSONField(blank=True, default=list)),
26+
('enabled_trade_scopes', models.JSONField(blank=True, default=list)),
27+
('trust_level', models.CharField(default='unreviewed', max_length=32)),
28+
('last_sync_at', models.DateTimeField(blank=True, null=True)),
29+
('last_health_status', models.CharField(default='not_checked', max_length=32)),
30+
('drift_status', models.CharField(default='none', max_length=32)),
31+
('metadata', models.JSONField(blank=True, default=dict)),
32+
('created_at', models.DateTimeField(auto_now_add=True)),
33+
('updated_at', models.DateTimeField(auto_now=True)),
34+
],
35+
options={
36+
'verbose_name': 'Broker connection',
37+
'verbose_name_plural': 'Broker connections',
38+
'ordering': ['display_name', 'broker_id'],
39+
},
40+
),
41+
migrations.CreateModel(
42+
name='BrokerAccount',
43+
fields=[
44+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
45+
('broker_account_id', models.CharField(max_length=160)),
46+
('account_label', models.CharField(blank=True, max_length=160)),
47+
('account_type', models.CharField(default='paper', max_length=64)),
48+
('base_currency', models.CharField(default='KRW', max_length=16)),
49+
('masked_identifier', models.CharField(blank=True, max_length=120)),
50+
('trading_enabled', models.BooleanField(default=False)),
51+
('discovered_at', models.DateTimeField(auto_now_add=True)),
52+
('last_seen_at', models.DateTimeField(blank=True, null=True)),
53+
('metadata', models.JSONField(blank=True, default=dict)),
54+
('broker_connection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='integrations.brokerconnection')),
55+
],
56+
options={
57+
'verbose_name': 'Broker account',
58+
'verbose_name_plural': 'Broker accounts',
59+
'ordering': ['broker_connection__display_name', 'broker_account_id'],
60+
'constraints': [models.UniqueConstraint(fields=('broker_connection', 'broker_account_id'), name='unique_broker_account_per_connection')],
61+
},
62+
),
63+
migrations.CreateModel(
64+
name='InstrumentMap',
65+
fields=[
66+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
67+
('canonical_symbol', models.CharField(max_length=64)),
68+
('broker_symbol', models.CharField(max_length=120)),
69+
('exchange', models.CharField(blank=True, max_length=64)),
70+
('asset_type', models.CharField(default='equity', max_length=64)),
71+
('currency', models.CharField(default='KRW', max_length=16)),
72+
('tick_size', models.DecimalField(blank=True, decimal_places=8, max_digits=20, null=True)),
73+
('lot_size', models.DecimalField(blank=True, decimal_places=8, max_digits=20, null=True)),
74+
('min_order_quantity', models.DecimalField(blank=True, decimal_places=8, max_digits=20, null=True)),
75+
('metadata', models.JSONField(blank=True, default=dict)),
76+
('broker_connection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instrument_maps', to='integrations.brokerconnection')),
77+
],
78+
options={
79+
'verbose_name': 'Instrument map',
80+
'verbose_name_plural': 'Instrument maps',
81+
'ordering': ['canonical_symbol', 'broker_symbol'],
82+
'constraints': [models.UniqueConstraint(fields=('broker_connection', 'canonical_symbol', 'broker_symbol'), name='unique_instrument_map_per_broker')],
83+
},
84+
),
85+
]

apps/integrations/models.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,84 @@ class Meta:
1414

1515
def __str__(self) -> str:
1616
return self.adapter_id
17+
18+
19+
class BrokerConnection(models.Model):
20+
broker_id = models.CharField(max_length=120, unique=True)
21+
display_name = models.CharField(max_length=160)
22+
transport = models.CharField(max_length=32, default="paper")
23+
adapter_type = models.CharField(max_length=64, default="paper")
24+
status = models.CharField(max_length=32, default="read_only")
25+
credential_ref = models.CharField(max_length=255, blank=True)
26+
capabilities = models.JSONField(default=list, blank=True)
27+
enabled_read_scopes = models.JSONField(default=list, blank=True)
28+
enabled_trade_scopes = models.JSONField(default=list, blank=True)
29+
trust_level = models.CharField(max_length=32, default="unreviewed")
30+
last_sync_at = models.DateTimeField(null=True, blank=True)
31+
last_health_status = models.CharField(max_length=32, default="not_checked")
32+
drift_status = models.CharField(max_length=32, default="none")
33+
metadata = models.JSONField(default=dict, blank=True)
34+
created_at = models.DateTimeField(auto_now_add=True)
35+
updated_at = models.DateTimeField(auto_now=True)
36+
37+
class Meta:
38+
ordering = ["display_name", "broker_id"]
39+
verbose_name = "Broker connection"
40+
verbose_name_plural = "Broker connections"
41+
42+
def __str__(self) -> str:
43+
return f"{self.display_name} ({self.status})"
44+
45+
46+
class BrokerAccount(models.Model):
47+
broker_connection = models.ForeignKey(BrokerConnection, on_delete=models.CASCADE, related_name="accounts")
48+
broker_account_id = models.CharField(max_length=160)
49+
account_label = models.CharField(max_length=160, blank=True)
50+
account_type = models.CharField(max_length=64, default="paper")
51+
base_currency = models.CharField(max_length=16, default="KRW")
52+
masked_identifier = models.CharField(max_length=120, blank=True)
53+
trading_enabled = models.BooleanField(default=False)
54+
discovered_at = models.DateTimeField(auto_now_add=True)
55+
last_seen_at = models.DateTimeField(null=True, blank=True)
56+
metadata = models.JSONField(default=dict, blank=True)
57+
58+
class Meta:
59+
ordering = ["broker_connection__display_name", "broker_account_id"]
60+
constraints = [
61+
models.UniqueConstraint(
62+
fields=["broker_connection", "broker_account_id"],
63+
name="unique_broker_account_per_connection",
64+
)
65+
]
66+
verbose_name = "Broker account"
67+
verbose_name_plural = "Broker accounts"
68+
69+
def __str__(self) -> str:
70+
return self.account_label or self.broker_account_id
71+
72+
73+
class InstrumentMap(models.Model):
74+
canonical_symbol = models.CharField(max_length=64)
75+
broker_symbol = models.CharField(max_length=120)
76+
broker_connection = models.ForeignKey(BrokerConnection, on_delete=models.CASCADE, related_name="instrument_maps")
77+
exchange = models.CharField(max_length=64, blank=True)
78+
asset_type = models.CharField(max_length=64, default="equity")
79+
currency = models.CharField(max_length=16, default="KRW")
80+
tick_size = models.DecimalField(max_digits=20, decimal_places=8, null=True, blank=True)
81+
lot_size = models.DecimalField(max_digits=20, decimal_places=8, null=True, blank=True)
82+
min_order_quantity = models.DecimalField(max_digits=20, decimal_places=8, null=True, blank=True)
83+
metadata = models.JSONField(default=dict, blank=True)
84+
85+
class Meta:
86+
ordering = ["canonical_symbol", "broker_symbol"]
87+
constraints = [
88+
models.UniqueConstraint(
89+
fields=["broker_connection", "canonical_symbol", "broker_symbol"],
90+
name="unique_instrument_map_per_broker",
91+
)
92+
]
93+
verbose_name = "Instrument map"
94+
verbose_name_plural = "Instrument maps"
95+
96+
def __str__(self) -> str:
97+
return f"{self.canonical_symbol} -> {self.broker_symbol}"

0 commit comments

Comments
 (0)