@bloomai/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/SKILL.md +120 -0
- package/dist/index.js +805 -0
- package/package.json +24 -0
- package/src/api.ts +67 -0
- package/src/auth.ts +83 -0
- package/src/commands/auth.ts +45 -0
- package/src/commands/catalysts.ts +18 -0
- package/src/commands/collections.ts +20 -0
- package/src/commands/earnings.ts +18 -0
- package/src/commands/financials.ts +30 -0
- package/src/commands/info.ts +79 -0
- package/src/commands/market.ts +22 -0
- package/src/commands/news.ts +48 -0
- package/src/commands/options.ts +35 -0
- package/src/commands/portfolio.ts +28 -0
- package/src/commands/position.ts +44 -0
- package/src/commands/price.ts +76 -0
- package/src/commands/screen.ts +77 -0
- package/src/commands/sentiment.ts +51 -0
- package/src/commands/technicals.ts +25 -0
- package/src/commands/trades.ts +41 -0
- package/src/commands/transcript.ts +69 -0
- package/src/format.ts +179 -0
- package/src/global-opts.ts +9 -0
- package/src/index.ts +49 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bloomai/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for Bloom financial data - download market data to files for analysis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bloom": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["finance", "stocks", "cli", "bloom", "market-data"],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"cli-table3": "^0.6.5",
|
|
17
|
+
"commander": "^13.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.13.0",
|
|
21
|
+
"tsup": "^8.4.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { requireApiKey } from "./auth.js";
|
|
2
|
+
|
|
3
|
+
const MCP_URL = "https://api.getbloom.app/mcp/";
|
|
4
|
+
|
|
5
|
+
let requestId = 0;
|
|
6
|
+
|
|
7
|
+
interface McpResponse {
|
|
8
|
+
jsonrpc: string;
|
|
9
|
+
id: number;
|
|
10
|
+
result?: {
|
|
11
|
+
content: Array<{ type: string; text: string }>;
|
|
12
|
+
};
|
|
13
|
+
error?: {
|
|
14
|
+
code: number;
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function callTool(
|
|
20
|
+
toolName: string,
|
|
21
|
+
args: Record<string, unknown>
|
|
22
|
+
): Promise<unknown> {
|
|
23
|
+
const apiKey = requireApiKey();
|
|
24
|
+
requestId++;
|
|
25
|
+
|
|
26
|
+
const body = {
|
|
27
|
+
jsonrpc: "2.0",
|
|
28
|
+
method: "tools/call",
|
|
29
|
+
params: {
|
|
30
|
+
name: toolName,
|
|
31
|
+
arguments: args,
|
|
32
|
+
},
|
|
33
|
+
id: requestId,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const res = await fetch(MCP_URL, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${apiKey}`,
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(body),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const text = await res.text().catch(() => "");
|
|
47
|
+
throw new Error(`API error ${res.status}: ${text || res.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = (await res.json()) as McpResponse;
|
|
51
|
+
|
|
52
|
+
if (data.error) {
|
|
53
|
+
throw new Error(`MCP error ${data.error.code}: ${data.error.message}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!data.result?.content?.length) {
|
|
57
|
+
throw new Error("Empty response from API");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The MCP response wraps the data as a JSON string inside content[0].text
|
|
61
|
+
const text = data.result.content[0].text;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".config", "bloom-cli");
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
+
|
|
8
|
+
interface Config {
|
|
9
|
+
api_key?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readConfig(): Config {
|
|
13
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeConfig(config: Config): void {
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
24
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
25
|
+
mode: 0o600,
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
console.error(
|
|
30
|
+
`Error: Could not write config to ${CONFIG_FILE}: ${msg}\n` +
|
|
31
|
+
`Try using BLOOM_API_KEY environment variable instead.`
|
|
32
|
+
);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getApiKey(): string | undefined {
|
|
38
|
+
return process.env.BLOOM_API_KEY || readConfig().api_key;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function requireApiKey(): string {
|
|
42
|
+
const key = getApiKey();
|
|
43
|
+
if (!key) {
|
|
44
|
+
console.error(
|
|
45
|
+
"Error: No API key found. Set BLOOM_API_KEY or run: bloom auth login"
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return key;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function login(apiKey: string): void {
|
|
53
|
+
const config = readConfig();
|
|
54
|
+
config.api_key = apiKey;
|
|
55
|
+
writeConfig(config);
|
|
56
|
+
console.log("API key saved to " + CONFIG_FILE);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function logout(): void {
|
|
60
|
+
if (existsSync(CONFIG_FILE)) {
|
|
61
|
+
const config = readConfig();
|
|
62
|
+
delete config.api_key;
|
|
63
|
+
writeConfig(config);
|
|
64
|
+
}
|
|
65
|
+
console.log("Logged out. API key cleared.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function status(): void {
|
|
69
|
+
const envKey = process.env.BLOOM_API_KEY;
|
|
70
|
+
const storedKey = readConfig().api_key;
|
|
71
|
+
if (envKey) {
|
|
72
|
+
console.log(`Authenticated via BLOOM_API_KEY (${maskKey(envKey)})`);
|
|
73
|
+
} else if (storedKey) {
|
|
74
|
+
console.log(`Authenticated via ${CONFIG_FILE} (${maskKey(storedKey)})`);
|
|
75
|
+
} else {
|
|
76
|
+
console.log("Not authenticated. Set BLOOM_API_KEY or run: bloom auth login");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function maskKey(key: string): string {
|
|
81
|
+
if (key.length <= 8) return "****";
|
|
82
|
+
return key.slice(0, 4) + "..." + key.slice(-4);
|
|
83
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { login, logout, status } from "../auth.js";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
|
|
5
|
+
export function registerAuthCommand(program: Command): void {
|
|
6
|
+
const auth = program.command("auth").description("Manage API authentication");
|
|
7
|
+
|
|
8
|
+
auth
|
|
9
|
+
.command("login")
|
|
10
|
+
.description("Store API key")
|
|
11
|
+
.argument("[key]", "API key (or pass via stdin)")
|
|
12
|
+
.action(async (key?: string) => {
|
|
13
|
+
if (!key) {
|
|
14
|
+
const rl = createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stderr,
|
|
17
|
+
});
|
|
18
|
+
key = await new Promise<string>((resolve) => {
|
|
19
|
+
rl.question("API key: ", (answer) => {
|
|
20
|
+
rl.close();
|
|
21
|
+
resolve(answer.trim());
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (!key) {
|
|
26
|
+
console.error("Error: No API key provided");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
login(key);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
auth
|
|
33
|
+
.command("status")
|
|
34
|
+
.description("Show current auth state")
|
|
35
|
+
.action(() => {
|
|
36
|
+
status();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
auth
|
|
40
|
+
.command("logout")
|
|
41
|
+
.description("Clear stored credentials")
|
|
42
|
+
.action(() => {
|
|
43
|
+
logout();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerCatalystsCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("catalysts")
|
|
10
|
+
.description("Forward-looking value catalysts")
|
|
11
|
+
.argument("<symbols...>", "Ticker symbols (up to 5)")
|
|
12
|
+
).action(async (symbols: string[], opts: OutputOptions) => {
|
|
13
|
+
const data = await callTool("generate_catalysts", {
|
|
14
|
+
symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5),
|
|
15
|
+
});
|
|
16
|
+
outputResult(data, opts);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions, parseIntSafe } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerCollectionsCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("collections")
|
|
10
|
+
.description("Curated stock collections by theme")
|
|
11
|
+
.option("--section <s>", "GET_STARTED|VALUE|TRENDS|BEAT_UP|FAMOUS_INVESTORS|HIGH_FLYERS|SECTORS|GEO|LEADERBOARDS", "GET_STARTED")
|
|
12
|
+
.option("--limit <n>", "Max collections", "6")
|
|
13
|
+
).action(async (opts: OutputOptions & { section: string; limit: string }) => {
|
|
14
|
+
const data = await callTool("get_collections", {
|
|
15
|
+
section: opts.section,
|
|
16
|
+
limit: parseIntSafe(opts.limit, 6),
|
|
17
|
+
});
|
|
18
|
+
outputResult(data, opts);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerEarningsCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("earnings")
|
|
10
|
+
.description("Earnings history, estimates, surprises")
|
|
11
|
+
.argument("<symbols...>", "Ticker symbols (up to 5)")
|
|
12
|
+
).action(async (symbols: string[], opts: OutputOptions) => {
|
|
13
|
+
const data = await callTool("analyze_earnings", {
|
|
14
|
+
symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5),
|
|
15
|
+
});
|
|
16
|
+
outputResult(data, opts);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
const CHART_TYPES = [
|
|
7
|
+
"revenue",
|
|
8
|
+
"revenue_growth",
|
|
9
|
+
"operating_margin",
|
|
10
|
+
"net_earnings",
|
|
11
|
+
"net_earnings_growth",
|
|
12
|
+
"free_cash_flow",
|
|
13
|
+
"free_cash_flow_growth",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function registerFinancialsCommand(program: Command): void {
|
|
17
|
+
addGlobalOptions(
|
|
18
|
+
program
|
|
19
|
+
.command("financials")
|
|
20
|
+
.description("Quarterly/annual financial data with YoY growth")
|
|
21
|
+
.argument("<symbol>", "Ticker symbol")
|
|
22
|
+
.option("--type <type>", "Chart type: " + CHART_TYPES.join(", "), "revenue")
|
|
23
|
+
).action(async (symbol: string, opts: OutputOptions & { type: string }) => {
|
|
24
|
+
const data = await callTool("get_financial_charts", {
|
|
25
|
+
symbol: symbol.toUpperCase(),
|
|
26
|
+
chart_type: opts.type,
|
|
27
|
+
});
|
|
28
|
+
outputResult(data, opts);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
const VALID_FIELDS = [
|
|
7
|
+
"basic_metadata",
|
|
8
|
+
"bottom_line_ratings",
|
|
9
|
+
"embedded_expectations",
|
|
10
|
+
"peers",
|
|
11
|
+
"key_events",
|
|
12
|
+
"holdings",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function registerInfoCommand(program: Command): void {
|
|
16
|
+
addGlobalOptions(
|
|
17
|
+
program
|
|
18
|
+
.command("info")
|
|
19
|
+
.description("Company fundamentals, ratings, peers, holdings")
|
|
20
|
+
.argument("<symbols...>", "Ticker symbols (e.g. AAPL MSFT)")
|
|
21
|
+
.option("--fields <fields>", "Comma-separated: " + VALID_FIELDS.join(","))
|
|
22
|
+
).action(async (symbols: string[], opts: OutputOptions & { fields?: string }) => {
|
|
23
|
+
const args: Record<string, unknown> = {
|
|
24
|
+
symbols: symbols.map((s) => s.toUpperCase()),
|
|
25
|
+
};
|
|
26
|
+
if (opts.fields) {
|
|
27
|
+
args.fields = opts.fields.split(",").map((f) => f.trim());
|
|
28
|
+
}
|
|
29
|
+
const data = await callTool("get_symbols_metadata", args);
|
|
30
|
+
outputResult(data, opts, (d) => formatInfo(d));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatInfo(data: unknown): string {
|
|
35
|
+
if (!data || typeof data !== "object") return String(data);
|
|
36
|
+
const obj = data as Record<string, unknown>;
|
|
37
|
+
const sections: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
40
|
+
if (key === "timestamp" || key === "source") continue;
|
|
41
|
+
if (val && typeof val === "object") {
|
|
42
|
+
sections.push(`\n=== ${key.toUpperCase()} ===`);
|
|
43
|
+
sections.push(formatSection(val as Record<string, unknown>));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return sections.length > 0 ? sections.join("\n") : JSON.stringify(data, null, 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatSection(obj: Record<string, unknown>, indent = ""): string {
|
|
51
|
+
const lines: string[] = [];
|
|
52
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
53
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
54
|
+
lines.push(`${indent}${k}:`);
|
|
55
|
+
lines.push(formatSection(v as Record<string, unknown>, indent + " "));
|
|
56
|
+
} else if (Array.isArray(v)) {
|
|
57
|
+
if (v.length === 0) continue;
|
|
58
|
+
if (typeof v[0] === "string" || typeof v[0] === "number") {
|
|
59
|
+
lines.push(`${indent}${k}: ${v.join(", ")}`);
|
|
60
|
+
} else {
|
|
61
|
+
lines.push(`${indent}${k}:`);
|
|
62
|
+
for (const item of v) {
|
|
63
|
+
if (typeof item === "object" && item) {
|
|
64
|
+
const summary = Object.entries(item as Record<string, unknown>)
|
|
65
|
+
.slice(0, 4)
|
|
66
|
+
.map(([ik, iv]) => `${ik}=${iv}`)
|
|
67
|
+
.join(" ");
|
|
68
|
+
lines.push(`${indent} - ${summary}`);
|
|
69
|
+
} else {
|
|
70
|
+
lines.push(`${indent} - ${item}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
lines.push(`${indent}${k}: ${v}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions, parseIntSafe } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerMarketCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("market")
|
|
10
|
+
.description("Market performance, sectors, global")
|
|
11
|
+
.option("--type <type>", "top_movers|sector_breakdown|global_markets|major_indexes", "top_movers")
|
|
12
|
+
.option("--period <p>", "1d|1w|1m|3m|6m|ytd|1y|5y", "1d")
|
|
13
|
+
.option("--limit <n>", "Max stocks for top_movers", "24")
|
|
14
|
+
).action(async (opts: OutputOptions & { type: string; period: string; limit: string }) => {
|
|
15
|
+
const data = await callTool("get_market_map", {
|
|
16
|
+
map_type: opts.type,
|
|
17
|
+
period: opts.period,
|
|
18
|
+
limit: parseIntSafe(opts.limit, 24),
|
|
19
|
+
});
|
|
20
|
+
outputResult(data, opts);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions, makeTable, parseIntSafe } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerNewsCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("news")
|
|
10
|
+
.description("Recent news from Benzinga channels")
|
|
11
|
+
.argument("<symbols...>", "Ticker symbols")
|
|
12
|
+
.option("--limit <n>", "Max articles (default: 15)", "15")
|
|
13
|
+
).action(async (symbols: string[], opts: OutputOptions & { limit: string }) => {
|
|
14
|
+
const data = await callTool("get_news_for_symbols", {
|
|
15
|
+
symbols: symbols.map((s) => s.toUpperCase()),
|
|
16
|
+
limit: parseIntSafe(opts.limit, 15),
|
|
17
|
+
});
|
|
18
|
+
outputResult(data, opts, (d, o) => formatNews(d, o));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatNews(data: unknown, opts: OutputOptions): string {
|
|
23
|
+
if (!data || typeof data !== "object") return String(data);
|
|
24
|
+
const obj = data as Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
let articles: Record<string, unknown>[] = [];
|
|
27
|
+
if (Array.isArray(data)) {
|
|
28
|
+
articles = data as Record<string, unknown>[];
|
|
29
|
+
} else {
|
|
30
|
+
for (const val of Object.values(obj)) {
|
|
31
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
32
|
+
articles = val as Record<string, unknown>[];
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (articles.length === 0) return "(no articles)";
|
|
39
|
+
|
|
40
|
+
const rows = articles.map((a) => [
|
|
41
|
+
String(a.date || a.published || a.created || "").slice(0, 16),
|
|
42
|
+
String(a.title || a.headline || "").slice(0, 80),
|
|
43
|
+
String(a.symbols || a.tickers || a.stocks || "").slice(0, 20),
|
|
44
|
+
String(a.channel || a.source || ""),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
return makeTable(["Date", "Title", "Symbols", "Channel"], rows, opts);
|
|
48
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions, parseIntSafe } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerOptionsCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("options-history")
|
|
10
|
+
.description("Options sentiment evolution over time")
|
|
11
|
+
.argument("<symbol>", "Ticker symbol")
|
|
12
|
+
.option("--days <n>", "Lookback days (7-90)", "30")
|
|
13
|
+
.option("--weekly", "Use weekly sampling (default)")
|
|
14
|
+
.option("--daily", "Use daily data")
|
|
15
|
+
.option("--greeks", "Include advanced Greeks analysis")
|
|
16
|
+
).action(
|
|
17
|
+
async (
|
|
18
|
+
symbol: string,
|
|
19
|
+
opts: OutputOptions & {
|
|
20
|
+
days: string;
|
|
21
|
+
weekly?: boolean;
|
|
22
|
+
daily?: boolean;
|
|
23
|
+
greeks?: boolean;
|
|
24
|
+
}
|
|
25
|
+
) => {
|
|
26
|
+
const data = await callTool("analyze_options_history", {
|
|
27
|
+
symbol: symbol.toUpperCase(),
|
|
28
|
+
lookback_days: parseIntSafe(opts.days, 30),
|
|
29
|
+
weekly_sampling: opts.weekly ?? !opts.daily,
|
|
30
|
+
include_greeks_analysis: !!opts.greeks,
|
|
31
|
+
});
|
|
32
|
+
outputResult(data, opts);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerPortfolioCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("portfolio")
|
|
10
|
+
.description("Portfolio risk-return analysis")
|
|
11
|
+
.argument("<holdings>", 'JSON holdings, e.g. \'{"AAPL":0.3,"MSFT":0.7}\'')
|
|
12
|
+
.option("--strategy <s>", "Investment strategy context")
|
|
13
|
+
).action(async (holdingsStr: string, opts: OutputOptions & { strategy?: string }) => {
|
|
14
|
+
let holdings: Record<string, number>;
|
|
15
|
+
try {
|
|
16
|
+
holdings = JSON.parse(holdingsStr);
|
|
17
|
+
} catch {
|
|
18
|
+
console.error('Error: Invalid JSON for holdings. Use format: \'{"AAPL":0.3,"MSFT":0.7}\'');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const args: Record<string, unknown> = { holdings };
|
|
23
|
+
if (opts.strategy) args.strategy = opts.strategy;
|
|
24
|
+
|
|
25
|
+
const data = await callTool("analyze_portfolio", args);
|
|
26
|
+
outputResult(data, opts);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions, parseIntSafe } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerPositionCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("position-size")
|
|
10
|
+
.description("Kelly criterion position sizing")
|
|
11
|
+
.requiredOption("--bull <n>", "Expected bull return (decimal, e.g. 0.6)")
|
|
12
|
+
.requiredOption("--bear <n>", "Expected bear return (decimal, e.g. -0.15)")
|
|
13
|
+
.requiredOption("--conviction <n>", "Conviction level 1-10")
|
|
14
|
+
.requiredOption("--value <n>", "Portfolio value in dollars")
|
|
15
|
+
.option("--symbol <s>", "Optional symbol for valuation context")
|
|
16
|
+
).action(
|
|
17
|
+
async (
|
|
18
|
+
opts: OutputOptions & {
|
|
19
|
+
bull: string;
|
|
20
|
+
bear: string;
|
|
21
|
+
conviction: string;
|
|
22
|
+
value: string;
|
|
23
|
+
symbol?: string;
|
|
24
|
+
}
|
|
25
|
+
) => {
|
|
26
|
+
const bullReturn = parseFloat(opts.bull);
|
|
27
|
+
const bearReturn = parseFloat(opts.bear);
|
|
28
|
+
const portfolioValue = parseFloat(opts.value);
|
|
29
|
+
if (isNaN(bullReturn)) { console.error("Error: --bull must be a decimal number (e.g. 0.6)"); process.exit(1); }
|
|
30
|
+
if (isNaN(bearReturn)) { console.error("Error: --bear must be a decimal number (e.g. -0.15)"); process.exit(1); }
|
|
31
|
+
if (isNaN(portfolioValue)) { console.error("Error: --value must be a number"); process.exit(1); }
|
|
32
|
+
const args: Record<string, unknown> = {
|
|
33
|
+
bull_return: bullReturn,
|
|
34
|
+
bear_return: bearReturn,
|
|
35
|
+
conviction: parseIntSafe(opts.conviction, 5),
|
|
36
|
+
portfolio_value: portfolioValue,
|
|
37
|
+
};
|
|
38
|
+
if (opts.symbol) args.symbol = opts.symbol.toUpperCase();
|
|
39
|
+
|
|
40
|
+
const data = await callTool("calculate_position_size", args);
|
|
41
|
+
outputResult(data, opts);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { callTool } from "../api.js";
|
|
3
|
+
import { outputResult, OutputOptions, makeTable } from "../format.js";
|
|
4
|
+
import { addGlobalOptions } from "../global-opts.js";
|
|
5
|
+
|
|
6
|
+
export function registerPriceCommand(program: Command): void {
|
|
7
|
+
addGlobalOptions(
|
|
8
|
+
program
|
|
9
|
+
.command("price")
|
|
10
|
+
.description("Historical price data")
|
|
11
|
+
.argument("<symbol>", "Ticker symbol")
|
|
12
|
+
.option("--timeframes <tf>", "Comma-separated: 1d,1w,1m,3m,6m,ytd,1y,2y,5y", "1d,1m,1y")
|
|
13
|
+
.option("--ohlc", "Include full OHLC data")
|
|
14
|
+
).action(async (symbol: string, opts: OutputOptions & { timeframes: string; ohlc?: boolean }) => {
|
|
15
|
+
const data = await callTool("get_price_charts", {
|
|
16
|
+
symbol: symbol.toUpperCase(),
|
|
17
|
+
timeframes: opts.timeframes.split(",").map((t) => t.trim()),
|
|
18
|
+
include_ohlc: !!opts.ohlc,
|
|
19
|
+
});
|
|
20
|
+
outputResult(data, opts, (d, o) => formatPrices(d, o));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatPrices(data: unknown, opts: OutputOptions): string {
|
|
25
|
+
if (!data || typeof data !== "object") return String(data);
|
|
26
|
+
const obj = data as Record<string, unknown>;
|
|
27
|
+
const sections: string[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [tf, val] of Object.entries(obj)) {
|
|
30
|
+
if (tf === "symbol" || tf === "timestamp" || tf === "source" || tf === "type") continue;
|
|
31
|
+
if (!val || typeof val !== "object") continue;
|
|
32
|
+
|
|
33
|
+
const tfData = val as Record<string, unknown>;
|
|
34
|
+
let points: Record<string, unknown>[] = [];
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(tfData)) {
|
|
37
|
+
points = tfData as Record<string, unknown>[];
|
|
38
|
+
} else if (tfData.data && Array.isArray(tfData.data)) {
|
|
39
|
+
points = tfData.data as Record<string, unknown>[];
|
|
40
|
+
} else if (tfData.prices && Array.isArray(tfData.prices)) {
|
|
41
|
+
points = tfData.prices as Record<string, unknown>[];
|
|
42
|
+
} else {
|
|
43
|
+
// Fallback: find the first array of objects (skip primitive arrays)
|
|
44
|
+
for (const v of Object.values(tfData)) {
|
|
45
|
+
if (Array.isArray(v) && v.length > 0 && typeof v[0] === "object" && v[0] !== null) {
|
|
46
|
+
points = v as Record<string, unknown>[];
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (points.length === 0) continue;
|
|
53
|
+
|
|
54
|
+
sections.push(`\n--- ${tf.toUpperCase()} (${points.length} points) ---`);
|
|
55
|
+
|
|
56
|
+
const show = points.length <= 12 ? points : [
|
|
57
|
+
...points.slice(0, 5),
|
|
58
|
+
{ date: "...", price: "..." } as Record<string, unknown>,
|
|
59
|
+
...points.slice(-5),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const rows = show.map((p) => [
|
|
63
|
+
String(p.date ?? p.datetime ?? p.timestamp ?? ""),
|
|
64
|
+
String(p.price ?? p.close ?? ""),
|
|
65
|
+
p.open != null ? String(p.open) : "",
|
|
66
|
+
p.high != null ? String(p.high) : "",
|
|
67
|
+
p.low != null ? String(p.low) : "",
|
|
68
|
+
p.volume != null ? String(p.volume) : "",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const headers = ["Date", "Price", "Open", "High", "Low", "Volume"];
|
|
72
|
+
sections.push(makeTable(headers, rows, opts));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return sections.join("\n") || JSON.stringify(data, null, 2);
|
|
76
|
+
}
|