@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.
@@ -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
- return { output: renderPortfolio(snap, riskConfig) };
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
- tradeLog.append({
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
- tradeLog.append({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.19.0",
3
+ "version": "3.20.0",
4
4
  "description": "Franklin Agent — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {