agentbnb 5.1.0 → 5.1.2
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/cli/index.js +9 -5
- package/dist/index.js +4 -1
- package/dist/{service-coordinator-5R4LQW6L.js → service-coordinator-UTKI4FRI.js} +7 -2
- package/dist/skills/agentbnb/bootstrap.js +22 -7
- package/package.json +1 -1
- package/skills/agentbnb/SKILL.md +10 -2
- package/skills/agentbnb/bootstrap.ts +17 -6
- package/skills/deep-stock-analyst/package.json +24 -0
- package/skills/deep-stock-analyst/src/analysis/financial-health.ts +167 -0
- package/skills/deep-stock-analyst/src/analysis/sentiment.ts +68 -0
- package/skills/deep-stock-analyst/src/analysis/signal.ts +188 -0
- package/skills/deep-stock-analyst/src/analysis/technicals.ts +318 -0
- package/skills/deep-stock-analyst/src/analysis/utils.ts +137 -0
- package/skills/deep-stock-analyst/src/analysis/valuation.ts +95 -0
- package/skills/deep-stock-analyst/src/api/alpha-vantage.ts +133 -0
- package/skills/deep-stock-analyst/src/api/types.ts +238 -0
- package/skills/deep-stock-analyst/src/index.ts +84 -0
- package/skills/deep-stock-analyst/src/llm/thesis.ts +101 -0
- package/skills/deep-stock-analyst/src/orchestrator.ts +228 -0
- package/skills/deep-stock-analyst/tsconfig.json +21 -0
package/dist/cli/index.js
CHANGED
|
@@ -590,7 +590,10 @@ program.command("init").description("Initialize AgentBnB config and create agent
|
|
|
590
590
|
credit_db_path: creditDbPath,
|
|
591
591
|
token: existingConfig?.token ?? token,
|
|
592
592
|
// Preserve existing token
|
|
593
|
-
api_key: existingConfig?.api_key ?? randomBytes(32).toString("hex")
|
|
593
|
+
api_key: existingConfig?.api_key ?? randomBytes(32).toString("hex"),
|
|
594
|
+
// Default registry for fresh installs: auto-set in --yes (automated) mode only.
|
|
595
|
+
// Interactive init leaves registry unset so users can configure it explicitly.
|
|
596
|
+
...existingConfig?.registry ? { registry: existingConfig.registry } : opts.yes ? { registry: "https://agentbnb.fly.dev" } : {}
|
|
594
597
|
};
|
|
595
598
|
saveConfig(config);
|
|
596
599
|
let keypairStatus = "existing";
|
|
@@ -651,11 +654,11 @@ program.command("init").description("Initialize AgentBnB config and create agent
|
|
|
651
654
|
bootstrapAgent(creditDb, owner, 100);
|
|
652
655
|
creditDb.close();
|
|
653
656
|
let registryBalance;
|
|
654
|
-
if (
|
|
657
|
+
if (config.registry) {
|
|
655
658
|
try {
|
|
656
659
|
const identityAuth = loadIdentityAuth(owner);
|
|
657
660
|
const ledger = createLedger({
|
|
658
|
-
registryUrl:
|
|
661
|
+
registryUrl: config.registry,
|
|
659
662
|
ownerPublicKey: identityAuth.publicKey,
|
|
660
663
|
privateKey: identityAuth.privateKey
|
|
661
664
|
});
|
|
@@ -1310,8 +1313,9 @@ Batch Results (${res.results.length} items):`);
|
|
|
1310
1313
|
}
|
|
1311
1314
|
}
|
|
1312
1315
|
const useReceipt = isRemoteRequest && opts.receipt !== false;
|
|
1316
|
+
const isRelayOnly = isRemoteRequest && !gatewayUrl;
|
|
1313
1317
|
const useRegistryLedger = isRemoteRequest && !!config.registry && !!gatewayUrl;
|
|
1314
|
-
if (useReceipt && !opts.cost) {
|
|
1318
|
+
if (useReceipt && !opts.cost && !isRelayOnly) {
|
|
1315
1319
|
console.error("Error: --cost <credits> is required for remote requests. Specify the credits to commit.");
|
|
1316
1320
|
process.exit(1);
|
|
1317
1321
|
}
|
|
@@ -1552,7 +1556,7 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
1552
1556
|
process.exit(1);
|
|
1553
1557
|
}
|
|
1554
1558
|
const { ProcessGuard } = await import("../process-guard-CC7CNRQJ.js");
|
|
1555
|
-
const { ServiceCoordinator } = await import("../service-coordinator-
|
|
1559
|
+
const { ServiceCoordinator } = await import("../service-coordinator-UTKI4FRI.js");
|
|
1556
1560
|
const port = opts.port ? parseInt(opts.port, 10) : config.gateway_port;
|
|
1557
1561
|
const registryPort = parseInt(opts.registryPort, 10);
|
|
1558
1562
|
if (!Number.isFinite(port) || !Number.isFinite(registryPort)) {
|
package/dist/index.js
CHANGED
|
@@ -1929,10 +1929,13 @@ var CommandExecutor = class {
|
|
|
1929
1929
|
const timeout = cmdConfig.timeout_ms ?? 3e4;
|
|
1930
1930
|
const cwd = cmdConfig.working_dir ?? process.cwd();
|
|
1931
1931
|
let stdout;
|
|
1932
|
+
const env = { ...process.env };
|
|
1933
|
+
delete env["CLAUDECODE"];
|
|
1932
1934
|
try {
|
|
1933
|
-
const result = await execFileAsync2("/bin/sh", ["-c", interpolatedCommand], {
|
|
1935
|
+
const result = await execFileAsync2("/bin/sh", ["-c", `${interpolatedCommand} < /dev/null`], {
|
|
1934
1936
|
timeout,
|
|
1935
1937
|
cwd,
|
|
1938
|
+
env,
|
|
1936
1939
|
maxBuffer: 10 * 1024 * 1024
|
|
1937
1940
|
// 10 MB
|
|
1938
1941
|
});
|
|
@@ -777,10 +777,13 @@ var CommandExecutor = class {
|
|
|
777
777
|
const timeout = cmdConfig.timeout_ms ?? 3e4;
|
|
778
778
|
const cwd = cmdConfig.working_dir ?? process.cwd();
|
|
779
779
|
let stdout;
|
|
780
|
+
const env = { ...process.env };
|
|
781
|
+
delete env["CLAUDECODE"];
|
|
780
782
|
try {
|
|
781
|
-
const result = await execFileAsync2("/bin/sh", ["-c", interpolatedCommand], {
|
|
783
|
+
const result = await execFileAsync2("/bin/sh", ["-c", `${interpolatedCommand} < /dev/null`], {
|
|
782
784
|
timeout,
|
|
783
785
|
cwd,
|
|
786
|
+
env,
|
|
784
787
|
maxBuffer: 10 * 1024 * 1024
|
|
785
788
|
// 10 MB
|
|
786
789
|
});
|
|
@@ -3234,8 +3237,10 @@ function createRegistryServer(opts) {
|
|
|
3234
3237
|
const __filename = fileURLToPath(import.meta.url);
|
|
3235
3238
|
const __dirname = dirname(__filename);
|
|
3236
3239
|
const hubDistCandidates = [
|
|
3240
|
+
join(__dirname, "../hub/dist"),
|
|
3241
|
+
// When in dist/ (tsup chunk, e.g. dist/server-XYZ.js)
|
|
3237
3242
|
join(__dirname, "../../hub/dist"),
|
|
3238
|
-
// When
|
|
3243
|
+
// When in dist/registry/ or dist/cli/
|
|
3239
3244
|
join(__dirname, "../../../hub/dist")
|
|
3240
3245
|
// Fallback for alternative layouts
|
|
3241
3246
|
];
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
// skills/agentbnb/bootstrap.ts
|
|
68
68
|
import { join as join5 } from "path";
|
|
69
69
|
import { homedir as homedir3 } from "os";
|
|
70
|
+
import { spawnSync } from "child_process";
|
|
70
71
|
|
|
71
72
|
// src/runtime/process-guard.ts
|
|
72
73
|
import { dirname, join } from "path";
|
|
@@ -934,10 +935,13 @@ var CommandExecutor = class {
|
|
|
934
935
|
const timeout = cmdConfig.timeout_ms ?? 3e4;
|
|
935
936
|
const cwd = cmdConfig.working_dir ?? process.cwd();
|
|
936
937
|
let stdout;
|
|
938
|
+
const env = { ...process.env };
|
|
939
|
+
delete env["CLAUDECODE"];
|
|
937
940
|
try {
|
|
938
|
-
const result = await execFileAsync2("/bin/sh", ["-c", interpolatedCommand], {
|
|
941
|
+
const result = await execFileAsync2("/bin/sh", ["-c", `${interpolatedCommand} < /dev/null`], {
|
|
939
942
|
timeout,
|
|
940
943
|
cwd,
|
|
944
|
+
env,
|
|
941
945
|
maxBuffer: 10 * 1024 * 1024
|
|
942
946
|
// 10 MB
|
|
943
947
|
});
|
|
@@ -4035,8 +4039,10 @@ function createRegistryServer(opts) {
|
|
|
4035
4039
|
const __filename = fileURLToPath(import.meta.url);
|
|
4036
4040
|
const __dirname = dirname2(__filename);
|
|
4037
4041
|
const hubDistCandidates = [
|
|
4042
|
+
join3(__dirname, "../hub/dist"),
|
|
4043
|
+
// When in dist/ (tsup chunk, e.g. dist/server-XYZ.js)
|
|
4038
4044
|
join3(__dirname, "../../hub/dist"),
|
|
4039
|
-
// When
|
|
4045
|
+
// When in dist/registry/ or dist/cli/
|
|
4040
4046
|
join3(__dirname, "../../../hub/dist")
|
|
4041
4047
|
// Fallback for alternative layouts
|
|
4042
4048
|
];
|
|
@@ -6131,12 +6137,21 @@ function isNetworkError(err) {
|
|
|
6131
6137
|
|
|
6132
6138
|
// skills/agentbnb/bootstrap.ts
|
|
6133
6139
|
async function activate(config = {}) {
|
|
6134
|
-
|
|
6140
|
+
let agentConfig = loadConfig();
|
|
6135
6141
|
if (!agentConfig) {
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6142
|
+
const result = spawnSync("agentbnb", ["init", "--yes", "--no-detect"], {
|
|
6143
|
+
stdio: "pipe",
|
|
6144
|
+
env: { ...process.env },
|
|
6145
|
+
encoding: "utf-8"
|
|
6146
|
+
});
|
|
6147
|
+
if (result.error || result.status !== 0) {
|
|
6148
|
+
const msg = result.error?.message ?? result.stderr?.trim() ?? "agentbnb init failed";
|
|
6149
|
+
throw new AgentBnBError(`Auto-init failed: ${msg}`, "INIT_FAILED");
|
|
6150
|
+
}
|
|
6151
|
+
agentConfig = loadConfig();
|
|
6152
|
+
if (!agentConfig) {
|
|
6153
|
+
throw new AgentBnBError("AgentBnB config still not found after auto-init", "CONFIG_NOT_FOUND");
|
|
6154
|
+
}
|
|
6140
6155
|
}
|
|
6141
6156
|
const guard = new ProcessGuard(join5(homedir3(), ".agentbnb", ".pid"));
|
|
6142
6157
|
const coordinator = new ServiceCoordinator(agentConfig, guard);
|
package/package.json
CHANGED
package/skills/agentbnb/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: "Requires Node.js >= 20 and pnpm. Designed for OpenClaw agents. Compatible with Claude Code, Gemini CLI, and other AgentSkills-compatible tools."
|
|
6
6
|
metadata:
|
|
7
7
|
author: "Cheng Wen Chen"
|
|
8
|
-
version: "5.1.
|
|
8
|
+
version: "5.1.2"
|
|
9
9
|
tags: "ai-agent-skill,claude-code,agent-skills,p2p,capability-sharing"
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -165,12 +165,20 @@ agentbnb config set tier1 <N> # Set Tier 1 credit threshold
|
|
|
165
165
|
agentbnb config set tier2 <N> # Set Tier 2 credit threshold
|
|
166
166
|
agentbnb config set reserve <N> # Set minimum credit reserve floor
|
|
167
167
|
agentbnb discover # Find peers on the local network via mDNS
|
|
168
|
-
agentbnb
|
|
168
|
+
agentbnb discover --registry # Search remote registry for capability cards
|
|
169
|
+
agentbnb request <cardId> --skill <skillId> --params '{"key":"val"}' --json
|
|
170
|
+
# Request a capability (relay-only: no --cost needed)
|
|
171
|
+
agentbnb request <cardId> --skill <skillId> --params '{"key":"val"}' --cost 5 --json
|
|
172
|
+
# Request a capability with direct HTTP escrow payment
|
|
169
173
|
```
|
|
170
174
|
|
|
171
175
|
> **Note:** When using OpenClaw, `activate()` handles node startup automatically.
|
|
172
176
|
> `agentbnb serve` is only needed when running AgentBnB as a standalone CLI process.
|
|
173
177
|
|
|
178
|
+
> **Multi-agent tip:** If multiple agents share the same machine, each agent should use its own
|
|
179
|
+
> config directory. Set `AGENTBNB_DIR=<path>` before any `agentbnb` CLI call, or pass it in
|
|
180
|
+
> the shell environment. Example: `AGENTBNB_DIR=~/.openclaw/agents/mybot/.agentbnb agentbnb status`
|
|
181
|
+
|
|
174
182
|
## Adapters
|
|
175
183
|
|
|
176
184
|
Individual adapters are available if you need custom wiring outside of `bootstrap.ts`.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
13
14
|
|
|
14
15
|
import { loadConfig } from '../../src/cli/config.js';
|
|
15
16
|
import { AgentBnBError } from '../../src/types/index.js';
|
|
@@ -48,18 +49,28 @@ export interface BootstrapContext {
|
|
|
48
49
|
* Brings an AgentBnB node online (idempotent — safe to call when already running).
|
|
49
50
|
* Registers SIGTERM/SIGINT handlers that conditionally stop the node on process exit.
|
|
50
51
|
*
|
|
51
|
-
* @throws {AgentBnBError}
|
|
52
|
+
* @throws {AgentBnBError} INIT_FAILED if auto-init fails when no config exists.
|
|
52
53
|
*
|
|
53
54
|
* TODO: Once ServiceCoordinator gains its own signal handling, remove the handlers
|
|
54
55
|
* registered here to avoid double-handler conflicts. Track in Layer A implementation.
|
|
55
56
|
*/
|
|
56
57
|
export async function activate(config: BootstrapConfig = {}): Promise<BootstrapContext> {
|
|
57
|
-
|
|
58
|
+
let agentConfig = loadConfig();
|
|
58
59
|
if (!agentConfig) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
60
|
+
// Auto-init for first-time OpenClaw plugin activation
|
|
61
|
+
const result = spawnSync('agentbnb', ['init', '--yes', '--no-detect'], {
|
|
62
|
+
stdio: 'pipe',
|
|
63
|
+
env: { ...process.env },
|
|
64
|
+
encoding: 'utf-8',
|
|
65
|
+
});
|
|
66
|
+
if (result.error || result.status !== 0) {
|
|
67
|
+
const msg = result.error?.message ?? (result.stderr as string | null)?.trim() ?? 'agentbnb init failed';
|
|
68
|
+
throw new AgentBnBError(`Auto-init failed: ${msg}`, 'INIT_FAILED');
|
|
69
|
+
}
|
|
70
|
+
agentConfig = loadConfig();
|
|
71
|
+
if (!agentConfig) {
|
|
72
|
+
throw new AgentBnBError('AgentBnB config still not found after auto-init', 'CONFIG_NOT_FOUND');
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
const guard = new ProcessGuard(join(homedir(), '.agentbnb', '.pid'));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentbnb/skill-deep-stock-analyst",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Deep quantitative stock analysis: 12-endpoint Alpha Vantage pipeline + 5 analysis modules + Gemini thesis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=tests/",
|
|
11
|
+
"dev": "tsx src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@google/generative-ai": "^0.21.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.0.0",
|
|
18
|
+
"typescript": "^5.5.0",
|
|
19
|
+
"tsx": "^4.19.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AVOverview,
|
|
3
|
+
AVIncomeStatement,
|
|
4
|
+
AVBalanceSheet,
|
|
5
|
+
AVCashFlow,
|
|
6
|
+
AVEarnings,
|
|
7
|
+
} from '../api/types.js';
|
|
8
|
+
import {
|
|
9
|
+
scoreMetric,
|
|
10
|
+
scoreMetricInverse,
|
|
11
|
+
weightedAvg,
|
|
12
|
+
calcYoYGrowth,
|
|
13
|
+
scoreGrowth,
|
|
14
|
+
scoreSurprise,
|
|
15
|
+
countConsecutiveBeats,
|
|
16
|
+
sp,
|
|
17
|
+
} from './utils.js';
|
|
18
|
+
|
|
19
|
+
export interface FinancialHealth {
|
|
20
|
+
profitability_score: number;
|
|
21
|
+
growth_score: number;
|
|
22
|
+
leverage_score: number;
|
|
23
|
+
efficiency_score: number;
|
|
24
|
+
composite: number;
|
|
25
|
+
red_flags: string[];
|
|
26
|
+
green_flags: string[];
|
|
27
|
+
raw: {
|
|
28
|
+
grossMarginPct: number;
|
|
29
|
+
operatingMarginPct: number;
|
|
30
|
+
netMarginPct: number;
|
|
31
|
+
roe: number;
|
|
32
|
+
debtToEquity: number;
|
|
33
|
+
currentRatio: number;
|
|
34
|
+
revenueGrowthPct: number;
|
|
35
|
+
earningsGrowthPct: number;
|
|
36
|
+
lastSurpisePct: number;
|
|
37
|
+
consecutiveBeats: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function calcInterestCoverage(report: AVIncomeStatement['annualReports'][number] | undefined): number {
|
|
42
|
+
if (!report) return 5;
|
|
43
|
+
const ebit = sp(report.ebit);
|
|
44
|
+
const interest = sp(report.interestExpense);
|
|
45
|
+
if (interest === 0) return 10; // no debt
|
|
46
|
+
return Math.abs(ebit / interest);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function calcROIC(
|
|
50
|
+
income: AVIncomeStatement['annualReports'][number] | undefined,
|
|
51
|
+
balance: AVBalanceSheet['annualReports'][number] | undefined,
|
|
52
|
+
): number {
|
|
53
|
+
if (!income || !balance) return 0;
|
|
54
|
+
const nopat = sp(income.ebit) * 0.79; // rough 21% tax
|
|
55
|
+
const investedCapital =
|
|
56
|
+
sp(balance.totalShareholderEquity) + sp(balance.longTermDebt) + sp(balance.shortTermDebt);
|
|
57
|
+
if (investedCapital === 0) return 0;
|
|
58
|
+
return nopat / investedCapital;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function analyzeFinancialHealth(
|
|
62
|
+
overview: AVOverview,
|
|
63
|
+
income: AVIncomeStatement,
|
|
64
|
+
balance: AVBalanceSheet,
|
|
65
|
+
cashflow: AVCashFlow,
|
|
66
|
+
earnings: AVEarnings,
|
|
67
|
+
): FinancialHealth {
|
|
68
|
+
const red_flags: string[] = [];
|
|
69
|
+
const green_flags: string[] = [];
|
|
70
|
+
|
|
71
|
+
// === Profitability ===
|
|
72
|
+
const grossProfit = sp(overview.GrossProfitTTM);
|
|
73
|
+
const revenue = sp(overview.RevenueTTM);
|
|
74
|
+
const grossMarginPct = revenue > 0 ? (grossProfit / revenue) * 100 : 0;
|
|
75
|
+
const operatingMarginPct = sp(overview.OperatingMarginTTM) * 100;
|
|
76
|
+
const netMarginPct = sp(overview.ProfitMargin) * 100;
|
|
77
|
+
const roe = sp(overview.ReturnOnEquityTTM) * 100;
|
|
78
|
+
|
|
79
|
+
if (grossMarginPct > 60) green_flags.push(`Gross margin ${grossMarginPct.toFixed(1)}% — strong pricing power`);
|
|
80
|
+
if (netMarginPct < 0) red_flags.push(`Net margin negative at ${netMarginPct.toFixed(1)}%`);
|
|
81
|
+
if (roe > 20) green_flags.push(`ROE ${roe.toFixed(1)}% — excellent capital efficiency`);
|
|
82
|
+
if (roe < 0) red_flags.push(`ROE negative at ${roe.toFixed(1)}% — destroying shareholder value`);
|
|
83
|
+
|
|
84
|
+
const profitability_score = weightedAvg([
|
|
85
|
+
[scoreMetricInverse(grossMarginPct, { excellent: 60, good: 40, fair: 25, poor: 10 }), 0.30],
|
|
86
|
+
[scoreMetricInverse(operatingMarginPct, { excellent: 25, good: 15, fair: 8, poor: 0 }), 0.35],
|
|
87
|
+
[scoreMetricInverse(roe, { excellent: 25, good: 15, fair: 8, poor: 0 }), 0.35],
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// === Growth ===
|
|
91
|
+
const revenueGrowthPct = calcYoYGrowth(income.quarterlyReports, 'totalRevenue');
|
|
92
|
+
const earningsGrowthPct = calcYoYGrowth(income.quarterlyReports, 'netIncome');
|
|
93
|
+
const fcfGrowthPct = calcYoYGrowth(cashflow.quarterlyReports, 'operatingCashflow');
|
|
94
|
+
|
|
95
|
+
const lastEarnings = earnings.quarterlyEarnings[0];
|
|
96
|
+
const lastSurpisePct = sp(lastEarnings?.surprisePercentage);
|
|
97
|
+
if (lastSurpisePct > 10) green_flags.push(`Last earnings beat estimates by ${lastSurpisePct.toFixed(1)}%`);
|
|
98
|
+
if (lastSurpisePct < -10) red_flags.push(`Last earnings missed estimates by ${Math.abs(lastSurpisePct).toFixed(1)}%`);
|
|
99
|
+
|
|
100
|
+
const consecutiveBeats = countConsecutiveBeats(earnings.quarterlyEarnings);
|
|
101
|
+
if (consecutiveBeats >= 4) green_flags.push(`${consecutiveBeats} consecutive quarters of earnings beats`);
|
|
102
|
+
if (revenueGrowthPct > 20) green_flags.push(`Revenue growing ${revenueGrowthPct.toFixed(1)}% YoY`);
|
|
103
|
+
if (revenueGrowthPct < -10) red_flags.push(`Revenue declining ${Math.abs(revenueGrowthPct).toFixed(1)}% YoY`);
|
|
104
|
+
|
|
105
|
+
const growth_score = weightedAvg([
|
|
106
|
+
[scoreGrowth(revenueGrowthPct), 0.35],
|
|
107
|
+
[scoreGrowth(earningsGrowthPct), 0.35],
|
|
108
|
+
[scoreGrowth(fcfGrowthPct), 0.20],
|
|
109
|
+
[scoreSurprise(lastSurpisePct), 0.10],
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
// === Leverage ===
|
|
113
|
+
const debtToEquity = sp(overview.DebtToEquity);
|
|
114
|
+
const currentRatio = sp(overview.CurrentRatio);
|
|
115
|
+
const interestCoverage = calcInterestCoverage(income.annualReports[0]);
|
|
116
|
+
|
|
117
|
+
if (debtToEquity > 2.0) red_flags.push(`Debt/Equity ${debtToEquity.toFixed(2)} — heavily leveraged`);
|
|
118
|
+
if (currentRatio < 1.0) red_flags.push(`Current ratio ${currentRatio.toFixed(2)} — potential liquidity risk`);
|
|
119
|
+
if (currentRatio > 2.0) green_flags.push(`Current ratio ${currentRatio.toFixed(2)} — strong liquidity`);
|
|
120
|
+
if (interestCoverage < 2.0) red_flags.push(`Interest coverage ${interestCoverage.toFixed(1)}x — thin margin`);
|
|
121
|
+
if (debtToEquity < 0.3) green_flags.push(`Low leverage: Debt/Equity ${debtToEquity.toFixed(2)}`);
|
|
122
|
+
|
|
123
|
+
const leverage_score = weightedAvg([
|
|
124
|
+
[scoreMetric(debtToEquity, { excellent: 0.3, good: 0.8, fair: 1.5, poor: 3.0 }), 0.40],
|
|
125
|
+
[scoreMetricInverse(currentRatio, { excellent: 2.5, good: 1.5, fair: 1.0, poor: 0.5 }), 0.30],
|
|
126
|
+
[scoreMetricInverse(interestCoverage, { excellent: 10, good: 5, fair: 2, poor: 1 }), 0.30],
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
// === Efficiency ===
|
|
130
|
+
const totalAssets = sp(balance.annualReports[0]?.totalAssets);
|
|
131
|
+
const assetTurnover = totalAssets > 0 ? revenue / totalAssets : 0;
|
|
132
|
+
const roic = calcROIC(income.annualReports[0], balance.annualReports[0]);
|
|
133
|
+
|
|
134
|
+
const efficiency_score = weightedAvg([
|
|
135
|
+
[scoreMetricInverse(assetTurnover, { excellent: 1.5, good: 1.0, fair: 0.5, poor: 0.2 }), 0.50],
|
|
136
|
+
[scoreMetricInverse(roic * 100, { excellent: 20, good: 12, fair: 7, poor: 0 }), 0.50],
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const composite = weightedAvg([
|
|
140
|
+
[profitability_score, 0.30],
|
|
141
|
+
[growth_score, 0.30],
|
|
142
|
+
[leverage_score, 0.20],
|
|
143
|
+
[efficiency_score, 0.20],
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
profitability_score,
|
|
148
|
+
growth_score,
|
|
149
|
+
leverage_score,
|
|
150
|
+
efficiency_score,
|
|
151
|
+
composite,
|
|
152
|
+
red_flags,
|
|
153
|
+
green_flags,
|
|
154
|
+
raw: {
|
|
155
|
+
grossMarginPct,
|
|
156
|
+
operatingMarginPct,
|
|
157
|
+
netMarginPct,
|
|
158
|
+
roe,
|
|
159
|
+
debtToEquity,
|
|
160
|
+
currentRatio,
|
|
161
|
+
revenueGrowthPct,
|
|
162
|
+
earningsGrowthPct,
|
|
163
|
+
lastSurpisePct,
|
|
164
|
+
consecutiveBeats,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AVNewsSentiment } from '../api/types.js';
|
|
2
|
+
import { mapRange, sp } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export interface SentimentScore {
|
|
5
|
+
news_sentiment: number; // -1 to +1
|
|
6
|
+
news_volume: number; // relevant articles in feed
|
|
7
|
+
bullish_ratio: number; // 0–1
|
|
8
|
+
topic_breakdown: Record<string, number>; // topic → avg relevance
|
|
9
|
+
key_headlines: string[]; // top 5 by sentiment magnitude
|
|
10
|
+
composite: number; // 0–100
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function analyzeSentiment(newsData: AVNewsSentiment, ticker: string): SentimentScore {
|
|
14
|
+
const articles = newsData.feed ?? [];
|
|
15
|
+
const t = ticker.toUpperCase();
|
|
16
|
+
|
|
17
|
+
// Filter to articles where this ticker has relevance_score > 0.5
|
|
18
|
+
const relevant = articles.filter((a) => {
|
|
19
|
+
const ts = a.ticker_sentiment?.find(
|
|
20
|
+
(s) => s.ticker === t && sp(s.relevance_score) > 0.5,
|
|
21
|
+
);
|
|
22
|
+
return !!ts;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let totalSentiment = 0;
|
|
26
|
+
let bullishCount = 0;
|
|
27
|
+
|
|
28
|
+
for (const article of relevant) {
|
|
29
|
+
const ts = article.ticker_sentiment.find((s) => s.ticker === t);
|
|
30
|
+
if (!ts) continue;
|
|
31
|
+
const score = sp(ts.ticker_sentiment_score);
|
|
32
|
+
totalSentiment += score;
|
|
33
|
+
if (score > 0.15) bullishCount++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const news_sentiment = relevant.length > 0 ? totalSentiment / relevant.length : 0;
|
|
37
|
+
const bullish_ratio = relevant.length > 0 ? bullishCount / relevant.length : 0.5;
|
|
38
|
+
|
|
39
|
+
// Topic breakdown: topic → avg relevance across articles
|
|
40
|
+
const topicMap: Record<string, number[]> = {};
|
|
41
|
+
for (const article of relevant) {
|
|
42
|
+
for (const topic of article.topics ?? []) {
|
|
43
|
+
if (!topicMap[topic.topic]) topicMap[topic.topic] = [];
|
|
44
|
+
topicMap[topic.topic]!.push(sp(topic.relevance_score));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const topic_breakdown: Record<string, number> = {};
|
|
48
|
+
for (const [topic, scores] of Object.entries(topicMap)) {
|
|
49
|
+
topic_breakdown[topic] = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Top 5 headlines sorted by overall sentiment magnitude
|
|
53
|
+
const key_headlines = relevant
|
|
54
|
+
.sort((a, b) =>
|
|
55
|
+
Math.abs(sp(b.overall_sentiment_score)) - Math.abs(sp(a.overall_sentiment_score)),
|
|
56
|
+
)
|
|
57
|
+
.slice(0, 5)
|
|
58
|
+
.map((a) => `[${a.overall_sentiment_label}] ${a.title}`);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
news_sentiment,
|
|
62
|
+
news_volume: relevant.length,
|
|
63
|
+
bullish_ratio,
|
|
64
|
+
topic_breakdown,
|
|
65
|
+
key_headlines,
|
|
66
|
+
composite: mapRange(news_sentiment, -1, 1, 0, 100),
|
|
67
|
+
};
|
|
68
|
+
}
|