@blockrun/franklin 3.15.12 → 3.15.13

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.
@@ -326,6 +326,12 @@ Your training data is frozen in the past. Live-world questions MUST be answered
326
326
 
327
327
  If you find yourself about to emit one of these, stop and call the tool instead. If you don't know which ticker the user means, call ExaSearch or AskUser — never deflect.
328
328
 
329
+ **Trading verdicts (TradingSignal).** When the user asks "how does $TICKER look" / "should I buy X" / "is BTC overbought":
330
+ - Run **TradingSignal** with default lookback (90d). Lower values leave MACD undefined.
331
+ - The tool returns a **Verdict** section with \`Direction\`, \`Bull signals\`, \`Bear signals\`. Echo it directly. Do not soften "bullish" to "leaning slightly positive" — say what the data says.
332
+ - If \`Data Notes\` lists an indicator as "insufficient data", state that explicitly to the user and suggest re-running with more days. Do NOT pretend that indicator is "neutral".
333
+ - **Forbidden default**: "持有观望", "wait and see", "hold for clearer signals" — these are bugs when ≥2 indicators voted in a clear direction. Bail out to those phrases ONLY when (a) the Verdict says \`neutral\` AND (b) the bull/bear signal lists are both genuinely empty or one of each. Otherwise commit to a direction with the reasoning the tool already gave you.
334
+
329
335
  **Media generation (ImageGen / VideoGen).** Pass just the user's descriptive prompt and the output path — do NOT pass \`model\`. The harness picks the right model for the requested style + budget, refines loose prompts using a 5-slot template (scene / subject / details / use case / constraints), and surfaces both the refinement and a cost proposal through AskUser before spending. If the user wants their prompt left exactly as written, prefix it with \`///\` to skip refinement. Only pass \`model\` explicitly if the user named one specifically.`;
330
336
  }
331
337
  function getTokenEfficiencySection() {
@@ -16,8 +16,19 @@ function formatUsd(n) {
16
16
  return `$${(n / 1e3).toFixed(1)}K`;
17
17
  return `$${n.toFixed(2)}`;
18
18
  }
19
+ // MACD needs slow EMA (26) + signal EMA (9) = 35 closes minimum for the
20
+ // signal/histogram to be defined. Default was 30, which left signal=NaN
21
+ // and trend stuck at 'neutral' on every call — see the 2026-05-03 BTC
22
+ // report where the agent had to write "MACD signal can't be computed
23
+ // due to insufficient data". 90d gives stable MACD plus enough room for
24
+ // reasonable Bollinger bandwidth and annualized volatility readings.
25
+ const DEFAULT_LOOKBACK_DAYS = 90;
26
+ const MIN_DAYS_FOR_MACD = 35;
27
+ function fmtNumber(n, digits) {
28
+ return Number.isFinite(n) ? n.toFixed(digits) : 'n/a';
29
+ }
19
30
  async function executeSignal(input, _ctx) {
20
- const { ticker, days = 30 } = input;
31
+ const { ticker, days = DEFAULT_LOOKBACK_DAYS } = input;
21
32
  if (!ticker) {
22
33
  return { output: 'Error: ticker is required', isError: true };
23
34
  }
@@ -37,23 +48,45 @@ async function executeSignal(input, _ctx) {
37
48
  const macdResult = macd(closes);
38
49
  const bbResult = bollingerBands(closes);
39
50
  const volResult = volatility(closes);
40
- // Determine overall direction from indicators
51
+ // Per-indicator validity. Each has its own minimum sample requirement
52
+ // and we surface the gap rather than silently defaulting to 'neutral'.
53
+ const macdValid = Number.isFinite(macdResult.signal) && Number.isFinite(macdResult.histogram);
54
+ const dataNotes = [];
55
+ if (!macdValid) {
56
+ dataNotes.push(`MACD signal/histogram unavailable — need ≥${MIN_DAYS_FOR_MACD} closes, got ${closes.length}. ` +
57
+ `Re-run with days=${MIN_DAYS_FOR_MACD} or higher for full trend detection.`);
58
+ }
59
+ // Direction count — only valid indicators contribute. A NaN MACD must
60
+ // not be counted as a 'neutral' vote, otherwise the agent reads weak
61
+ // data as a reason to recommend "wait and see".
41
62
  let bullish = 0;
42
63
  let bearish = 0;
43
- if (rsiResult.interpretation === 'oversold')
44
- bullish++;
45
- if (rsiResult.interpretation === 'overbought')
46
- bearish++;
47
- if (macdResult.trend === 'bullish')
48
- bullish++;
49
- if (macdResult.trend === 'bearish')
50
- bearish++;
51
- if (bbResult.position === 'below')
52
- bullish++;
53
- if (bbResult.position === 'above')
54
- bearish++;
64
+ let votingIndicators = 0;
65
+ if (Number.isFinite(rsiResult.value)) {
66
+ votingIndicators++;
67
+ if (rsiResult.interpretation === 'oversold')
68
+ bullish++;
69
+ if (rsiResult.interpretation === 'overbought')
70
+ bearish++;
71
+ }
72
+ if (macdValid) {
73
+ votingIndicators++;
74
+ if (macdResult.trend === 'bullish')
75
+ bullish++;
76
+ if (macdResult.trend === 'bearish')
77
+ bearish++;
78
+ }
79
+ if (Number.isFinite(bbResult.middle)) {
80
+ votingIndicators++;
81
+ if (bbResult.position === 'below')
82
+ bullish++;
83
+ if (bbResult.position === 'above')
84
+ bearish++;
85
+ }
55
86
  const direction = bullish > bearish ? 'bullish' : bearish > bullish ? 'bearish' : 'neutral';
56
- const confidence = Math.max(bullish, bearish) / 3;
87
+ const confidence = votingIndicators > 0
88
+ ? Math.max(bullish, bearish) / votingIndicators
89
+ : 0;
57
90
  bus.emit(makeEvent({
58
91
  type: 'signal.detected',
59
92
  source: 'trading',
@@ -71,6 +104,28 @@ async function executeSignal(input, _ctx) {
71
104
  }));
72
105
  const { price, change24h, marketCap, volume24h } = priceResult;
73
106
  const last5 = closes.slice(-5).map(c => c.toFixed(2)).join(', ');
107
+ // MACD line: when signal/histogram are NaN, say so explicitly instead
108
+ // of rendering "1822.7300 / Signal: NaN / Histogram: NaN — neutral",
109
+ // which read as a real signal to translation models.
110
+ const macdLine = macdValid
111
+ ? `- **MACD:** ${fmtNumber(macdResult.macd, 4)} / Signal: ${fmtNumber(macdResult.signal, 4)} / Histogram: ${fmtNumber(macdResult.histogram, 4)} — ${macdResult.trend}`
112
+ : `- **MACD:** ${fmtNumber(macdResult.macd, 4)} / Signal: insufficient data / Histogram: insufficient data — *not enough closes for trend*`;
113
+ // Bull / bear breakdown so the agent can echo a real verdict instead
114
+ // of falling back to "wait and see".
115
+ const bullSignals = [];
116
+ const bearSignals = [];
117
+ if (rsiResult.interpretation === 'oversold')
118
+ bullSignals.push('RSI oversold');
119
+ if (rsiResult.interpretation === 'overbought')
120
+ bearSignals.push('RSI overbought');
121
+ if (macdValid && macdResult.trend === 'bullish')
122
+ bullSignals.push('MACD trending up');
123
+ if (macdValid && macdResult.trend === 'bearish')
124
+ bearSignals.push('MACD trending down');
125
+ if (Number.isFinite(bbResult.middle) && bbResult.position === 'below')
126
+ bullSignals.push('price below lower Bollinger');
127
+ if (Number.isFinite(bbResult.middle) && bbResult.position === 'above')
128
+ bearSignals.push('price above upper Bollinger');
74
129
  const output = [
75
130
  `## ${upper} Signal Report`,
76
131
  '',
@@ -78,11 +133,17 @@ async function executeSignal(input, _ctx) {
78
133
  `**Market Cap:** ${formatUsd(marketCap)}`,
79
134
  `**24h Volume:** ${formatUsd(volume24h)}`,
80
135
  '',
81
- `### Technical Indicators (${days}d lookback)`,
82
- `- **RSI(14):** ${rsiResult.value.toFixed(1)} — ${rsiResult.interpretation}`,
83
- `- **MACD:** ${macdResult.macd.toFixed(4)} / Signal: ${macdResult.signal.toFixed(4)} / Histogram: ${macdResult.histogram.toFixed(4)} — ${macdResult.trend}`,
84
- `- **Bollinger:** Upper ${bbResult.upper.toFixed(2)} / Middle ${bbResult.middle.toFixed(2)} / Lower ${bbResult.lower.toFixed(2)} — Price ${bbResult.position}`,
85
- `- **Volatility:** ${(volResult.annualized * 100).toFixed(1)}% annualized — ${volResult.interpretation}`,
136
+ `### Technical Indicators (${days}d lookback, ${closes.length} closes)`,
137
+ `- **RSI(14):** ${fmtNumber(rsiResult.value, 1)} — ${rsiResult.interpretation}`,
138
+ macdLine,
139
+ `- **Bollinger:** Upper ${fmtNumber(bbResult.upper, 2)} / Middle ${fmtNumber(bbResult.middle, 2)} / Lower ${fmtNumber(bbResult.lower, 2)} — Price ${bbResult.position}`,
140
+ `- **Volatility:** ${fmtNumber(volResult.annualized * 100, 1)}% annualized — ${volResult.interpretation}`,
141
+ '',
142
+ `### Verdict`,
143
+ `**Direction:** ${direction} (${votingIndicators} indicator${votingIndicators === 1 ? '' : 's'} voting, confidence ${(confidence * 100).toFixed(0)}%)`,
144
+ bullSignals.length > 0 ? `**Bull signals:** ${bullSignals.join(', ')}` : '**Bull signals:** none',
145
+ bearSignals.length > 0 ? `**Bear signals:** ${bearSignals.join(', ')}` : '**Bear signals:** none',
146
+ ...(dataNotes.length > 0 ? ['', `### Data Notes`, ...dataNotes.map(n => `- ${n}`)] : []),
86
147
  '',
87
148
  `### Raw Data`,
88
149
  `Closes (last 5): ${last5}`,
@@ -92,12 +153,12 @@ async function executeSignal(input, _ctx) {
92
153
  export const tradingSignalCapability = {
93
154
  spec: {
94
155
  name: 'TradingSignal',
95
- description: 'Get current price, technical indicators (RSI, MACD, Bollinger Bands, volatility), and a signal summary for a cryptocurrency. Returns raw data for the agent to analyze and interpret.',
156
+ description: 'Get current price, technical indicators (RSI, MACD, Bollinger Bands, volatility), and a verdict (bullish / bearish / neutral with confidence) for a cryptocurrency. Always returns a Verdict section with bull/bear signal lists — echo it directly. When MACD signal/histogram report "insufficient data", say so explicitly; do NOT default to "wait and see".',
96
157
  input_schema: {
97
158
  type: 'object',
98
159
  properties: {
99
160
  ticker: { type: 'string', description: 'Cryptocurrency ticker, e.g. "BTC", "ETH"' },
100
- days: { type: 'number', description: 'Lookback period for indicators. Default: 30' },
161
+ days: { type: 'number', description: 'Lookback period in days. Default 90 (recommended). Below 35 will leave MACD signal/histogram undefined.' },
101
162
  },
102
163
  required: ['ticker'],
103
164
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.12",
3
+ "version": "3.15.13",
4
4
  "description": "Franklin — 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": {