@blockrun/franklin 3.8.7 → 3.8.9
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/agent/bash-guard.js +29 -0
- package/dist/agent/loop.js +61 -2
- package/dist/agent/permissions.js +2 -2
- package/dist/agent/types.d.ts +7 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/doctor.js +251 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.js +72 -2
- package/dist/index.js +17 -1
- package/dist/panel/html.js +111 -21
- package/dist/panel/server.js +15 -4
- package/dist/tools/activate.d.ts +29 -0
- package/dist/tools/activate.js +96 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/read.js +20 -1
- package/dist/tools/tool-categories.d.ts +22 -0
- package/dist/tools/tool-categories.js +44 -0
- package/dist/tools/trading-execute.d.ts +11 -21
- package/dist/tools/trading-execute.js +43 -130
- package/dist/tools/trading-views.d.ts +64 -0
- package/dist/tools/trading-views.js +115 -0
- package/dist/tools/trading.js +86 -7
- package/dist/tools/webhook.d.ts +18 -0
- package/dist/tools/webhook.js +185 -0
- package/dist/tools/write.js +20 -0
- package/dist/trading/data.d.ts +24 -1
- package/dist/trading/data.js +67 -102
- package/dist/trading/providers/blockrun/client.d.ts +48 -0
- package/dist/trading/providers/blockrun/client.js +253 -0
- package/dist/trading/providers/blockrun/price.d.ts +24 -0
- package/dist/trading/providers/blockrun/price.js +110 -0
- package/dist/trading/providers/coingecko/client.d.ts +20 -0
- package/dist/trading/providers/coingecko/client.js +87 -0
- package/dist/trading/providers/coingecko/markets.d.ts +3 -0
- package/dist/trading/providers/coingecko/markets.js +25 -0
- package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
- package/dist/trading/providers/coingecko/ohlcv.js +29 -0
- package/dist/trading/providers/coingecko/price.d.ts +11 -0
- package/dist/trading/providers/coingecko/price.js +41 -0
- package/dist/trading/providers/coingecko/trending.d.ts +3 -0
- package/dist/trading/providers/coingecko/trending.js +22 -0
- package/dist/trading/providers/fetcher.d.ts +43 -0
- package/dist/trading/providers/fetcher.js +45 -0
- package/dist/trading/providers/registry.d.ts +45 -0
- package/dist/trading/providers/registry.js +82 -0
- package/dist/trading/providers/standard-models.d.ts +94 -0
- package/dist/trading/providers/standard-models.js +21 -0
- package/dist/trading/providers/telemetry.d.ts +51 -0
- package/dist/trading/providers/telemetry.js +115 -0
- package/package.json +1 -1
|
@@ -1,54 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Trading execution capabilities
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Trading execution capabilities — the three-to-four tools that let the
|
|
3
|
+
* agent inspect its portfolio, open/close paper positions, and (when a
|
|
4
|
+
* persistent trade log is attached) query cross-session history.
|
|
5
5
|
*
|
|
6
|
-
* This is the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* P&L, fill detail) without a follow-up tool call.
|
|
12
|
-
*
|
|
13
|
-
* Factory-style construction (createTradingCapabilities) keeps testing
|
|
14
|
-
* clean: production code calls it with a default disk-backed engine;
|
|
15
|
-
* tests inject a MockExchange-backed engine and assert behavior without
|
|
16
|
-
* touching disk.
|
|
6
|
+
* This file is now the "router" layer only: it binds the engine to tool
|
|
7
|
+
* handlers and delegates rendering to `trading-views.ts`. The portfolio
|
|
8
|
+
* math, risk math, and exchange simulation all live in `../trading/*`.
|
|
9
|
+
* The split mirrors OpenBB's router/engine/view layering and keeps every
|
|
10
|
+
* layer testable in isolation.
|
|
17
11
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return `${sign}$${abs.toFixed(2)}`;
|
|
22
|
-
}
|
|
23
|
-
function formatPct(n) {
|
|
24
|
-
return `${(n * 100).toFixed(1)}%`;
|
|
12
|
+
import { renderOrderBlocked, renderOrderFilled, renderPortfolio, renderPositionClosed, renderTradeHistory, windowToSince, } from './trading-views.js';
|
|
13
|
+
function enginePortfolio(engine) {
|
|
14
|
+
return engine.deps.portfolio;
|
|
25
15
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
const arrow = p.unrealizedPnlUsd >= 0 ? '↑' : '↓';
|
|
29
|
-
return (`- **${p.symbol}** qty=${p.qty} @ avg ${formatUsd(p.avgPriceUsd)} ` +
|
|
30
|
-
`| mark ${formatUsd(p.markUsd)} ${arrow} ` +
|
|
31
|
-
`| unrealized ${formatUsd(p.unrealizedPnlUsd)} (${formatPct(pctReturn)})`);
|
|
16
|
+
function engineExchange(engine) {
|
|
17
|
+
return engine.deps.exchange;
|
|
32
18
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
case 'd': return now - n * 86_400_000;
|
|
42
|
-
case 'w': return now - n * 7 * 86_400_000;
|
|
43
|
-
case 'm': return now - n * 30 * 86_400_000;
|
|
44
|
-
default: return 0;
|
|
19
|
+
async function buildPortfolioSnapshot(engine) {
|
|
20
|
+
const portfolio = enginePortfolio(engine);
|
|
21
|
+
const exchange = engineExchange(engine);
|
|
22
|
+
const priceTable = {};
|
|
23
|
+
for (const p of portfolio.listPositions()) {
|
|
24
|
+
const quote = await exchange.getPrice(p.symbol);
|
|
25
|
+
if (quote != null)
|
|
26
|
+
priceTable[p.symbol] = quote;
|
|
45
27
|
}
|
|
46
|
-
|
|
47
|
-
function formatTradeLine(entry) {
|
|
48
|
-
const when = new Date(entry.timestamp).toISOString().replace('T', ' ').slice(0, 16);
|
|
49
|
-
const side = entry.side.toUpperCase();
|
|
50
|
-
const pnl = entry.realizedPnlUsd === 0 ? '' : ` → realized ${formatUsd(entry.realizedPnlUsd)}`;
|
|
51
|
-
return `- ${when} ${side} ${entry.qty} ${entry.symbol} @ ${formatUsd(entry.priceUsd)}${pnl}`;
|
|
28
|
+
return portfolio.markToMarket(priceTable);
|
|
52
29
|
}
|
|
53
30
|
export function createTradingCapabilities(deps) {
|
|
54
31
|
const { engine, riskConfig, onStateChange, tradeLog } = deps;
|
|
@@ -66,39 +43,8 @@ export function createTradingCapabilities(deps) {
|
|
|
66
43
|
},
|
|
67
44
|
concurrent: true,
|
|
68
45
|
async execute(_input, _ctx) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const priceTable = {};
|
|
72
|
-
for (const p of engine.deps.portfolio.listPositions()) {
|
|
73
|
-
const quote = await engine.deps.exchange.getPrice(p.symbol);
|
|
74
|
-
if (quote != null)
|
|
75
|
-
priceTable[p.symbol] = quote;
|
|
76
|
-
}
|
|
77
|
-
const portfolio = engine.deps.portfolio;
|
|
78
|
-
const snap = portfolio.markToMarket(priceTable);
|
|
79
|
-
const lines = [];
|
|
80
|
-
lines.push('## Portfolio');
|
|
81
|
-
lines.push(`- Cash: ${formatUsd(snap.cashUsd)}`);
|
|
82
|
-
lines.push(`- Equity (cash + positions marked-to-market): ${formatUsd(snap.equityUsd)}`);
|
|
83
|
-
lines.push(`- Unrealized P&L: ${formatUsd(snap.unrealizedPnlUsd)}`);
|
|
84
|
-
lines.push(`- Realized P&L (this session): ${formatUsd(snap.realizedPnlUsd)}`);
|
|
85
|
-
lines.push('');
|
|
86
|
-
if (snap.positions.length === 0) {
|
|
87
|
-
lines.push('_No open positions._');
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
lines.push('### Open positions');
|
|
91
|
-
for (const p of snap.positions)
|
|
92
|
-
lines.push(formatPositionLine(p));
|
|
93
|
-
}
|
|
94
|
-
if (riskConfig) {
|
|
95
|
-
const totalExposure = snap.positions.reduce((a, p) => a + p.qty * p.markUsd, 0);
|
|
96
|
-
lines.push('');
|
|
97
|
-
lines.push('### Risk utilization');
|
|
98
|
-
lines.push(`- Total exposure: ${formatUsd(totalExposure)} / cap ${formatUsd(riskConfig.maxTotalExposureUsd)} ` +
|
|
99
|
-
`(${formatPct(totalExposure / riskConfig.maxTotalExposureUsd)})`);
|
|
100
|
-
}
|
|
101
|
-
return { output: lines.join('\n') };
|
|
46
|
+
const snap = await buildPortfolioSnapshot(engine);
|
|
47
|
+
return { output: renderPortfolio(snap, riskConfig) };
|
|
102
48
|
},
|
|
103
49
|
};
|
|
104
50
|
const tradingOpenPosition = {
|
|
@@ -132,14 +78,7 @@ export function createTradingCapabilities(deps) {
|
|
|
132
78
|
}
|
|
133
79
|
const outcome = await engine.openPosition({ symbol, qty, priceUsd });
|
|
134
80
|
if (outcome.status === 'blocked') {
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
output: `## Order blocked\n` +
|
|
138
|
-
`- Symbol: ${symbol}\n` +
|
|
139
|
-
`- Attempted: buy ${qty} @ ${formatUsd(priceUsd)}\n` +
|
|
140
|
-
`- Reason: ${outcome.reason}\n\n` +
|
|
141
|
-
`Try a smaller qty, or close other positions first to free up exposure headroom.`,
|
|
142
|
-
};
|
|
81
|
+
return { output: renderOrderBlocked({ symbol, qty, priceUsd, reason: outcome.reason }) };
|
|
143
82
|
}
|
|
144
83
|
if (outcome.status === 'noop') {
|
|
145
84
|
return { output: `No-op: ${outcome.reason}` };
|
|
@@ -157,14 +96,12 @@ export function createTradingCapabilities(deps) {
|
|
|
157
96
|
}
|
|
158
97
|
if (onStateChange)
|
|
159
98
|
await onStateChange();
|
|
160
|
-
const portfolio = engine.deps.portfolio;
|
|
161
|
-
const pos = portfolio.getPosition(symbol);
|
|
162
99
|
return {
|
|
163
|
-
output:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
100
|
+
output: renderOrderFilled({
|
|
101
|
+
symbol,
|
|
102
|
+
fill: outcome.fill,
|
|
103
|
+
portfolio: enginePortfolio(engine),
|
|
104
|
+
}),
|
|
168
105
|
};
|
|
169
106
|
},
|
|
170
107
|
};
|
|
@@ -172,8 +109,8 @@ export function createTradingCapabilities(deps) {
|
|
|
172
109
|
spec: {
|
|
173
110
|
name: 'TradingClosePosition',
|
|
174
111
|
description: 'Close (sell) an open position, realizing P&L against the average entry price. ' +
|
|
175
|
-
|
|
176
|
-
|
|
112
|
+
"Omit qty to flatten the position entirely; pass qty to partially reduce. Uses the " +
|
|
113
|
+
"exchange's current mark — no manual price required.",
|
|
177
114
|
input_schema: {
|
|
178
115
|
type: 'object',
|
|
179
116
|
required: ['symbol'],
|
|
@@ -192,27 +129,19 @@ export function createTradingCapabilities(deps) {
|
|
|
192
129
|
const symbol = String(input.symbol ?? '').toUpperCase();
|
|
193
130
|
const qty = input.qty != null ? Number(input.qty) : undefined;
|
|
194
131
|
if (!symbol) {
|
|
195
|
-
return {
|
|
196
|
-
output: 'Error: TradingClosePosition requires symbol.',
|
|
197
|
-
isError: true,
|
|
198
|
-
};
|
|
132
|
+
return { output: 'Error: TradingClosePosition requires symbol.', isError: true };
|
|
199
133
|
}
|
|
200
134
|
if (qty != null && (!Number.isFinite(qty) || qty <= 0)) {
|
|
201
|
-
return {
|
|
202
|
-
output: 'Error: if qty is provided, it must be > 0.',
|
|
203
|
-
isError: true,
|
|
204
|
-
};
|
|
135
|
+
return { output: 'Error: if qty is provided, it must be > 0.', isError: true };
|
|
205
136
|
}
|
|
206
|
-
const portfolio = engine
|
|
137
|
+
const portfolio = enginePortfolio(engine);
|
|
207
138
|
const priorRealized = portfolio.realizedPnlUsd;
|
|
208
139
|
const outcome = await engine.closePosition({ symbol, qty });
|
|
209
140
|
if (outcome.status === 'noop') {
|
|
210
141
|
return { output: `No open ${symbol} position to close.` };
|
|
211
142
|
}
|
|
212
143
|
if (outcome.status === 'blocked') {
|
|
213
|
-
return {
|
|
214
|
-
output: `## Close blocked\n- Symbol: ${symbol}\n- Reason: ${outcome.reason}`,
|
|
215
|
-
};
|
|
144
|
+
return { output: `## Close blocked\n- Symbol: ${symbol}\n- Reason: ${outcome.reason}` };
|
|
216
145
|
}
|
|
217
146
|
const tradeRealized = portfolio.realizedPnlUsd - priorRealized;
|
|
218
147
|
if (tradeLog) {
|
|
@@ -228,15 +157,13 @@ export function createTradingCapabilities(deps) {
|
|
|
228
157
|
}
|
|
229
158
|
if (onStateChange)
|
|
230
159
|
await onStateChange();
|
|
231
|
-
const remaining = portfolio.getPosition(symbol);
|
|
232
160
|
return {
|
|
233
|
-
output:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`Session realized P&L: ${formatUsd(portfolio.realizedPnlUsd)}`,
|
|
161
|
+
output: renderPositionClosed({
|
|
162
|
+
symbol,
|
|
163
|
+
fill: outcome.fill,
|
|
164
|
+
tradeRealized,
|
|
165
|
+
portfolio,
|
|
166
|
+
}),
|
|
240
167
|
};
|
|
241
168
|
},
|
|
242
169
|
};
|
|
@@ -274,21 +201,7 @@ export function createTradingCapabilities(deps) {
|
|
|
274
201
|
const since = windowRaw.toLowerCase() === 'all' ? 0 : windowToSince(windowRaw, now);
|
|
275
202
|
const entries = tradeLog.recent(limit).filter((e) => e.timestamp >= since);
|
|
276
203
|
const realized = tradeLog.realizedSince(since);
|
|
277
|
-
|
|
278
|
-
const closes = entries.filter((e) => e.side === 'sell').length;
|
|
279
|
-
const lines = [];
|
|
280
|
-
lines.push(`## Trade history (${windowRaw})`);
|
|
281
|
-
lines.push(`- ${windowRaw} P&L (realized): ${formatUsd(realized)}`);
|
|
282
|
-
lines.push(`- Trades: ${entries.length} (${opens} opens, ${closes} closes)`);
|
|
283
|
-
lines.push('');
|
|
284
|
-
if (entries.length === 0) {
|
|
285
|
-
lines.push('_No trades in this window._');
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
for (const e of entries)
|
|
289
|
-
lines.push(formatTradeLine(e));
|
|
290
|
-
}
|
|
291
|
-
return { output: lines.join('\n') };
|
|
204
|
+
return { output: renderTradeHistory({ windowRaw, entries, realized }) };
|
|
292
205
|
},
|
|
293
206
|
};
|
|
294
207
|
caps.push(tradingHistory);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trading view/formatter helpers.
|
|
3
|
+
*
|
|
4
|
+
* Anything that turns engine state into human/agent-readable markdown
|
|
5
|
+
* belongs here. Split out of `trading-execute.ts` so the tool handlers in
|
|
6
|
+
* `trading-router.ts` stay focused on request handling and the engine
|
|
7
|
+
* stays free of presentation concerns. This mirrors the view/controller
|
|
8
|
+
* separation OpenBB enforces between `standard_models` (data) and the
|
|
9
|
+
* router-side rendering that happens in their MCP layer.
|
|
10
|
+
*/
|
|
11
|
+
import type { Position } from '../trading/portfolio.js';
|
|
12
|
+
import type { Portfolio } from '../trading/portfolio.js';
|
|
13
|
+
import type { RiskConfig } from '../trading/risk.js';
|
|
14
|
+
import type { TradeLogEntry } from '../trading/trade-log.js';
|
|
15
|
+
export declare function formatUsd(n: number): string;
|
|
16
|
+
export declare function formatPct(n: number): string;
|
|
17
|
+
export declare function formatPositionLine(p: Position & {
|
|
18
|
+
markUsd: number;
|
|
19
|
+
unrealizedPnlUsd: number;
|
|
20
|
+
}): string;
|
|
21
|
+
export declare function formatTradeLine(entry: TradeLogEntry): string;
|
|
22
|
+
/** Parse a window string ("24h", "7d", "all") into a lower-bound timestamp. */
|
|
23
|
+
export declare function windowToSince(window: string, now: number): number;
|
|
24
|
+
export interface PortfolioSnapshot {
|
|
25
|
+
cashUsd: number;
|
|
26
|
+
equityUsd: number;
|
|
27
|
+
unrealizedPnlUsd: number;
|
|
28
|
+
realizedPnlUsd: number;
|
|
29
|
+
positions: (Position & {
|
|
30
|
+
markUsd: number;
|
|
31
|
+
unrealizedPnlUsd: number;
|
|
32
|
+
})[];
|
|
33
|
+
}
|
|
34
|
+
export declare function renderPortfolio(snap: PortfolioSnapshot, riskConfig?: RiskConfig): string;
|
|
35
|
+
export declare function renderOrderFilled(params: {
|
|
36
|
+
symbol: string;
|
|
37
|
+
fill: {
|
|
38
|
+
qty: number;
|
|
39
|
+
priceUsd: number;
|
|
40
|
+
feeUsd: number;
|
|
41
|
+
};
|
|
42
|
+
portfolio: Portfolio;
|
|
43
|
+
}): string;
|
|
44
|
+
export declare function renderOrderBlocked(params: {
|
|
45
|
+
symbol: string;
|
|
46
|
+
qty: number;
|
|
47
|
+
priceUsd: number;
|
|
48
|
+
reason: string;
|
|
49
|
+
}): string;
|
|
50
|
+
export declare function renderPositionClosed(params: {
|
|
51
|
+
symbol: string;
|
|
52
|
+
fill: {
|
|
53
|
+
qty: number;
|
|
54
|
+
priceUsd: number;
|
|
55
|
+
feeUsd: number;
|
|
56
|
+
};
|
|
57
|
+
tradeRealized: number;
|
|
58
|
+
portfolio: Portfolio;
|
|
59
|
+
}): string;
|
|
60
|
+
export declare function renderTradeHistory(params: {
|
|
61
|
+
windowRaw: string;
|
|
62
|
+
entries: TradeLogEntry[];
|
|
63
|
+
realized: number;
|
|
64
|
+
}): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trading view/formatter helpers.
|
|
3
|
+
*
|
|
4
|
+
* Anything that turns engine state into human/agent-readable markdown
|
|
5
|
+
* belongs here. Split out of `trading-execute.ts` so the tool handlers in
|
|
6
|
+
* `trading-router.ts` stay focused on request handling and the engine
|
|
7
|
+
* stays free of presentation concerns. This mirrors the view/controller
|
|
8
|
+
* separation OpenBB enforces between `standard_models` (data) and the
|
|
9
|
+
* router-side rendering that happens in their MCP layer.
|
|
10
|
+
*/
|
|
11
|
+
export function formatUsd(n) {
|
|
12
|
+
const sign = n < 0 ? '-' : '';
|
|
13
|
+
const abs = Math.abs(n);
|
|
14
|
+
return `${sign}$${abs.toFixed(2)}`;
|
|
15
|
+
}
|
|
16
|
+
export function formatPct(n) {
|
|
17
|
+
return `${(n * 100).toFixed(1)}%`;
|
|
18
|
+
}
|
|
19
|
+
export function formatPositionLine(p) {
|
|
20
|
+
const pctReturn = (p.markUsd - p.avgPriceUsd) / p.avgPriceUsd;
|
|
21
|
+
const arrow = p.unrealizedPnlUsd >= 0 ? '↑' : '↓';
|
|
22
|
+
return (`- **${p.symbol}** qty=${p.qty} @ avg ${formatUsd(p.avgPriceUsd)} ` +
|
|
23
|
+
`| mark ${formatUsd(p.markUsd)} ${arrow} ` +
|
|
24
|
+
`| unrealized ${formatUsd(p.unrealizedPnlUsd)} (${formatPct(pctReturn)})`);
|
|
25
|
+
}
|
|
26
|
+
export function formatTradeLine(entry) {
|
|
27
|
+
const when = new Date(entry.timestamp).toISOString().replace('T', ' ').slice(0, 16);
|
|
28
|
+
const side = entry.side.toUpperCase();
|
|
29
|
+
const pnl = entry.realizedPnlUsd === 0 ? '' : ` → realized ${formatUsd(entry.realizedPnlUsd)}`;
|
|
30
|
+
return `- ${when} ${side} ${entry.qty} ${entry.symbol} @ ${formatUsd(entry.priceUsd)}${pnl}`;
|
|
31
|
+
}
|
|
32
|
+
/** Parse a window string ("24h", "7d", "all") into a lower-bound timestamp. */
|
|
33
|
+
export function windowToSince(window, now) {
|
|
34
|
+
const m = /^(\d+)\s*([hdwm])$/i.exec(window.trim());
|
|
35
|
+
if (!m)
|
|
36
|
+
return 0;
|
|
37
|
+
const n = parseInt(m[1], 10);
|
|
38
|
+
switch (m[2].toLowerCase()) {
|
|
39
|
+
case 'h': return now - n * 3_600_000;
|
|
40
|
+
case 'd': return now - n * 86_400_000;
|
|
41
|
+
case 'w': return now - n * 7 * 86_400_000;
|
|
42
|
+
case 'm': return now - n * 30 * 86_400_000;
|
|
43
|
+
default: return 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function renderPortfolio(snap, riskConfig) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push('## Portfolio');
|
|
49
|
+
lines.push(`- Cash: ${formatUsd(snap.cashUsd)}`);
|
|
50
|
+
lines.push(`- Equity (cash + positions marked-to-market): ${formatUsd(snap.equityUsd)}`);
|
|
51
|
+
lines.push(`- Unrealized P&L: ${formatUsd(snap.unrealizedPnlUsd)}`);
|
|
52
|
+
lines.push(`- Realized P&L (this session): ${formatUsd(snap.realizedPnlUsd)}`);
|
|
53
|
+
lines.push('');
|
|
54
|
+
if (snap.positions.length === 0) {
|
|
55
|
+
lines.push('_No open positions._');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
lines.push('### Open positions');
|
|
59
|
+
for (const p of snap.positions)
|
|
60
|
+
lines.push(formatPositionLine(p));
|
|
61
|
+
}
|
|
62
|
+
if (riskConfig) {
|
|
63
|
+
const totalExposure = snap.positions.reduce((a, p) => a + p.qty * p.markUsd, 0);
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('### Risk utilization');
|
|
66
|
+
lines.push(`- Total exposure: ${formatUsd(totalExposure)} / cap ${formatUsd(riskConfig.maxTotalExposureUsd)} ` +
|
|
67
|
+
`(${formatPct(totalExposure / riskConfig.maxTotalExposureUsd)})`);
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
export function renderOrderFilled(params) {
|
|
72
|
+
const { symbol, fill, portfolio } = params;
|
|
73
|
+
const pos = portfolio.getPosition(symbol);
|
|
74
|
+
return (`## Order filled\n` +
|
|
75
|
+
`- Bought ${fill.qty} ${symbol} @ ${formatUsd(fill.priceUsd)} ` +
|
|
76
|
+
`(fee ${formatUsd(fill.feeUsd)})\n` +
|
|
77
|
+
`- Position now: ${pos ? `${pos.qty} ${symbol} @ avg ${formatUsd(pos.avgPriceUsd)}` : '(none)'}\n` +
|
|
78
|
+
`- Cash remaining: ${formatUsd(portfolio.cashUsd)}`);
|
|
79
|
+
}
|
|
80
|
+
export function renderOrderBlocked(params) {
|
|
81
|
+
return (`## Order blocked\n` +
|
|
82
|
+
`- Symbol: ${params.symbol}\n` +
|
|
83
|
+
`- Attempted: buy ${params.qty} @ ${formatUsd(params.priceUsd)}\n` +
|
|
84
|
+
`- Reason: ${params.reason}\n\n` +
|
|
85
|
+
`Try a smaller qty, or close other positions first to free up exposure headroom.`);
|
|
86
|
+
}
|
|
87
|
+
export function renderPositionClosed(params) {
|
|
88
|
+
const { symbol, fill, tradeRealized, portfolio } = params;
|
|
89
|
+
const remaining = portfolio.getPosition(symbol);
|
|
90
|
+
return (`## Position closed\n` +
|
|
91
|
+
`- Sold ${fill.qty} ${symbol} @ ${formatUsd(fill.priceUsd)} ` +
|
|
92
|
+
`(fee ${formatUsd(fill.feeUsd)})\n` +
|
|
93
|
+
`- Realized on this trade: ${formatUsd(tradeRealized)}\n` +
|
|
94
|
+
`- Remaining ${symbol}: ${remaining ? `${remaining.qty} @ avg ${formatUsd(remaining.avgPriceUsd)}` : '(flat)'}\n` +
|
|
95
|
+
`- Cash: ${formatUsd(portfolio.cashUsd)} · ` +
|
|
96
|
+
`Session realized P&L: ${formatUsd(portfolio.realizedPnlUsd)}`);
|
|
97
|
+
}
|
|
98
|
+
export function renderTradeHistory(params) {
|
|
99
|
+
const { windowRaw, entries, realized } = params;
|
|
100
|
+
const opens = entries.filter(e => e.side === 'buy').length;
|
|
101
|
+
const closes = entries.filter(e => e.side === 'sell').length;
|
|
102
|
+
const lines = [];
|
|
103
|
+
lines.push(`## Trade history (${windowRaw})`);
|
|
104
|
+
lines.push(`- ${windowRaw} P&L (realized): ${formatUsd(realized)}`);
|
|
105
|
+
lines.push(`- Trades: ${entries.length} (${opens} opens, ${closes} closes)`);
|
|
106
|
+
lines.push('');
|
|
107
|
+
if (entries.length === 0) {
|
|
108
|
+
lines.push('_No trades in this window._');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
for (const e of entries)
|
|
112
|
+
lines.push(formatTradeLine(e));
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
package/dist/tools/trading.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { getPrice, getOHLCV, getTrending, getMarketOverview } from '../trading/data.js';
|
|
1
|
+
import { getPrice, getOHLCV, getTrending, getMarketOverview, getFxPrice, getCommodityPrice, getStockPrice, } from '../trading/data.js';
|
|
2
|
+
const SUPPORTED_STOCK_MARKETS = [
|
|
3
|
+
'us', 'hk', 'jp', 'kr', 'gb', 'de', 'fr', 'nl', 'ie', 'lu', 'cn', 'ca',
|
|
4
|
+
];
|
|
2
5
|
import { rsi, macd, bollingerBands, volatility } from '../trading/metrics.js';
|
|
3
6
|
import { bus } from '../events/bus.js';
|
|
4
7
|
import { makeEvent } from '../events/types.js';
|
|
@@ -102,8 +105,17 @@ export const tradingSignalCapability = {
|
|
|
102
105
|
execute: executeSignal,
|
|
103
106
|
concurrent: true,
|
|
104
107
|
};
|
|
108
|
+
function formatPriceLine(label, priceUsd, change24hPct, opts = {}) {
|
|
109
|
+
const digits = opts.fractionDigits ?? 2;
|
|
110
|
+
const priceStr = `$${priceUsd.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits })}`;
|
|
111
|
+
if (opts.showChange === false || !Number.isFinite(change24hPct)) {
|
|
112
|
+
return `${label}: ${priceStr}`;
|
|
113
|
+
}
|
|
114
|
+
const sign = change24hPct > 0 ? '+' : '';
|
|
115
|
+
return `${label}: ${priceStr} (${sign}${change24hPct.toFixed(2)}% 24h)`;
|
|
116
|
+
}
|
|
105
117
|
async function executeMarket(input, _ctx) {
|
|
106
|
-
const { action, ticker } = input;
|
|
118
|
+
const { action, ticker, market } = input;
|
|
107
119
|
if (!action) {
|
|
108
120
|
return { output: 'Error: action is required', isError: true };
|
|
109
121
|
}
|
|
@@ -121,6 +133,58 @@ async function executeMarket(input, _ctx) {
|
|
|
121
133
|
output: `${ticker.toUpperCase()}: $${price.toLocaleString()} (${change24h > 0 ? '+' : ''}${change24h.toFixed(2)}% 24h), Market Cap: ${formatUsd(marketCap)}, Volume: ${formatUsd(volume24h)}`,
|
|
122
134
|
};
|
|
123
135
|
}
|
|
136
|
+
case 'fxPrice': {
|
|
137
|
+
if (!ticker) {
|
|
138
|
+
return { output: 'Error: ticker is required (e.g. "EUR-USD")', isError: true };
|
|
139
|
+
}
|
|
140
|
+
const result = await getFxPrice(ticker);
|
|
141
|
+
if (typeof result === 'string') {
|
|
142
|
+
return { output: `Error: ${result}`, isError: true };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
output: formatPriceLine(ticker.toUpperCase(), result.price, result.change24h, { fractionDigits: 4 }) +
|
|
146
|
+
' · source: BlockRun Gateway / Pyth (free)',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
case 'commodityPrice': {
|
|
150
|
+
if (!ticker) {
|
|
151
|
+
return { output: 'Error: ticker is required (e.g. "XAU-USD" for gold)', isError: true };
|
|
152
|
+
}
|
|
153
|
+
const result = await getCommodityPrice(ticker);
|
|
154
|
+
if (typeof result === 'string') {
|
|
155
|
+
return { output: `Error: ${result}`, isError: true };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
output: formatPriceLine(ticker.toUpperCase(), result.price, result.change24h, { fractionDigits: 2 }) +
|
|
159
|
+
' · source: BlockRun Gateway / Pyth (free)',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
case 'stockPrice': {
|
|
163
|
+
if (!ticker) {
|
|
164
|
+
return { output: 'Error: ticker is required (e.g. "AAPL" on market "us")', isError: true };
|
|
165
|
+
}
|
|
166
|
+
if (!market) {
|
|
167
|
+
return {
|
|
168
|
+
output: `Error: market code is required for stockPrice. Supported: ${SUPPORTED_STOCK_MARKETS.join(', ')}`,
|
|
169
|
+
isError: true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (!SUPPORTED_STOCK_MARKETS.includes(market)) {
|
|
173
|
+
return {
|
|
174
|
+
output: `Error: unsupported market "${market}". Supported: ${SUPPORTED_STOCK_MARKETS.join(', ')}`,
|
|
175
|
+
isError: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const result = await getStockPrice(ticker, market);
|
|
179
|
+
if (typeof result === 'string') {
|
|
180
|
+
return { output: `Error: ${result}`, isError: true };
|
|
181
|
+
}
|
|
182
|
+
const tickerLabel = `${ticker.toUpperCase()} (${market})`;
|
|
183
|
+
return {
|
|
184
|
+
output: formatPriceLine(tickerLabel, result.price, result.change24h, { fractionDigits: 2 }) +
|
|
185
|
+
' · source: BlockRun Gateway / Pyth · $0.001 paid from wallet',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
124
188
|
case 'trending': {
|
|
125
189
|
const result = await getTrending();
|
|
126
190
|
if (typeof result === 'string') {
|
|
@@ -140,24 +204,39 @@ async function executeMarket(input, _ctx) {
|
|
|
140
204
|
return { output: `Top 20 by Market Cap:\n${header}\n${sep}\n${rows.join('\n')}` };
|
|
141
205
|
}
|
|
142
206
|
default:
|
|
143
|
-
return {
|
|
207
|
+
return {
|
|
208
|
+
output: `Error: unknown action "${action}". Use: price, trending, overview, fxPrice, commodityPrice, stockPrice`,
|
|
209
|
+
isError: true,
|
|
210
|
+
};
|
|
144
211
|
}
|
|
145
212
|
}
|
|
146
213
|
export const tradingMarketCapability = {
|
|
147
214
|
spec: {
|
|
148
215
|
name: 'TradingMarket',
|
|
149
|
-
description: 'Get
|
|
216
|
+
description: 'Get market data across asset classes. Actions: ' +
|
|
217
|
+
'`price` (crypto spot via CoinGecko, free), ' +
|
|
218
|
+
'`trending` (top trending coins), ' +
|
|
219
|
+
'`overview` (top 20 by market cap), ' +
|
|
220
|
+
'`fxPrice` (FX pair like EUR-USD, BlockRun Gateway/Pyth, free), ' +
|
|
221
|
+
'`commodityPrice` (XAU-USD for gold, XAG-USD for silver, etc., free), ' +
|
|
222
|
+
'`stockPrice` (any of 1,746 tickers across us/hk/jp/kr/gb/de/fr/nl/ie/lu/cn/ca, BlockRun Gateway/Pyth, $0.001 per call paid from the agent wallet). ' +
|
|
223
|
+
'Prefer stockPrice for any equity question — CRCL, AAPL, 7203.JP, 0005.HK, etc.',
|
|
150
224
|
input_schema: {
|
|
151
225
|
type: 'object',
|
|
152
226
|
properties: {
|
|
153
227
|
action: {
|
|
154
228
|
type: 'string',
|
|
155
|
-
enum: ['price', 'trending', 'overview'],
|
|
156
|
-
description: 'What to fetch
|
|
229
|
+
enum: ['price', 'trending', 'overview', 'fxPrice', 'commodityPrice', 'stockPrice'],
|
|
230
|
+
description: 'What to fetch. See tool description for cost + source per action.',
|
|
157
231
|
},
|
|
158
232
|
ticker: {
|
|
159
233
|
type: 'string',
|
|
160
|
-
description: '
|
|
234
|
+
description: 'Ticker. Crypto: "BTC". FX: "EUR-USD". Commodity: "XAU-USD" (gold). Stock: "AAPL", "CRCL", "7203" (Toyota on jp), "0005" (HSBC on hk). Required for all price actions.',
|
|
235
|
+
},
|
|
236
|
+
market: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
enum: ['us', 'hk', 'jp', 'kr', 'gb', 'de', 'fr', 'nl', 'ie', 'lu', 'cn', 'ca'],
|
|
239
|
+
description: 'Stock exchange market code. Required when action="stockPrice". Ignored for other actions.',
|
|
161
240
|
},
|
|
162
241
|
},
|
|
163
242
|
required: ['action'],
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebhookPost — generic HTTP POST tool for pushing agent output to any
|
|
3
|
+
* webhook endpoint. Covers WeChat Work, Feishu, Discord, Slack, Telegram
|
|
4
|
+
* Bot API, PushPlus, ServerChan, custom HTTP receivers — anything that
|
|
5
|
+
* accepts a JSON body over POST.
|
|
6
|
+
*
|
|
7
|
+
* Intentionally not per-vendor: those integrations differ only in body
|
|
8
|
+
* shape, which the agent already knows how to construct. One tool, eight
|
|
9
|
+
* channels. If a channel needs a signature header (e.g., Feishu sign
|
|
10
|
+
* mode), the agent passes it in via `headers`.
|
|
11
|
+
*
|
|
12
|
+
* Safety: outbound URLs are a publish surface. We refuse localhost,
|
|
13
|
+
* private ranges, and file schemes so an agent can't be tricked into
|
|
14
|
+
* hitting internal services. A permission prompt fires on first use per
|
|
15
|
+
* session.
|
|
16
|
+
*/
|
|
17
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
18
|
+
export declare const webhookPostCapability: CapabilityHandler;
|