@blockrun/franklin 3.8.8 → 3.8.10
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/error-classifier.js +1 -0
- package/dist/agent/llm.d.ts +7 -0
- package/dist/agent/llm.js +48 -7
- package/dist/agent/loop.js +66 -3
- package/dist/agent/permissions.js +2 -2
- package/dist/agent/types.d.ts +7 -0
- package/dist/banner.js +15 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.js +72 -2
- package/dist/index.js +11 -3
- 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/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/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/dist/ui/app.js +28 -2
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivateTool — meta-capability that lets the agent pull on-demand tools
|
|
3
|
+
* into the active toolset per session.
|
|
4
|
+
*
|
|
5
|
+
* Pattern borrowed from OpenBB MCP server's per-session tool visibility:
|
|
6
|
+
* a weak model confronted with 25+ tool definitions starts inventing names
|
|
7
|
+
* or emits role-play "[TOOLCALL]" fragments. Register only the core file/
|
|
8
|
+
* shell tools by default and let the model explicitly opt in to the rest.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* - `ActivateTool()` with no args → lists every inactive tool with a
|
|
12
|
+
* one-line description so the model knows what's available.
|
|
13
|
+
* - `ActivateTool({ names: ["ExaSearch", "ExaReadUrls"] })` → adds the
|
|
14
|
+
* named tools to the session's active set; subsequent turns include
|
|
15
|
+
* their full schemas. Returns a concise confirmation.
|
|
16
|
+
*
|
|
17
|
+
* The factory captures the shared `activeTools` Set that the loop filters
|
|
18
|
+
* against and the full `allTools` map used for name resolution. Both live
|
|
19
|
+
* in the session — activation is not durable across restarts on purpose,
|
|
20
|
+
* since the model can always re-activate on the next turn if it needs to.
|
|
21
|
+
*/
|
|
22
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
23
|
+
export interface ActivateToolDeps {
|
|
24
|
+
/** Mutable set of tool names currently visible to the model. */
|
|
25
|
+
activeTools: Set<string>;
|
|
26
|
+
/** Map of every registered capability, keyed by name. */
|
|
27
|
+
allTools: Map<string, CapabilityHandler>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createActivateToolCapability(deps: ActivateToolDeps): CapabilityHandler;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivateTool — meta-capability that lets the agent pull on-demand tools
|
|
3
|
+
* into the active toolset per session.
|
|
4
|
+
*
|
|
5
|
+
* Pattern borrowed from OpenBB MCP server's per-session tool visibility:
|
|
6
|
+
* a weak model confronted with 25+ tool definitions starts inventing names
|
|
7
|
+
* or emits role-play "[TOOLCALL]" fragments. Register only the core file/
|
|
8
|
+
* shell tools by default and let the model explicitly opt in to the rest.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* - `ActivateTool()` with no args → lists every inactive tool with a
|
|
12
|
+
* one-line description so the model knows what's available.
|
|
13
|
+
* - `ActivateTool({ names: ["ExaSearch", "ExaReadUrls"] })` → adds the
|
|
14
|
+
* named tools to the session's active set; subsequent turns include
|
|
15
|
+
* their full schemas. Returns a concise confirmation.
|
|
16
|
+
*
|
|
17
|
+
* The factory captures the shared `activeTools` Set that the loop filters
|
|
18
|
+
* against and the full `allTools` map used for name resolution. Both live
|
|
19
|
+
* in the session — activation is not durable across restarts on purpose,
|
|
20
|
+
* since the model can always re-activate on the next turn if it needs to.
|
|
21
|
+
*/
|
|
22
|
+
function shortDesc(desc) {
|
|
23
|
+
// First sentence or first 120 chars, whichever is shorter.
|
|
24
|
+
const firstSentence = desc.split(/[.\n]/)[0]?.trim() ?? '';
|
|
25
|
+
if (firstSentence && firstSentence.length <= 120)
|
|
26
|
+
return firstSentence;
|
|
27
|
+
const trimmed = desc.replace(/\s+/g, ' ').trim();
|
|
28
|
+
return trimmed.length <= 120 ? trimmed : trimmed.slice(0, 117) + '...';
|
|
29
|
+
}
|
|
30
|
+
export function createActivateToolCapability(deps) {
|
|
31
|
+
const { activeTools, allTools } = deps;
|
|
32
|
+
return {
|
|
33
|
+
spec: {
|
|
34
|
+
name: 'ActivateTool',
|
|
35
|
+
description: 'Activate additional tools for this session. Most tools are hidden by default to keep your tool inventory small. ' +
|
|
36
|
+
'Call with no arguments to see what is available. Call with { "names": ["ToolA", "ToolB"] } to enable specific tools — ' +
|
|
37
|
+
'they become visible in your tool list on the next turn. Activate only what you need; extra tools crowd the inventory.',
|
|
38
|
+
input_schema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
names: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: { type: 'string' },
|
|
44
|
+
description: 'List of tool names to activate. Omit to list what is available.',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
concurrent: false,
|
|
50
|
+
async execute(input) {
|
|
51
|
+
const raw = input.names;
|
|
52
|
+
const names = Array.isArray(raw) ? raw.filter((n) => typeof n === 'string') : undefined;
|
|
53
|
+
// No args → catalog the inactive tools so the model knows what's there.
|
|
54
|
+
if (!names || names.length === 0) {
|
|
55
|
+
const inactive = [...allTools.values()]
|
|
56
|
+
.filter(t => !activeTools.has(t.spec.name))
|
|
57
|
+
.sort((a, b) => a.spec.name.localeCompare(b.spec.name));
|
|
58
|
+
if (inactive.length === 0) {
|
|
59
|
+
return { output: 'All registered tools are already active.' };
|
|
60
|
+
}
|
|
61
|
+
const lines = inactive.map(t => `- ${t.spec.name}: ${shortDesc(t.spec.description)}`);
|
|
62
|
+
return {
|
|
63
|
+
output: `Available on-demand tools (${inactive.length}). Activate with ` +
|
|
64
|
+
`ActivateTool({ "names": ["<name>", ...] }):\n` +
|
|
65
|
+
lines.join('\n'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Activate each named tool.
|
|
69
|
+
const activated = [];
|
|
70
|
+
const alreadyActive = [];
|
|
71
|
+
const unknown = [];
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
if (!allTools.has(name)) {
|
|
74
|
+
unknown.push(name);
|
|
75
|
+
}
|
|
76
|
+
else if (activeTools.has(name)) {
|
|
77
|
+
alreadyActive.push(name);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
activeTools.add(name);
|
|
81
|
+
activated.push(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (activated.length)
|
|
86
|
+
parts.push(`Activated: ${activated.join(', ')}`);
|
|
87
|
+
if (alreadyActive.length)
|
|
88
|
+
parts.push(`Already active: ${alreadyActive.join(', ')}`);
|
|
89
|
+
if (unknown.length)
|
|
90
|
+
parts.push(`Unknown (not registered): ${unknown.join(', ')}`);
|
|
91
|
+
const output = parts.length ? parts.join('. ') + '.' : 'No change.';
|
|
92
|
+
const isError = activated.length === 0 && unknown.length > 0;
|
|
93
|
+
return { output, isError };
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { tradingSignalCapability, tradingMarketCapability } from './trading.js';
|
|
|
22
22
|
import { searchXCapability } from './searchx.js';
|
|
23
23
|
import { postToXCapability } from './posttox.js';
|
|
24
24
|
import { moaCapability } from './moa.js';
|
|
25
|
+
import { webhookPostCapability } from './webhook.js';
|
|
25
26
|
import { createTradingCapabilities } from './trading-execute.js';
|
|
26
27
|
import { Portfolio } from '../trading/portfolio.js';
|
|
27
28
|
import { RiskEngine } from '../trading/risk.js';
|
|
@@ -138,6 +139,7 @@ export const allCapabilities = [
|
|
|
138
139
|
searchXCapability,
|
|
139
140
|
postToXCapability,
|
|
140
141
|
moaCapability,
|
|
142
|
+
webhookPostCapability,
|
|
141
143
|
];
|
|
142
144
|
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
143
145
|
export { createSubAgentCapability } from './subagent.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool visibility categories.
|
|
3
|
+
*
|
|
4
|
+
* Franklin ships with ~27 capabilities. Exposing all of them to the model on
|
|
5
|
+
* every turn makes the tool inventory large enough that weak models start
|
|
6
|
+
* hallucinating tool names or emitting role-play "[TOOLCALL]" fragments.
|
|
7
|
+
* The fix: keep a minimal always-on core (file ops, shell, ask) and gate the
|
|
8
|
+
* rest behind an `ActivateTool` meta-tool that the agent pulls on demand —
|
|
9
|
+
* the same per-session visibility pattern that OpenBB's MCP server uses.
|
|
10
|
+
*
|
|
11
|
+
* `CORE_TOOL_NAMES` is the per-session initial active set. Everything else
|
|
12
|
+
* becomes visible only after the agent calls ActivateTool with its name.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
|
|
15
|
+
/** True if this tool is always available without activation. */
|
|
16
|
+
export declare function isCoreTool(name: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Env opt-out: setting `FRANKLIN_DYNAMIC_TOOLS=0` disables the core/on-demand
|
|
19
|
+
* split and exposes every registered tool on every turn (pre-3.8.9 behavior).
|
|
20
|
+
* Kept as a safety valve for users whose workflows depend on the full surface.
|
|
21
|
+
*/
|
|
22
|
+
export declare function dynamicToolsEnabled(): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool visibility categories.
|
|
3
|
+
*
|
|
4
|
+
* Franklin ships with ~27 capabilities. Exposing all of them to the model on
|
|
5
|
+
* every turn makes the tool inventory large enough that weak models start
|
|
6
|
+
* hallucinating tool names or emitting role-play "[TOOLCALL]" fragments.
|
|
7
|
+
* The fix: keep a minimal always-on core (file ops, shell, ask) and gate the
|
|
8
|
+
* rest behind an `ActivateTool` meta-tool that the agent pulls on demand —
|
|
9
|
+
* the same per-session visibility pattern that OpenBB's MCP server uses.
|
|
10
|
+
*
|
|
11
|
+
* `CORE_TOOL_NAMES` is the per-session initial active set. Everything else
|
|
12
|
+
* becomes visible only after the agent calls ActivateTool with its name.
|
|
13
|
+
*/
|
|
14
|
+
export const CORE_TOOL_NAMES = new Set([
|
|
15
|
+
// File operations — nothing else works without these.
|
|
16
|
+
'Read',
|
|
17
|
+
'Write',
|
|
18
|
+
'Edit',
|
|
19
|
+
// Shell execution — needed for running tests, builds, scripts.
|
|
20
|
+
'Bash',
|
|
21
|
+
// Search — code exploration is table stakes.
|
|
22
|
+
'Grep',
|
|
23
|
+
'Glob',
|
|
24
|
+
// User dialogue — the agent must be able to ask for clarification.
|
|
25
|
+
'AskUser',
|
|
26
|
+
// Sub-agent delegation — the sub-agent has its own tool resolution,
|
|
27
|
+
// so keeping this in the core doesn't leak the full inventory.
|
|
28
|
+
'Task',
|
|
29
|
+
// The meta-tool itself — must always be callable so the agent can
|
|
30
|
+
// discover and activate the rest.
|
|
31
|
+
'ActivateTool',
|
|
32
|
+
]);
|
|
33
|
+
/** True if this tool is always available without activation. */
|
|
34
|
+
export function isCoreTool(name) {
|
|
35
|
+
return CORE_TOOL_NAMES.has(name);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Env opt-out: setting `FRANKLIN_DYNAMIC_TOOLS=0` disables the core/on-demand
|
|
39
|
+
* split and exposes every registered tool on every turn (pre-3.8.9 behavior).
|
|
40
|
+
* Kept as a safety valve for users whose workflows depend on the full surface.
|
|
41
|
+
*/
|
|
42
|
+
export function dynamicToolsEnabled() {
|
|
43
|
+
return process.env.FRANKLIN_DYNAMIC_TOOLS !== '0';
|
|
44
|
+
}
|
|
@@ -1,19 +1,13 @@
|
|
|
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
12
|
import type { CapabilityHandler } from '../agent/types.js';
|
|
19
13
|
import type { TradingEngine } from '../trading/engine.js';
|
|
@@ -21,15 +15,11 @@ import type { RiskConfig } from '../trading/risk.js';
|
|
|
21
15
|
import type { TradeLog } from '../trading/trade-log.js';
|
|
22
16
|
export interface TradingCapabilitiesDeps {
|
|
23
17
|
engine: TradingEngine;
|
|
24
|
-
/** Risk config used
|
|
18
|
+
/** Risk config used for "you're at X% of your exposure cap" readout. */
|
|
25
19
|
riskConfig?: RiskConfig;
|
|
26
|
-
/**
|
|
20
|
+
/** Hook run after state-changing calls — typically persists to disk. */
|
|
27
21
|
onStateChange?: () => void | Promise<void>;
|
|
28
|
-
/**
|
|
29
|
-
* Optional persistent trade log. When provided, opens and closes are
|
|
30
|
-
* appended to it and the TradingHistory capability is registered so the
|
|
31
|
-
* agent can query cross-session P&L.
|
|
32
|
-
*/
|
|
22
|
+
/** Persistent trade log; when provided, TradingHistory is registered. */
|
|
33
23
|
tradeLog?: TradeLog;
|
|
34
24
|
}
|
|
35
25
|
export declare function createTradingCapabilities(deps: TradingCapabilitiesDeps): CapabilityHandler[];
|
|
@@ -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;
|