Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions FUND.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Fund Repository Format

This document defines the exact on-disk shape of a broker-cli fund observability repository.

The broker setup flow can create this repository automatically, and broker-daemon keeps it updated during trading.

## Directory Layout

```text
<fund-dir>/
config.json
fills.json
cash_events.json
decisions/
<timestamp>.md
```

Notes:
- `<fund-dir>` is configured in broker config at `broker.observability.fund_dir`.
- Decision filenames are timestamp IDs (for example: `20260220T153012123456Z.md`).

## `config.json`

Fund metadata and initialization parameters.

Example:

```json
{
"name": "Atlas Fund",
"slug": "atlas",
"inception": "2026-02-20T15:12:30Z",
"currency": "USD",
"initialCapital": 100000.0,
"benchmarks": [],
"cashInterestPolicy": {
"enabled": true,
"source": "inferred_from_broker_cash_balance"
}
}
```

Fields:
- `name: string`
- `slug: string`
- `inception: string` (ISO-8601 timestamp)
- `currency: "USD"`
- `initialCapital: number`
- `benchmarks: string[]`
- `cashInterestPolicy.enabled: boolean`
- `cashInterestPolicy.source: string`

## `fills.json`

Append-only array of executed fills.

Example entry:

```json
{
"id": "fill-001",
"symbol": "NVDA",
"side": "buy",
"qty": 50,
"price": 480.25,
"commission": 1.0,
"timestamp": "2026-02-20T15:16:07.184321+00:00",
"decisionId": "20260220T151530112233Z"
}
```

Fields:
- `id: string` (unique fill identifier, dedup key)
- `symbol: string`
- `side: "buy" | "sell"`
- `qty: number`
- `price: number`
- `commission: number`
- `timestamp: string` (ISO-8601 timestamp)
- `decisionId: string | null`

## `cash_events.json`

Append-only array of cash adjustments not represented by trade notional directly.

Current event type:
- `interest` (cash interest inferred from broker cash balance reconciliation)

Example entry:

```json
{
"id": "interest-20260220T160001987654Z",
"type": "interest",
"amount": 3.42,
"timestamp": "2026-02-20T16:00:01.987654+00:00",
"source": "inferred_from_broker_cash_balance"
}
```

Fields:
- `id: string`
- `type: string` (currently `"interest"`)
- `amount: number` (positive or negative)
- `timestamp: string` (ISO-8601 timestamp)
- `source: string`

## `decisions/<timestamp>.md`

Markdown decision files with YAML frontmatter.

Example:

```markdown
---
date: 2026-02-20
type: buy
tickers: [NVDA]
title: "Initiate NVDA Position"
summary: "Started a core position after earnings revision."
---

## Thesis

Long-form markdown reasoning content...
```

Frontmatter fields:
- `date: string` (ISO date)
- `type: "buy" | "sell"`
- `tickers: string[]`
- `title: string`
- `summary: string`

Body:
- Free-form markdown reasoning (from `--decision-reasoning`).

## Sync Behavior

When observability sync is enabled:
- Decision files are written when orders are placed.
- Fills are appended on executions.
- Cash interest events are appended when inferred deltas are detected.
- Changes are committed and pushed to `origin` automatically.

Git behavior assumptions:
- `<fund-dir>` is a git repository.
- `origin` remote exists and is pushable from the runtime environment.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ broker daemon status # Check connection
broker quote AAPL MSFT # Get quotes
broker positions # View portfolio
broker exposure --by symbol # Exposure analysis
broker order buy AAPL 100 --limit 185 # Place an order
broker order buy AAPL 100 --limit 185 \
--decision-name "Initiate AAPL Position" \
--decision-summary "Open core position" \
--decision-reasoning "## Thesis\nHigh-conviction setup." # Place an order
```

## Built for Agents
Expand Down Expand Up @@ -73,9 +76,9 @@ broker quote SYMBOL... Snapshot quotes
broker watch SYMBOL Live quote stream
broker chain SYMBOL Option chain with greeks
broker history SYMBOL Historical bars
broker order buy SYMBOL QTY Buy order (market/limit/stop)
broker order sell SYMBOL QTY Sell order
broker order bracket SYMBOL QTY Bracket order (entry + TP + SL)
broker order buy SYMBOL QTY Buy order (requires --decision-name/--decision-summary/--decision-reasoning)
broker order sell SYMBOL QTY Sell order (requires --decision-name/--decision-summary/--decision-reasoning)
broker order bracket SYMBOL QTY Bracket order (entry + TP + SL, requires decision flags)
broker order status ORDER_ID Order status
broker orders List orders
broker cancel ORDER_ID Cancel an order
Expand Down
90 changes: 90 additions & 0 deletions cli/src/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ def buy(
"--idempotency-key",
help="Stable key for safe retries (maps to client_order_id).",
),
decision_name: str = typer.Option(
...,
"--decision-name",
help="Required title-case plain text decision title.",
),
decision_summary: str = typer.Option(
...,
"--decision-summary",
help="Required single-line plain text summary.",
),
decision_reasoning: str = typer.Option(
...,
"--decision-reasoning",
help="Required long-form markdown reasoning.",
),
) -> None:
_place(
ctx,
Expand All @@ -45,6 +60,9 @@ def buy(
tif=tif,
dry_run=dry_run,
idempotency_key=idempotency_key,
decision_name=decision_name,
decision_summary=decision_summary,
decision_reasoning=decision_reasoning,
)


Expand All @@ -62,6 +80,21 @@ def sell(
"--idempotency-key",
help="Stable key for safe retries (maps to client_order_id).",
),
decision_name: str = typer.Option(
...,
"--decision-name",
help="Required title-case plain text decision title.",
),
decision_summary: str = typer.Option(
...,
"--decision-summary",
help="Required single-line plain text summary.",
),
decision_reasoning: str = typer.Option(
...,
"--decision-reasoning",
help="Required long-form markdown reasoning.",
),
) -> None:
_place(
ctx,
Expand All @@ -73,6 +106,9 @@ def sell(
tif=tif,
dry_run=dry_run,
idempotency_key=idempotency_key,
decision_name=decision_name,
decision_summary=decision_summary,
decision_reasoning=decision_reasoning,
)


Expand All @@ -86,9 +122,27 @@ def bracket(
sl: float = typer.Option(..., "--sl", help="Stop-loss price."),
side: Side = typer.Option(Side.BUY, "--side", case_sensitive=False, help="buy or sell."),
tif: TIF = typer.Option(TIF.DAY, "--tif", case_sensitive=False, help="DAY, GTC, IOC."),
decision_name: str = typer.Option(
...,
"--decision-name",
help="Required title-case plain text decision title.",
),
decision_summary: str = typer.Option(
...,
"--decision-summary",
help="Required single-line plain text summary.",
),
decision_reasoning: str = typer.Option(
...,
"--decision-reasoning",
help="Required long-form markdown reasoning.",
),
) -> None:
state = get_state(ctx)
command = "order.bracket"
decision_name = _normalize_decision_name(decision_name)
decision_summary = _normalize_single_line(decision_summary, "decision summary")
decision_reasoning = _normalize_required_text(decision_reasoning, "decision reasoning")
try:
result = run_async(
daemon_request(
Expand All @@ -102,6 +156,9 @@ def bracket(
"sl": sl,
"side": side.value,
"tif": tif.value,
"decision_name": decision_name,
"decision_summary": decision_summary,
"decision_reasoning": decision_reasoning,
},
)
)
Expand Down Expand Up @@ -238,14 +295,23 @@ def _place(
tif: TIF,
dry_run: bool,
idempotency_key: str | None,
decision_name: str,
decision_summary: str,
decision_reasoning: str,
) -> None:
state = get_state(ctx)
command = "order.place"
decision_name = _normalize_decision_name(decision_name)
decision_summary = _normalize_single_line(decision_summary, "decision summary")
decision_reasoning = _normalize_required_text(decision_reasoning, "decision reasoning")
params: dict[str, object] = {
"side": side,
"symbol": symbol,
"qty": qty,
"tif": tif.value,
"decision_name": decision_name,
"decision_summary": decision_summary,
"decision_reasoning": decision_reasoning,
}
if limit is not None:
params["limit"] = limit
Expand All @@ -267,3 +333,27 @@ def _place(
)
except BrokerError as exc:
handle_error(exc, json_output=state.json_output, command=command, strict=state.strict)


def _normalize_required_text(value: str, label: str) -> str:
out = value.strip()
if not out:
raise typer.BadParameter(f"{label} is required")
return out


def _normalize_single_line(value: str, label: str) -> str:
out = _normalize_required_text(value, label)
if "\n" in out or "\r" in out:
raise typer.BadParameter(f"{label} must be single-line plain text")
return out


def _normalize_decision_name(value: str) -> str:
out = _normalize_single_line(value, "decision name")
words = [part for part in out.split(" ") if part]
if not words:
raise typer.BadParameter("decision name is required")
if not all(word[0].isupper() for word in words if word and word[0].isalpha()):
raise typer.BadParameter("decision name must be title case plain text")
return out
54 changes: 51 additions & 3 deletions cli/tests/test_cli/test_command_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,57 @@ def test_subcommand_surface_contract(
(["capabilities"], "market.capabilities"),
(["chain", "AAPL"], "market.chain"),
(["history", "AAPL", "--period", "1d", "--bar", "1m"], "market.history"),
(["order", "buy", "AAPL", "1"], "order.place"),
(["order", "sell", "AAPL", "1"], "order.place"),
(["order", "bracket", "AAPL", "1", "--entry", "100", "--tp", "120", "--sl", "95"], "order.bracket"),
(
[
"order",
"buy",
"AAPL",
"1",
"--decision-name",
"Initiate AAPL Position",
"--decision-summary",
"Start core position",
"--decision-reasoning",
"## Thesis\nBuy quality growth.",
],
"order.place",
),
(
[
"order",
"sell",
"AAPL",
"1",
"--decision-name",
"Trim AAPL Position",
"--decision-summary",
"Reduce exposure",
"--decision-reasoning",
"## Thesis\nLock gains.",
],
"order.place",
),
(
[
"order",
"bracket",
"AAPL",
"1",
"--entry",
"100",
"--tp",
"120",
"--sl",
"95",
"--decision-name",
"Open AAPL Bracket",
"--decision-summary",
"Enter with defined risk",
"--decision-reasoning",
"## Plan\nUse bracket controls.",
],
"order.bracket",
),
(["order", "status", "cid-1"], "order.status"),
(["orders"], "orders.list"),
(["cancel", "cid-1"], "order.cancel"),
Expand Down
Loading