@blockrun/franklin 3.19.0 → 3.20.0
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.
- package/dist/skills-bundled/trade-discussion/SKILL.md +70 -0
- package/dist/skills-bundled/trade-signal/SKILL.md +79 -0
- package/dist/skills-bundled/trade-strategy/SKILL.md +91 -0
- package/dist/tools/trading-execute.js +79 -7
- package/dist/trading/journal-display.d.ts +16 -0
- package/dist/trading/journal-display.js +53 -0
- package/dist/trading/journal-quality.d.ts +42 -0
- package/dist/trading/journal-quality.js +109 -0
- package/dist/trading/trade-log.d.ts +37 -0
- package/package.json +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trade-discussion
|
|
3
|
+
description: Record market commentary, hypotheses, or open observations as a structured note — no trade fired, no position sized. Lighter than /trade-strategy. Saves to ~/.blockrun/notes/ for future reference. Use when the user wants to think out loud about the market.
|
|
4
|
+
triggers:
|
|
5
|
+
- "market commentary"
|
|
6
|
+
- "trade discussion"
|
|
7
|
+
- "observation"
|
|
8
|
+
- "just thinking"
|
|
9
|
+
- "market thought"
|
|
10
|
+
- "log a thought"
|
|
11
|
+
argument-hint: <topic>
|
|
12
|
+
cost-receipt: false
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
You are running inside Franklin on **{{wallet_chain}}**. This skill is the lightest of the three trading skills — no trade, no full strategy doc, just a short observation with enough structure to be useful later.
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
1. **Gather context only if cheap or asked.** Don't burn $0.005 on a chain query for a discussion note unless the user requested data. Default: write the note from what you already know.
|
|
20
|
+
|
|
21
|
+
2. **Write the note.** Use the `Write` tool to create:
|
|
22
|
+
|
|
23
|
+
- Path: `~/.blockrun/notes/<YYYY-MM-DD>-<slug>-discussion.md`
|
|
24
|
+
- Slug: 3–5-word kebab-case (e.g. `etf-flow-divergence-watch`)
|
|
25
|
+
- Content template:
|
|
26
|
+
|
|
27
|
+
```markdown
|
|
28
|
+
# Discussion — <Title>
|
|
29
|
+
|
|
30
|
+
**Date:** <YYYY-MM-DD>
|
|
31
|
+
**Author:** Franklin (skill: /trade-discussion)
|
|
32
|
+
|
|
33
|
+
## Observation
|
|
34
|
+
|
|
35
|
+
<1–3 paragraphs.>
|
|
36
|
+
|
|
37
|
+
## Symbols mentioned
|
|
38
|
+
|
|
39
|
+
`<SYMBOL1>` `<SYMBOL2>` (etc.)
|
|
40
|
+
|
|
41
|
+
## Tags
|
|
42
|
+
|
|
43
|
+
`<sentiment>` `<watch>` `<thesis-fragment>` (etc.)
|
|
44
|
+
|
|
45
|
+
## Open questions
|
|
46
|
+
|
|
47
|
+
- <Question 1 — what would confirm or invalidate this?>
|
|
48
|
+
- <Question 2>
|
|
49
|
+
|
|
50
|
+
## Possible follow-ups
|
|
51
|
+
|
|
52
|
+
- `/surf-market` for <specific data>
|
|
53
|
+
- `/surf-social` for <specific KOL or mindshare check>
|
|
54
|
+
- `/trade-strategy` if this matures into a plan
|
|
55
|
+
- `/trade-signal` if a clear entry emerges
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
3. **Confirm.** Report back: "Note saved to `<path>`. Tagged `<tags>`. No trade fired. If this hardens into a plan, run `/trade-strategy <topic>`."
|
|
59
|
+
|
|
60
|
+
4. **Do not call `TradingOpenPosition`.** Discussion notes are explicitly trade-free.
|
|
61
|
+
|
|
62
|
+
## When this skill fits vs the alternatives
|
|
63
|
+
|
|
64
|
+
- **`/trade-discussion`** — open-ended observation, hypothesis, "what if". Cheapest, least structured.
|
|
65
|
+
- **`/trade-strategy`** — committed plan with entry/exit/sizing. Reach for this when an observation has hardened.
|
|
66
|
+
- **`/trade-signal`** — the actual trade. Fires `TradingOpenPosition`, hits the wallet (paper trading) and the journal.
|
|
67
|
+
|
|
68
|
+
## The user said
|
|
69
|
+
|
|
70
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trade-signal
|
|
3
|
+
description: Open a paper-trade position with a structured rationale — direction, price target, stop, time horizon, conviction, evidence, tags, thesis. The journal scores the trade on discipline (verifiability, evidence, specificity, novelty, review), not P&L, and surfaces the trend in TradingPortfolio. Use any time the user wants to open a position with intent, not vibes.
|
|
4
|
+
triggers:
|
|
5
|
+
- "open a trade"
|
|
6
|
+
- "buy"
|
|
7
|
+
- "long"
|
|
8
|
+
- "short"
|
|
9
|
+
- "take a position"
|
|
10
|
+
- "trade signal"
|
|
11
|
+
- "trade idea"
|
|
12
|
+
argument-hint: <symbol or thesis>
|
|
13
|
+
cost-receipt: false
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
You are running inside Franklin on **{{wallet_chain}}**. This is paper trading — fills are simulated against a live mark — so the value is the discipline, not the dollars. Every trade entered through this skill carries a rationale that the journal scorer evaluates on five dimensions:
|
|
17
|
+
|
|
18
|
+
| Dimension | Weight | Earned by |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| verifiability | 30% | direction + priceTarget both set |
|
|
21
|
+
| evidence | 25% | thesis ≥ 200 chars + 3 evidence items + indicator keywords (RSI/MACD/funding/etc.) |
|
|
22
|
+
| specificity | 20% | symbol + ≥ 2 tags |
|
|
23
|
+
| novelty | 15% | not the 4th identical revenge-trade this week |
|
|
24
|
+
| review | 10% | post-trade note left at close |
|
|
25
|
+
|
|
26
|
+
Total is a 0–5 score, persisted with the trade and averaged across the last 10 entries in the portfolio footer.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
1. **Read the request.** The user's argument is below under "The user said". If it's a complete thesis (symbol + direction + reasoning + numbers), proceed to step 3. If anything's vague, ask **one** clarifying question — the cheapest call you have on the wallet is "tell me more before I burn $0.001 on a market quote."
|
|
31
|
+
|
|
32
|
+
2. **Optional context** — if you don't already have a recent quote, call `TradingMarket({ ticker, assetClass })` (free for crypto, $0.001 for stocks). For thesis support beyond price, the `/surf-market`, `/surf-chain`, or `/surf-social` skills can be invoked, each documenting their own endpoint costs.
|
|
33
|
+
|
|
34
|
+
3. **Construct the rationale.** Fill as many fields as the request justifies:
|
|
35
|
+
|
|
36
|
+
- `direction`: `"long"` for buys (paper trading is long-only today).
|
|
37
|
+
- `priceTarget`: where you expect to take profit (USD).
|
|
38
|
+
- `stopLoss`: where you'll exit if wrong (USD).
|
|
39
|
+
- `timeHorizon`: `"1h"`, `"1d"`, `"1w"`, `"1m"`, `"3m"` — match the trade type.
|
|
40
|
+
- `conviction`: 1 (low, "small probe") → 5 (high, "size up").
|
|
41
|
+
- `evidence`: 2–4 items. Indicator readings, news links, on-chain stats, comparable trades.
|
|
42
|
+
- `tags`: 2+ categories — `"momentum"`, `"mean-reversion"`, `"macro"`, `"event"`, `"sentiment"`, etc.
|
|
43
|
+
- `thesis`: a paragraph (target 200+ chars) connecting the evidence to the trade. Mention at least one named indicator if you cite one.
|
|
44
|
+
|
|
45
|
+
4. **Size with discipline.** Default per-position cap is $400, total exposure $900 (see TradingPortfolio for current utilization). Don't size beyond what conviction justifies — a conviction-2 trade at the cap is a discipline red flag.
|
|
46
|
+
|
|
47
|
+
5. **Fire the trade** by calling `TradingOpenPosition`:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
TradingOpenPosition({
|
|
51
|
+
symbol: "<TICKER>",
|
|
52
|
+
qty: <quantity>,
|
|
53
|
+
priceUsd: <fill price>,
|
|
54
|
+
rationale: {
|
|
55
|
+
direction: "long",
|
|
56
|
+
priceTarget: <number>,
|
|
57
|
+
stopLoss: <number>,
|
|
58
|
+
timeHorizon: "<period>",
|
|
59
|
+
conviction: <1-5>,
|
|
60
|
+
evidence: ["<source 1>", "<source 2>", ...],
|
|
61
|
+
tags: ["<tag 1>", "<tag 2>"],
|
|
62
|
+
thesis: "<200+ char paragraph>"
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
6. **Surface the score.** The tool result shows the fill. Then call `TradingPortfolio` once and quote the new discipline score back to the user — "Trade booked. Journal score on this entry: 4.2/5. Discipline trend over the last 10 trades: 3.6/5 (evidence flagged below 3 — keep citing indicators)."
|
|
68
|
+
|
|
69
|
+
7. **Stop.** Don't fan out to multiple trades unless the user explicitly asks for portfolio construction. One disciplined trade beats five vibes-trades.
|
|
70
|
+
|
|
71
|
+
## Anti-patterns
|
|
72
|
+
|
|
73
|
+
- Firing `TradingOpenPosition` without a rationale block. The journal still records the trade but it scores ~1/5 on discipline. Don't do this.
|
|
74
|
+
- Inventing evidence. If the user says "feels like a top" and you can't find supporting data, write that into the thesis verbatim and let the score reflect it. The journal is a mirror, not a press release.
|
|
75
|
+
- Trading the same symbol + direction four times in a week. The novelty penalty fires for a reason — that's revenge trading.
|
|
76
|
+
|
|
77
|
+
## The user said
|
|
78
|
+
|
|
79
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trade-strategy
|
|
3
|
+
description: Write a long-form trading strategy document — thesis, entry triggers, exit rules, position sizing, hold horizon, kill criteria. No trade is fired. Saves a structured markdown note to ~/.blockrun/notes/. Use when the user wants to plan an approach before committing capital.
|
|
4
|
+
triggers:
|
|
5
|
+
- "trading strategy"
|
|
6
|
+
- "write a strategy"
|
|
7
|
+
- "strategy doc"
|
|
8
|
+
- "trade plan"
|
|
9
|
+
- "planning a trade"
|
|
10
|
+
argument-hint: <topic or thesis>
|
|
11
|
+
cost-receipt: false
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
You are running inside Franklin on **{{wallet_chain}}**. This skill captures *intent* before *action*. The user wants a written strategy, not an executed trade. Result: a markdown file under `~/.blockrun/notes/` that the user (and future Franklin sessions) can reference when the actual trade fires via `/trade-signal`.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
1. **Clarify if needed.** If the argument is just a ticker ("BTC") without a direction, ask one clarifying question about what the user is trying to do (long, short, range, event-driven).
|
|
19
|
+
|
|
20
|
+
2. **Gather supporting context.** Use the `/surf-market`, `/surf-chain`, or `/surf-social` skills to pull data that informs the strategy. Each surfaces its own cost; mention what you spent at the end.
|
|
21
|
+
|
|
22
|
+
3. **Write the strategy doc.** Use the `Write` tool to create:
|
|
23
|
+
|
|
24
|
+
- Path: `~/.blockrun/notes/<YYYY-MM-DD>-<slug>-strategy.md` (replace `~` with the user's actual home dir; ask if you don't know it, or run `Bash("echo $HOME")` once)
|
|
25
|
+
- Slug: short kebab-case identifier derived from the topic (e.g. `btc-pre-halving-long`)
|
|
26
|
+
- Content template:
|
|
27
|
+
|
|
28
|
+
```markdown
|
|
29
|
+
# Strategy — <Title>
|
|
30
|
+
|
|
31
|
+
**Date:** <YYYY-MM-DD>
|
|
32
|
+
**Author:** Franklin (skill: /trade-strategy)
|
|
33
|
+
**Status:** draft
|
|
34
|
+
|
|
35
|
+
## Thesis
|
|
36
|
+
|
|
37
|
+
<2–4 paragraphs: why this trade now. Cite the data sources you pulled.>
|
|
38
|
+
|
|
39
|
+
## Symbols & direction
|
|
40
|
+
|
|
41
|
+
- Primary: <SYMBOL> <long|short>
|
|
42
|
+
- Hedges (if any): <SYMBOL> <long|short>
|
|
43
|
+
|
|
44
|
+
## Entry triggers
|
|
45
|
+
|
|
46
|
+
- <Specific price levels, indicator readings, or events that must hold>
|
|
47
|
+
- <…>
|
|
48
|
+
|
|
49
|
+
## Position sizing
|
|
50
|
+
|
|
51
|
+
- Per-symbol notional cap: $<N> (vs Franklin's $400 default)
|
|
52
|
+
- Conviction tier: <1–5> — justification: <…>
|
|
53
|
+
|
|
54
|
+
## Exit rules
|
|
55
|
+
|
|
56
|
+
- Take profit: <price | indicator | event>
|
|
57
|
+
- Stop loss: <price | indicator | event>
|
|
58
|
+
- Time stop: exit by <date / horizon> regardless
|
|
59
|
+
|
|
60
|
+
## Kill criteria
|
|
61
|
+
|
|
62
|
+
What invalidates the entire thesis? (Be specific — name a level, a metric, or a market regime change.)
|
|
63
|
+
|
|
64
|
+
## Evidence
|
|
65
|
+
|
|
66
|
+
- <source 1>
|
|
67
|
+
- <source 2>
|
|
68
|
+
- <indicator reading>
|
|
69
|
+
|
|
70
|
+
## Tags
|
|
71
|
+
|
|
72
|
+
`<momentum>` `<macro>` `<event>` (etc.)
|
|
73
|
+
|
|
74
|
+
## Linked trades
|
|
75
|
+
|
|
76
|
+
(Populated as `/trade-signal` invocations reference this doc.)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
4. **Confirm the path.** Report back to the user: "Strategy saved to `<path>`. Open it with your editor or pull it into your next `/trade-signal` call as context. Discipline score on the strategy is captured when the first trade fires."
|
|
80
|
+
|
|
81
|
+
5. **Do not call `TradingOpenPosition`.** This skill is plan-only. The user fires the trade separately via `/trade-signal` when ready.
|
|
82
|
+
|
|
83
|
+
## Anti-patterns
|
|
84
|
+
|
|
85
|
+
- Skipping kill criteria. Every strategy has them; "I'll know when to exit" is not a strategy.
|
|
86
|
+
- Position sizing without conviction-justified caps. A conviction-2 strategy that loads the full $400 cap is theater.
|
|
87
|
+
- Writing a strategy doc and immediately firing a trade. The point of separating the two is to force a pause. If the user wants both, run `/trade-strategy` first, then `/trade-signal` referencing the saved file.
|
|
88
|
+
|
|
89
|
+
## The user said
|
|
90
|
+
|
|
91
|
+
$ARGUMENTS
|
|
@@ -9,7 +9,38 @@
|
|
|
9
9
|
* The split mirrors OpenBB's router/engine/view layering and keeps every
|
|
10
10
|
* layer testable in isolation.
|
|
11
11
|
*/
|
|
12
|
+
import { scoreEntry } from '../trading/journal-quality.js';
|
|
13
|
+
import { renderDisciplineFooter } from '../trading/journal-display.js';
|
|
12
14
|
import { renderOrderBlocked, renderOrderFilled, renderPortfolio, renderPositionClosed, renderTradeHistory, windowToSince, } from './trading-views.js';
|
|
15
|
+
/**
|
|
16
|
+
* Pull a `rationale` object out of the LLM's input safely. Skips fields
|
|
17
|
+
* that don't match the expected shape so a half-filled rationale is still
|
|
18
|
+
* captured (the scorer rewards completeness, doesn't require it).
|
|
19
|
+
*/
|
|
20
|
+
function extractRationale(raw) {
|
|
21
|
+
if (!raw || typeof raw !== 'object')
|
|
22
|
+
return undefined;
|
|
23
|
+
const r = raw;
|
|
24
|
+
const out = {};
|
|
25
|
+
if (r.direction === 'long' || r.direction === 'short' || r.direction === 'neutral')
|
|
26
|
+
out.direction = r.direction;
|
|
27
|
+
if (typeof r.priceTarget === 'number' && r.priceTarget > 0)
|
|
28
|
+
out.priceTarget = r.priceTarget;
|
|
29
|
+
if (typeof r.stopLoss === 'number' && r.stopLoss > 0)
|
|
30
|
+
out.stopLoss = r.stopLoss;
|
|
31
|
+
if (typeof r.timeHorizon === 'string' && r.timeHorizon.trim())
|
|
32
|
+
out.timeHorizon = r.timeHorizon.trim();
|
|
33
|
+
if (typeof r.conviction === 'number' && r.conviction >= 1 && r.conviction <= 5) {
|
|
34
|
+
out.conviction = Math.round(r.conviction);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(r.evidence))
|
|
37
|
+
out.evidence = r.evidence.filter((x) => typeof x === 'string');
|
|
38
|
+
if (Array.isArray(r.tags))
|
|
39
|
+
out.tags = r.tags.filter((x) => typeof x === 'string');
|
|
40
|
+
if (typeof r.thesis === 'string' && r.thesis.trim())
|
|
41
|
+
out.thesis = r.thesis.trim();
|
|
42
|
+
return Object.keys(out).length ? out : undefined;
|
|
43
|
+
}
|
|
13
44
|
function enginePortfolio(engine) {
|
|
14
45
|
return engine.deps.portfolio;
|
|
15
46
|
}
|
|
@@ -44,7 +75,15 @@ export function createTradingCapabilities(deps) {
|
|
|
44
75
|
concurrent: true,
|
|
45
76
|
async execute(_input, _ctx) {
|
|
46
77
|
const snap = await buildPortfolioSnapshot(engine);
|
|
47
|
-
|
|
78
|
+
const base = renderPortfolio(snap, riskConfig);
|
|
79
|
+
if (!tradeLog)
|
|
80
|
+
return { output: base };
|
|
81
|
+
// Journal discipline footer — last 10 scored entries from the log.
|
|
82
|
+
// If no entries carry qualityScore yet (pre-v3.20 history or no rationale
|
|
83
|
+
// ever recorded), renderDisciplineFooter returns null and we skip silently.
|
|
84
|
+
const recent = tradeLog.recent(10);
|
|
85
|
+
const footer = renderDisciplineFooter(recent);
|
|
86
|
+
return { output: footer ? `${base}\n${footer}` : base };
|
|
48
87
|
},
|
|
49
88
|
};
|
|
50
89
|
const tradingOpenPosition = {
|
|
@@ -53,7 +92,10 @@ export function createTradingCapabilities(deps) {
|
|
|
53
92
|
description: 'Open (buy into) a position. Pre-trade risk checks enforce per-position and total ' +
|
|
54
93
|
'exposure caps; a blocked order returns a normal text result with the reason — the ' +
|
|
55
94
|
'agent should read it and try again with a smaller qty if appropriate. This is paper ' +
|
|
56
|
-
'trading: fills are simulated against the provided price.'
|
|
95
|
+
'trading: fills are simulated against the provided price. ' +
|
|
96
|
+
'Optionally pass a `rationale` object documenting why — direction, price target, stop, ' +
|
|
97
|
+
'time horizon, conviction, evidence, tags, thesis. The journal scores entries on ' +
|
|
98
|
+
'rationale completeness (not P&L) and surfaces the discipline trend in TradingPortfolio.',
|
|
57
99
|
input_schema: {
|
|
58
100
|
type: 'object',
|
|
59
101
|
required: ['symbol', 'qty', 'priceUsd'],
|
|
@@ -61,6 +103,21 @@ export function createTradingCapabilities(deps) {
|
|
|
61
103
|
symbol: { type: 'string', description: 'Ticker (e.g., "BTC", "ETH")' },
|
|
62
104
|
qty: { type: 'number', description: 'Quantity in base units (e.g., 0.01 for 0.01 BTC)' },
|
|
63
105
|
priceUsd: { type: 'number', description: 'Price at which to execute, in USD' },
|
|
106
|
+
rationale: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
description: 'Optional — why you are opening this position. Captured in the trade journal and scored on discipline (verifiability, evidence, specificity, novelty, review).',
|
|
109
|
+
properties: {
|
|
110
|
+
direction: { type: 'string', enum: ['long', 'short', 'neutral'], description: 'Trade direction. For paper-trade longs use "long".' },
|
|
111
|
+
priceTarget: { type: 'number', description: 'Expected exit price (USD).' },
|
|
112
|
+
stopLoss: { type: 'number', description: 'Forced exit floor (USD).' },
|
|
113
|
+
timeHorizon: { type: 'string', description: 'How long you expect to hold: "1h", "1d", "1w", "1m", "3m", etc.' },
|
|
114
|
+
conviction: { type: 'number', description: 'How sure are you, 1 (low) to 5 (high).' },
|
|
115
|
+
evidence: { type: 'array', items: { type: 'string' }, description: 'Sources, links, or indicator names supporting the thesis.' },
|
|
116
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Categorization: "momentum", "macro", "mean-reversion", etc.' },
|
|
117
|
+
thesis: { type: 'string', description: 'Free-text reasoning (≥200 chars scores best).' },
|
|
118
|
+
},
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
64
121
|
},
|
|
65
122
|
additionalProperties: false,
|
|
66
123
|
},
|
|
@@ -84,7 +141,8 @@ export function createTradingCapabilities(deps) {
|
|
|
84
141
|
return { output: `No-op: ${outcome.reason}` };
|
|
85
142
|
}
|
|
86
143
|
if (tradeLog) {
|
|
87
|
-
|
|
144
|
+
const rationale = extractRationale(input.rationale);
|
|
145
|
+
const draftEntry = {
|
|
88
146
|
timestamp: Date.now(),
|
|
89
147
|
symbol,
|
|
90
148
|
side: 'buy',
|
|
@@ -92,7 +150,11 @@ export function createTradingCapabilities(deps) {
|
|
|
92
150
|
priceUsd: outcome.fill.priceUsd,
|
|
93
151
|
feeUsd: outcome.fill.feeUsd,
|
|
94
152
|
realizedPnlUsd: 0,
|
|
95
|
-
|
|
153
|
+
rationale,
|
|
154
|
+
};
|
|
155
|
+
const history = tradeLog.all();
|
|
156
|
+
draftEntry.qualityScore = scoreEntry(draftEntry, history);
|
|
157
|
+
tradeLog.append(draftEntry);
|
|
96
158
|
}
|
|
97
159
|
if (onStateChange)
|
|
98
160
|
await onStateChange();
|
|
@@ -110,7 +172,9 @@ export function createTradingCapabilities(deps) {
|
|
|
110
172
|
name: 'TradingClosePosition',
|
|
111
173
|
description: 'Close (sell) an open position, realizing P&L against the average entry price. ' +
|
|
112
174
|
"Omit qty to flatten the position entirely; pass qty to partially reduce. Uses the " +
|
|
113
|
-
"exchange's current mark — no manual price required."
|
|
175
|
+
"exchange's current mark — no manual price required. " +
|
|
176
|
+
'Optionally pass a `review` note documenting whether the trade hit its plan; that ' +
|
|
177
|
+
'boosts the journal discipline score for this entry.',
|
|
114
178
|
input_schema: {
|
|
115
179
|
type: 'object',
|
|
116
180
|
required: ['symbol'],
|
|
@@ -120,6 +184,10 @@ export function createTradingCapabilities(deps) {
|
|
|
120
184
|
type: 'number',
|
|
121
185
|
description: 'Optional — partial size. Omit to close the full position.',
|
|
122
186
|
},
|
|
187
|
+
review: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Optional post-trade note: did the trade hit its target / stop / hypothesis? Boosts the journal "review" score component.',
|
|
190
|
+
},
|
|
123
191
|
},
|
|
124
192
|
additionalProperties: false,
|
|
125
193
|
},
|
|
@@ -145,7 +213,8 @@ export function createTradingCapabilities(deps) {
|
|
|
145
213
|
}
|
|
146
214
|
const tradeRealized = portfolio.realizedPnlUsd - priorRealized;
|
|
147
215
|
if (tradeLog) {
|
|
148
|
-
|
|
216
|
+
const review = typeof input.review === 'string' && input.review.trim() ? input.review.trim() : undefined;
|
|
217
|
+
const draftEntry = {
|
|
149
218
|
timestamp: Date.now(),
|
|
150
219
|
symbol,
|
|
151
220
|
side: 'sell',
|
|
@@ -153,7 +222,10 @@ export function createTradingCapabilities(deps) {
|
|
|
153
222
|
priceUsd: outcome.fill.priceUsd,
|
|
154
223
|
feeUsd: outcome.fill.feeUsd,
|
|
155
224
|
realizedPnlUsd: tradeRealized,
|
|
156
|
-
|
|
225
|
+
review,
|
|
226
|
+
};
|
|
227
|
+
draftEntry.qualityScore = scoreEntry(draftEntry, tradeLog.all());
|
|
228
|
+
tradeLog.append(draftEntry);
|
|
157
229
|
}
|
|
158
230
|
if (onStateChange)
|
|
159
231
|
await onStateChange();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown footer for `TradingPortfolio` — the discipline mirror.
|
|
3
|
+
*
|
|
4
|
+
* Shows the last-N trades' average quality score and flags any component
|
|
5
|
+
* that scored below 3.0 (the threshold AI-Trader uses too). The footer
|
|
6
|
+
* is the only place the discipline metric surfaces today; future
|
|
7
|
+
* releases can drop it into the panel Audit tab too.
|
|
8
|
+
*
|
|
9
|
+
* Pure formatting; takes AggregateScore from journal-quality.ts.
|
|
10
|
+
*/
|
|
11
|
+
import type { TradeLogEntry } from './trade-log.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build the discipline footer markdown for an existing portfolio output.
|
|
14
|
+
* Returns `null` when there's nothing to show (no scored entries yet).
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderDisciplineFooter(entries: TradeLogEntry[]): string | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown footer for `TradingPortfolio` — the discipline mirror.
|
|
3
|
+
*
|
|
4
|
+
* Shows the last-N trades' average quality score and flags any component
|
|
5
|
+
* that scored below 3.0 (the threshold AI-Trader uses too). The footer
|
|
6
|
+
* is the only place the discipline metric surfaces today; future
|
|
7
|
+
* releases can drop it into the panel Audit tab too.
|
|
8
|
+
*
|
|
9
|
+
* Pure formatting; takes AggregateScore from journal-quality.ts.
|
|
10
|
+
*/
|
|
11
|
+
import { aggregateScores } from './journal-quality.js';
|
|
12
|
+
const COMPONENT_WARN_THRESHOLD = 3.0; // out of 5
|
|
13
|
+
function fmt(n) {
|
|
14
|
+
return (Math.round(n * 100) / 100).toFixed(2);
|
|
15
|
+
}
|
|
16
|
+
function flagFor(component) {
|
|
17
|
+
switch (component) {
|
|
18
|
+
case 'averageVerifiability': return 'most trades missing direction or price target';
|
|
19
|
+
case 'averageEvidence': return 'most trades missing thesis or sources';
|
|
20
|
+
case 'averageSpecificity': return 'few tags — trades feel generic';
|
|
21
|
+
case 'averageNovelty': return 'repeating same symbol/direction';
|
|
22
|
+
case 'averageReview': return 'no post-trade notes';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the discipline footer markdown for an existing portfolio output.
|
|
27
|
+
* Returns `null` when there's nothing to show (no scored entries yet).
|
|
28
|
+
*/
|
|
29
|
+
export function renderDisciplineFooter(entries) {
|
|
30
|
+
const agg = aggregateScores(entries);
|
|
31
|
+
if (!agg)
|
|
32
|
+
return null;
|
|
33
|
+
const lines = [];
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push('### Journal discipline');
|
|
36
|
+
lines.push(`Last ${agg.count} scored trade${agg.count === 1 ? '' : 's'}: ` +
|
|
37
|
+
`**${fmt(agg.averageTotal)} / 5**`);
|
|
38
|
+
lines.push('');
|
|
39
|
+
// Each component scaled to 0–5 for display (internal is 0–1).
|
|
40
|
+
const components = [
|
|
41
|
+
{ key: 'averageVerifiability', label: 'verifiability', value: agg.averageVerifiability * 5 },
|
|
42
|
+
{ key: 'averageEvidence', label: 'evidence', value: agg.averageEvidence * 5 },
|
|
43
|
+
{ key: 'averageSpecificity', label: 'specificity', value: agg.averageSpecificity * 5 },
|
|
44
|
+
{ key: 'averageNovelty', label: 'novelty', value: agg.averageNovelty * 5 },
|
|
45
|
+
{ key: 'averageReview', label: 'review', value: agg.averageReview * 5 },
|
|
46
|
+
];
|
|
47
|
+
for (const c of components) {
|
|
48
|
+
const flagged = c.value < COMPONENT_WARN_THRESHOLD;
|
|
49
|
+
const flagText = flagged ? ` ← ${flagFor(c.key)}` : '';
|
|
50
|
+
lines.push(`- ${c.label.padEnd(14)} ${fmt(c.value).padStart(5)}${flagText}`);
|
|
51
|
+
}
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal quality scorer — non-outcome trade discipline metric.
|
|
3
|
+
*
|
|
4
|
+
* Scores each journal entry on how well it was *justified*, not whether
|
|
5
|
+
* it made money. The five components are weighted to reward the same
|
|
6
|
+
* habits a discretionary trader's playbook teaches:
|
|
7
|
+
*
|
|
8
|
+
* verifiability (30%) did the entry name a direction and a price target?
|
|
9
|
+
* evidence (25%) did it cite sources / thesis / indicators?
|
|
10
|
+
* specificity (20%) symbol, tags — not vague vibes?
|
|
11
|
+
* novelty (15%) not the 4th identical revenge-trade this week?
|
|
12
|
+
* review (10%) did the user write a post-trade note?
|
|
13
|
+
*
|
|
14
|
+
* The total is on a 0–5 scale, presented in the portfolio footer so the
|
|
15
|
+
* agent and the user can see the discipline curve over time.
|
|
16
|
+
*
|
|
17
|
+
* Pure function — no I/O, no clock, deterministic given inputs. Used at
|
|
18
|
+
* append time (TradeLog) and at render time (TradingPortfolio).
|
|
19
|
+
*/
|
|
20
|
+
import type { TradeLogEntry, QualityScore } from './trade-log.js';
|
|
21
|
+
/**
|
|
22
|
+
* Score one journal entry against the prior history (used for novelty).
|
|
23
|
+
* `history` should contain entries chronologically before `entry`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scoreEntry(entry: TradeLogEntry, history?: TradeLogEntry[]): QualityScore;
|
|
26
|
+
export interface AggregateScore {
|
|
27
|
+
count: number;
|
|
28
|
+
averageTotal: number;
|
|
29
|
+
averageVerifiability: number;
|
|
30
|
+
averageEvidence: number;
|
|
31
|
+
averageSpecificity: number;
|
|
32
|
+
averageNovelty: number;
|
|
33
|
+
averageReview: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Average the qualityScore fields across a set of entries — used by the
|
|
37
|
+
* portfolio footer to show "your last 10 trades scored 3.2 / 5 on average".
|
|
38
|
+
*
|
|
39
|
+
* Entries without a persisted qualityScore are skipped (back-compat with
|
|
40
|
+
* pre-v3.20 trades). Returns null when there's nothing scored to average.
|
|
41
|
+
*/
|
|
42
|
+
export declare function aggregateScores(entries: TradeLogEntry[]): AggregateScore | null;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journal quality scorer — non-outcome trade discipline metric.
|
|
3
|
+
*
|
|
4
|
+
* Scores each journal entry on how well it was *justified*, not whether
|
|
5
|
+
* it made money. The five components are weighted to reward the same
|
|
6
|
+
* habits a discretionary trader's playbook teaches:
|
|
7
|
+
*
|
|
8
|
+
* verifiability (30%) did the entry name a direction and a price target?
|
|
9
|
+
* evidence (25%) did it cite sources / thesis / indicators?
|
|
10
|
+
* specificity (20%) symbol, tags — not vague vibes?
|
|
11
|
+
* novelty (15%) not the 4th identical revenge-trade this week?
|
|
12
|
+
* review (10%) did the user write a post-trade note?
|
|
13
|
+
*
|
|
14
|
+
* The total is on a 0–5 scale, presented in the portfolio footer so the
|
|
15
|
+
* agent and the user can see the discipline curve over time.
|
|
16
|
+
*
|
|
17
|
+
* Pure function — no I/O, no clock, deterministic given inputs. Used at
|
|
18
|
+
* append time (TradeLog) and at render time (TradingPortfolio).
|
|
19
|
+
*/
|
|
20
|
+
const NOVELTY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21
|
+
const NOVELTY_PENALTY = 0.2; // per duplicate within window
|
|
22
|
+
const EVIDENCE_KEYWORD_REGEX = /\b(rsi|macd|bollinger|sma|ema|volatility|funding|liquidation|etf|on[-\s]?chain|catalyst|earnings|tvl|because|since|due to|supports?|resistance|breakout|breakdown|divergence|oversold|overbought)\b/i;
|
|
23
|
+
function clamp01(n) {
|
|
24
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
25
|
+
return 0;
|
|
26
|
+
if (n >= 1)
|
|
27
|
+
return 1;
|
|
28
|
+
return n;
|
|
29
|
+
}
|
|
30
|
+
function hasIndicatorKeyword(text) {
|
|
31
|
+
if (!text)
|
|
32
|
+
return 0;
|
|
33
|
+
return EVIDENCE_KEYWORD_REGEX.test(text) ? 1 : 0;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Score one journal entry against the prior history (used for novelty).
|
|
37
|
+
* `history` should contain entries chronologically before `entry`.
|
|
38
|
+
*/
|
|
39
|
+
export function scoreEntry(entry, history = []) {
|
|
40
|
+
const r = entry.rationale;
|
|
41
|
+
// ─ verifiability: direction + priceTarget each contribute half ─
|
|
42
|
+
const verifiability = (r?.direction ? 0.5 : 0) +
|
|
43
|
+
(typeof r?.priceTarget === 'number' && r.priceTarget > 0 ? 0.5 : 0);
|
|
44
|
+
// ─ evidence: array length, thesis length, indicator keyword presence ─
|
|
45
|
+
const evidenceArrLen = Array.isArray(r?.evidence) ? r.evidence.length : 0;
|
|
46
|
+
const thesisLen = (r?.thesis ?? '').trim().length;
|
|
47
|
+
const evidence = clamp01(0.4 * Math.min(1, evidenceArrLen / 3) +
|
|
48
|
+
0.4 * Math.min(1, thesisLen / 200) +
|
|
49
|
+
0.2 * hasIndicatorKeyword(r?.thesis));
|
|
50
|
+
// ─ specificity: symbol present + tags present ─
|
|
51
|
+
const tagCount = Array.isArray(r?.tags) ? r.tags.length : 0;
|
|
52
|
+
const specificity = (entry.symbol ? 0.5 : 0) +
|
|
53
|
+
Math.min(1, tagCount / 2) * 0.5;
|
|
54
|
+
// ─ novelty: penalize same symbol + direction within 7d ─
|
|
55
|
+
const sinceCutoff = entry.timestamp - NOVELTY_WINDOW_MS;
|
|
56
|
+
const recentSameCount = history.filter((e) => e.timestamp >= sinceCutoff &&
|
|
57
|
+
e.timestamp < entry.timestamp &&
|
|
58
|
+
e.symbol === entry.symbol &&
|
|
59
|
+
(e.rationale?.direction ?? null) === (r?.direction ?? null)).length;
|
|
60
|
+
const novelty = clamp01(1 - NOVELTY_PENALTY * recentSameCount);
|
|
61
|
+
// ─ review: did the user (or the agent in a follow-up turn) annotate? ─
|
|
62
|
+
const review = entry.review && entry.review.trim().length > 0 ? 1 : 0;
|
|
63
|
+
const total = 5 * (verifiability * 0.30 +
|
|
64
|
+
evidence * 0.25 +
|
|
65
|
+
specificity * 0.20 +
|
|
66
|
+
novelty * 0.15 +
|
|
67
|
+
review * 0.10);
|
|
68
|
+
return {
|
|
69
|
+
total: Math.round(total * 100) / 100, // 2 decimal places
|
|
70
|
+
verifiability,
|
|
71
|
+
evidence,
|
|
72
|
+
specificity,
|
|
73
|
+
novelty,
|
|
74
|
+
review,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Average the qualityScore fields across a set of entries — used by the
|
|
79
|
+
* portfolio footer to show "your last 10 trades scored 3.2 / 5 on average".
|
|
80
|
+
*
|
|
81
|
+
* Entries without a persisted qualityScore are skipped (back-compat with
|
|
82
|
+
* pre-v3.20 trades). Returns null when there's nothing scored to average.
|
|
83
|
+
*/
|
|
84
|
+
export function aggregateScores(entries) {
|
|
85
|
+
const scored = entries.filter((e) => e.qualityScore != null);
|
|
86
|
+
if (scored.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
const sum = scored.reduce((acc, e) => {
|
|
89
|
+
const q = e.qualityScore;
|
|
90
|
+
return {
|
|
91
|
+
total: acc.total + q.total,
|
|
92
|
+
v: acc.v + q.verifiability,
|
|
93
|
+
e: acc.e + q.evidence,
|
|
94
|
+
s: acc.s + q.specificity,
|
|
95
|
+
n: acc.n + q.novelty,
|
|
96
|
+
r: acc.r + q.review,
|
|
97
|
+
};
|
|
98
|
+
}, { total: 0, v: 0, e: 0, s: 0, n: 0, r: 0 });
|
|
99
|
+
const n = scored.length;
|
|
100
|
+
return {
|
|
101
|
+
count: n,
|
|
102
|
+
averageTotal: sum.total / n,
|
|
103
|
+
averageVerifiability: sum.v / n,
|
|
104
|
+
averageEvidence: sum.e / n,
|
|
105
|
+
averageSpecificity: sum.s / n,
|
|
106
|
+
averageNovelty: sum.n / n,
|
|
107
|
+
averageReview: sum.r / n,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -16,6 +16,37 @@
|
|
|
16
16
|
* prior crash never bricks the log.
|
|
17
17
|
*/
|
|
18
18
|
import type { Side } from './portfolio.js';
|
|
19
|
+
/**
|
|
20
|
+
* Trade rationale — the "why" behind a fill, captured at trade time so
|
|
21
|
+
* the journal can score for discipline (not P&L). Inspired by the AI-Trader
|
|
22
|
+
* signal-quality model: verifiability + evidence + specificity drive better
|
|
23
|
+
* decisions than rewarding outcomes (which incentivizes curve-fitting).
|
|
24
|
+
*
|
|
25
|
+
* All fields are optional; the scorer rewards completeness without forcing it.
|
|
26
|
+
*/
|
|
27
|
+
export interface TradeRationale {
|
|
28
|
+
direction?: 'long' | 'short' | 'neutral';
|
|
29
|
+
priceTarget?: number;
|
|
30
|
+
stopLoss?: number;
|
|
31
|
+
timeHorizon?: string;
|
|
32
|
+
conviction?: 1 | 2 | 3 | 4 | 5;
|
|
33
|
+
evidence?: string[];
|
|
34
|
+
tags?: string[];
|
|
35
|
+
thesis?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Persisted quality breakdown — five components on 0–1 scales plus a 0–5
|
|
39
|
+
* total. Written next to each entry at append time so portfolio reads
|
|
40
|
+
* never need to re-score.
|
|
41
|
+
*/
|
|
42
|
+
export interface QualityScore {
|
|
43
|
+
total: number;
|
|
44
|
+
verifiability: number;
|
|
45
|
+
evidence: number;
|
|
46
|
+
specificity: number;
|
|
47
|
+
novelty: number;
|
|
48
|
+
review: number;
|
|
49
|
+
}
|
|
19
50
|
export interface TradeLogEntry {
|
|
20
51
|
timestamp: number;
|
|
21
52
|
symbol: string;
|
|
@@ -25,6 +56,12 @@ export interface TradeLogEntry {
|
|
|
25
56
|
feeUsd: number;
|
|
26
57
|
/** Realized P&L from this specific fill — 0 for opens, ± for closes. */
|
|
27
58
|
realizedPnlUsd: number;
|
|
59
|
+
/** Journal-v2 fields (optional, back-compat: older entries lack these). */
|
|
60
|
+
rationale?: TradeRationale;
|
|
61
|
+
/** User's post-trade note. Boosts the `review` component of the score. */
|
|
62
|
+
review?: string;
|
|
63
|
+
/** Computed at append time so portfolio reads don't re-score on every render. */
|
|
64
|
+
qualityScore?: QualityScore;
|
|
28
65
|
}
|
|
29
66
|
export declare class TradeLog {
|
|
30
67
|
private filePath;
|
package/package.json
CHANGED