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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { AVOverview } from '../api/types.js';
|
|
2
|
+
import { scoreMetric, scoreMetricInverse, weightedAvg, sp } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export interface ValuationScore {
|
|
5
|
+
pe_score: number;
|
|
6
|
+
ps_score: number;
|
|
7
|
+
peg_score: number;
|
|
8
|
+
fcf_yield_score: number;
|
|
9
|
+
ev_ebitda_score: number;
|
|
10
|
+
composite: number;
|
|
11
|
+
verdict: 'undervalued' | 'fair' | 'overvalued' | 'expensive';
|
|
12
|
+
raw: {
|
|
13
|
+
pe: number;
|
|
14
|
+
forwardPE: number;
|
|
15
|
+
peg: number;
|
|
16
|
+
ps: number;
|
|
17
|
+
evEbitda: number;
|
|
18
|
+
fcfYieldPct: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function calculateValuation(overview: AVOverview): ValuationScore {
|
|
23
|
+
const pe = sp(overview.PERatio);
|
|
24
|
+
const forwardPE = sp(overview.ForwardPE);
|
|
25
|
+
const peg = sp(overview.PEGRatio);
|
|
26
|
+
const ps = sp(overview.PriceToSalesRatioTTM);
|
|
27
|
+
const evEbitda = sp(overview.EVToEBITDA);
|
|
28
|
+
const marketCap = sp(overview.MarketCapitalization);
|
|
29
|
+
const ocf = sp(overview.OperatingCashflowTTM);
|
|
30
|
+
|
|
31
|
+
// FCF Yield = Operating Cash Flow / Market Cap (as percentage)
|
|
32
|
+
const fcfYieldPct = marketCap > 0 ? (ocf / marketCap) * 100 : 0;
|
|
33
|
+
|
|
34
|
+
// Score each metric: 100 = very cheap, 0 = very expensive
|
|
35
|
+
// Thresholds calibrated to S&P 500 historical medians
|
|
36
|
+
const pe_score = scoreMetric(pe > 0 ? pe : forwardPE, {
|
|
37
|
+
excellent: 12,
|
|
38
|
+
good: 18,
|
|
39
|
+
fair: 25,
|
|
40
|
+
poor: 40,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const peg_score = scoreMetric(peg, {
|
|
44
|
+
excellent: 0.5,
|
|
45
|
+
good: 1.0,
|
|
46
|
+
fair: 1.5,
|
|
47
|
+
poor: 2.5,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const fcf_yield_score = scoreMetricInverse(fcfYieldPct, {
|
|
51
|
+
excellent: 8,
|
|
52
|
+
good: 5,
|
|
53
|
+
fair: 3,
|
|
54
|
+
poor: 1,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const ev_ebitda_score = scoreMetric(evEbitda, {
|
|
58
|
+
excellent: 8,
|
|
59
|
+
good: 12,
|
|
60
|
+
fair: 18,
|
|
61
|
+
poor: 30,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const ps_score = scoreMetric(ps, {
|
|
65
|
+
excellent: 1,
|
|
66
|
+
good: 3,
|
|
67
|
+
fair: 6,
|
|
68
|
+
poor: 12,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const composite = weightedAvg([
|
|
72
|
+
[pe_score, 0.25],
|
|
73
|
+
[peg_score, 0.20],
|
|
74
|
+
[fcf_yield_score, 0.25],
|
|
75
|
+
[ev_ebitda_score, 0.15],
|
|
76
|
+
[ps_score, 0.15],
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const verdict: ValuationScore['verdict'] =
|
|
80
|
+
composite > 70 ? 'undervalued'
|
|
81
|
+
: composite > 50 ? 'fair'
|
|
82
|
+
: composite > 30 ? 'overvalued'
|
|
83
|
+
: 'expensive';
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
pe_score,
|
|
87
|
+
ps_score,
|
|
88
|
+
peg_score,
|
|
89
|
+
fcf_yield_score,
|
|
90
|
+
ev_ebitda_score,
|
|
91
|
+
composite,
|
|
92
|
+
verdict,
|
|
93
|
+
raw: { pe, forwardPE, peg, ps, evEbitda, fcfYieldPct },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alpha Vantage API client.
|
|
3
|
+
* Free key: 25 req/day, 5 req/min → serialize calls with 200ms gap.
|
|
4
|
+
* Standard analysis = 12 calls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AllAVData,
|
|
9
|
+
AVDailyPrice,
|
|
10
|
+
AVRSIEntry,
|
|
11
|
+
AVMACDEntry,
|
|
12
|
+
AVBBandsEntry,
|
|
13
|
+
AVStochEntry,
|
|
14
|
+
AVADXEntry,
|
|
15
|
+
AVOverview,
|
|
16
|
+
AVIncomeStatement,
|
|
17
|
+
AVBalanceSheet,
|
|
18
|
+
AVCashFlow,
|
|
19
|
+
AVEarnings,
|
|
20
|
+
AVNewsSentiment,
|
|
21
|
+
} from './types.js';
|
|
22
|
+
|
|
23
|
+
const BASE_URL = 'https://www.alphavantage.co/query';
|
|
24
|
+
const RATE_DELAY_MS = 200; // 5 req/min on free key → be conservative
|
|
25
|
+
|
|
26
|
+
async function sleep(ms: number): Promise<void> {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function avFetch(params: Record<string, string>, apiKey: string): Promise<unknown> {
|
|
31
|
+
const url = new URL(BASE_URL);
|
|
32
|
+
url.searchParams.set('apikey', apiKey);
|
|
33
|
+
for (const [k, v] of Object.entries(params)) {
|
|
34
|
+
url.searchParams.set(k, v);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const res = await fetch(url.toString());
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
throw new Error(`Alpha Vantage HTTP ${res.status}: ${url.searchParams.get('function')}`);
|
|
40
|
+
}
|
|
41
|
+
const data = await res.json() as Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
// AV returns error messages as JSON object with "Information" or "Note" keys
|
|
44
|
+
if (typeof data['Information'] === 'string' && data['Information'].includes('rate limit')) {
|
|
45
|
+
throw new Error(`Alpha Vantage rate limit hit: ${data['Information']}`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof data['Note'] === 'string') {
|
|
48
|
+
throw new Error(`Alpha Vantage note (likely rate limited): ${data['Note']}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Parse TIME_SERIES_DAILY_ADJUSTED into flat array sorted newest-first */
|
|
55
|
+
function parseDailySeries(raw: unknown): AVDailyPrice[] {
|
|
56
|
+
const data = raw as Record<string, unknown>;
|
|
57
|
+
const series = data['Time Series (Daily)'] as Record<string, Record<string, string>> | undefined;
|
|
58
|
+
if (!series) return [];
|
|
59
|
+
|
|
60
|
+
return Object.entries(series)
|
|
61
|
+
.map(([date, v]) => ({
|
|
62
|
+
date,
|
|
63
|
+
open: v['1. open'] ?? '0',
|
|
64
|
+
high: v['2. high'] ?? '0',
|
|
65
|
+
low: v['3. low'] ?? '0',
|
|
66
|
+
close: v['4. close'] ?? '0',
|
|
67
|
+
adjustedClose: v['5. adjusted close'] ?? '0',
|
|
68
|
+
volume: v['6. volume'] ?? '0',
|
|
69
|
+
dividendAmount: v['7. dividend amount'] ?? '0',
|
|
70
|
+
splitCoefficient: v['8. split coefficient'] ?? '1',
|
|
71
|
+
}))
|
|
72
|
+
.sort((a, b) => b.date.localeCompare(a.date)); // newest first
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Parse a technical indicator response into flat array sorted newest-first */
|
|
76
|
+
function parseIndicator<T>(raw: unknown, key: string): T[] {
|
|
77
|
+
const data = raw as Record<string, unknown>;
|
|
78
|
+
const series = data[key] as Record<string, T> | undefined;
|
|
79
|
+
if (!series) return [];
|
|
80
|
+
return Object.entries(series)
|
|
81
|
+
.sort(([a], [b]) => b.localeCompare(a)) // newest first
|
|
82
|
+
.map(([, v]) => v);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetch all 12 Alpha Vantage endpoints for a standard analysis.
|
|
87
|
+
* Calls are serialized with a short delay to avoid rate limits.
|
|
88
|
+
*/
|
|
89
|
+
export async function fetchAllData(ticker: string, apiKey: string): Promise<AllAVData> {
|
|
90
|
+
const t = ticker.toUpperCase();
|
|
91
|
+
const results: unknown[] = [];
|
|
92
|
+
|
|
93
|
+
const calls: Array<Record<string, string>> = [
|
|
94
|
+
{ function: 'OVERVIEW', symbol: t },
|
|
95
|
+
{ function: 'INCOME_STATEMENT', symbol: t },
|
|
96
|
+
{ function: 'BALANCE_SHEET', symbol: t },
|
|
97
|
+
{ function: 'CASH_FLOW', symbol: t },
|
|
98
|
+
{ function: 'EARNINGS', symbol: t },
|
|
99
|
+
{ function: 'TIME_SERIES_DAILY_ADJUSTED', symbol: t, outputsize: 'full' },
|
|
100
|
+
{ function: 'RSI', symbol: t, interval: 'daily', time_period: '14', series_type: 'close' },
|
|
101
|
+
{ function: 'MACD', symbol: t, interval: 'daily', series_type: 'close' },
|
|
102
|
+
{ function: 'BBANDS', symbol: t, interval: 'daily', time_period: '20', series_type: 'close' },
|
|
103
|
+
{ function: 'STOCH', symbol: t, interval: 'daily' },
|
|
104
|
+
{ function: 'ADX', symbol: t, interval: 'daily', time_period: '14' },
|
|
105
|
+
{ function: 'NEWS_SENTIMENT', tickers: t, limit: '50', sort: 'LATEST' },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < calls.length; i++) {
|
|
109
|
+
const call = calls[i];
|
|
110
|
+
if (i > 0) await sleep(RATE_DELAY_MS);
|
|
111
|
+
results.push(await avFetch(call!, apiKey));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const [
|
|
115
|
+
overview, income, balance, cashflow, earnings,
|
|
116
|
+
dailyRaw, rsiRaw, macdRaw, bbandsRaw, stochRaw, adxRaw, newsRaw,
|
|
117
|
+
] = results;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
overview: overview as AVOverview,
|
|
121
|
+
income: income as AVIncomeStatement,
|
|
122
|
+
balance: balance as AVBalanceSheet,
|
|
123
|
+
cashflow: cashflow as AVCashFlow,
|
|
124
|
+
earnings: earnings as AVEarnings,
|
|
125
|
+
daily: parseDailySeries(dailyRaw),
|
|
126
|
+
rsi: parseIndicator<AVRSIEntry>(rsiRaw, 'Technical Analysis: RSI'),
|
|
127
|
+
macd: parseIndicator<AVMACDEntry>(macdRaw, 'Technical Analysis: MACD'),
|
|
128
|
+
bbands: parseIndicator<AVBBandsEntry>(bbandsRaw, 'Technical Analysis: BBANDS'),
|
|
129
|
+
stoch: parseIndicator<AVStochEntry>(stochRaw, 'Technical Analysis: STOCH'),
|
|
130
|
+
adx: parseIndicator<AVADXEntry>(adxRaw, 'Technical Analysis: ADX'),
|
|
131
|
+
news: newsRaw as AVNewsSentiment,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/** Alpha Vantage API response types */
|
|
2
|
+
|
|
3
|
+
export interface AVOverview {
|
|
4
|
+
Symbol: string;
|
|
5
|
+
Name: string;
|
|
6
|
+
Description: string;
|
|
7
|
+
Sector: string;
|
|
8
|
+
Industry: string;
|
|
9
|
+
MarketCapitalization: string;
|
|
10
|
+
PERatio: string;
|
|
11
|
+
ForwardPE: string;
|
|
12
|
+
PEGRatio: string;
|
|
13
|
+
PriceToSalesRatioTTM: string;
|
|
14
|
+
PriceToBookRatio: string;
|
|
15
|
+
EVToEBITDA: string;
|
|
16
|
+
EVToRevenue: string;
|
|
17
|
+
GrossProfitTTM: string;
|
|
18
|
+
RevenueTTM: string;
|
|
19
|
+
OperatingMarginTTM: string;
|
|
20
|
+
ProfitMargin: string;
|
|
21
|
+
ReturnOnEquityTTM: string;
|
|
22
|
+
ReturnOnAssetsTTM: string;
|
|
23
|
+
DebtToEquity: string;
|
|
24
|
+
CurrentRatio: string;
|
|
25
|
+
QuickRatio: string;
|
|
26
|
+
OperatingCashflowTTM: string;
|
|
27
|
+
RevenuePerShareTTM: string;
|
|
28
|
+
EPS: string;
|
|
29
|
+
DilutedEPSTTM: string;
|
|
30
|
+
Beta: string;
|
|
31
|
+
'52WeekHigh': string;
|
|
32
|
+
'52WeekLow': string;
|
|
33
|
+
'50DayMovingAverage': string;
|
|
34
|
+
'200DayMovingAverage': string;
|
|
35
|
+
SharesOutstanding: string;
|
|
36
|
+
DividendYield: string;
|
|
37
|
+
ExDividendDate: string;
|
|
38
|
+
AnalystTargetPrice: string;
|
|
39
|
+
AnalystRatingStrongBuy: string;
|
|
40
|
+
AnalystRatingBuy: string;
|
|
41
|
+
AnalystRatingHold: string;
|
|
42
|
+
AnalystRatingSell: string;
|
|
43
|
+
AnalystRatingStrongSell: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AVQuarterlyReport {
|
|
47
|
+
fiscalDateEnding: string;
|
|
48
|
+
reportedCurrency: string;
|
|
49
|
+
totalRevenue: string;
|
|
50
|
+
netIncome: string;
|
|
51
|
+
grossProfit: string;
|
|
52
|
+
ebit: string;
|
|
53
|
+
ebitda: string;
|
|
54
|
+
operatingIncome: string;
|
|
55
|
+
interestExpense: string;
|
|
56
|
+
researchAndDevelopment: string;
|
|
57
|
+
sellingGeneralAndAdministrative: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AVAnnualReport {
|
|
61
|
+
fiscalDateEnding: string;
|
|
62
|
+
reportedCurrency: string;
|
|
63
|
+
totalRevenue: string;
|
|
64
|
+
netIncome: string;
|
|
65
|
+
grossProfit: string;
|
|
66
|
+
ebit: string;
|
|
67
|
+
ebitda: string;
|
|
68
|
+
operatingIncome: string;
|
|
69
|
+
interestExpense: string;
|
|
70
|
+
researchAndDevelopment: string;
|
|
71
|
+
sellingGeneralAndAdministrative: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AVIncomeStatement {
|
|
75
|
+
symbol: string;
|
|
76
|
+
annualReports: AVAnnualReport[];
|
|
77
|
+
quarterlyReports: AVQuarterlyReport[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface AVBalanceSheetReport {
|
|
81
|
+
fiscalDateEnding: string;
|
|
82
|
+
reportedCurrency: string;
|
|
83
|
+
totalAssets: string;
|
|
84
|
+
totalCurrentAssets: string;
|
|
85
|
+
totalNonCurrentAssets: string;
|
|
86
|
+
totalLiabilities: string;
|
|
87
|
+
totalCurrentLiabilities: string;
|
|
88
|
+
totalNonCurrentLiabilities: string;
|
|
89
|
+
totalShareholderEquity: string;
|
|
90
|
+
longTermDebt: string;
|
|
91
|
+
shortTermDebt: string;
|
|
92
|
+
cashAndCashEquivalentsAtCarryingValue: string;
|
|
93
|
+
currentNetReceivables: string;
|
|
94
|
+
inventory: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface AVBalanceSheet {
|
|
98
|
+
symbol: string;
|
|
99
|
+
annualReports: AVBalanceSheetReport[];
|
|
100
|
+
quarterlyReports: AVBalanceSheetReport[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface AVCashFlowReport {
|
|
104
|
+
fiscalDateEnding: string;
|
|
105
|
+
reportedCurrency: string;
|
|
106
|
+
operatingCashflow: string;
|
|
107
|
+
capitalExpenditures: string;
|
|
108
|
+
cashflowFromInvestment: string;
|
|
109
|
+
cashflowFromFinancing: string;
|
|
110
|
+
netIncome: string;
|
|
111
|
+
dividendPayout: string;
|
|
112
|
+
changeInOperatingAssets: string;
|
|
113
|
+
changeInOperatingLiabilities: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface AVCashFlow {
|
|
117
|
+
symbol: string;
|
|
118
|
+
annualReports: AVCashFlowReport[];
|
|
119
|
+
quarterlyReports: AVCashFlowReport[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface AVQuarterlyEarning {
|
|
123
|
+
fiscalDateEnding: string;
|
|
124
|
+
reportedDate: string;
|
|
125
|
+
reportedEPS: string;
|
|
126
|
+
estimatedEPS: string;
|
|
127
|
+
surprise: string;
|
|
128
|
+
surprisePercentage: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface AVEarnings {
|
|
132
|
+
symbol: string;
|
|
133
|
+
quarterlyEarnings: AVQuarterlyEarning[];
|
|
134
|
+
annualEarnings: Array<{ fiscalDateEnding: string; reportedEPS: string }>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface AVDailyPrice {
|
|
138
|
+
date: string;
|
|
139
|
+
open: string;
|
|
140
|
+
high: string;
|
|
141
|
+
low: string;
|
|
142
|
+
close: string;
|
|
143
|
+
adjustedClose: string;
|
|
144
|
+
volume: string;
|
|
145
|
+
dividendAmount: string;
|
|
146
|
+
splitCoefficient: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface AVDailyTimeSeries {
|
|
150
|
+
'Meta Data': {
|
|
151
|
+
'1. Information': string;
|
|
152
|
+
'2. Symbol': string;
|
|
153
|
+
'3. Last Refreshed': string;
|
|
154
|
+
'4. Output Size': string;
|
|
155
|
+
'5. Time Zone': string;
|
|
156
|
+
};
|
|
157
|
+
'Time Series (Daily)': Record<string, {
|
|
158
|
+
'1. open': string;
|
|
159
|
+
'2. high': string;
|
|
160
|
+
'3. low': string;
|
|
161
|
+
'4. close': string;
|
|
162
|
+
'5. adjusted close': string;
|
|
163
|
+
'6. volume': string;
|
|
164
|
+
'7. dividend amount': string;
|
|
165
|
+
'8. split coefficient': string;
|
|
166
|
+
}>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface AVRSIEntry {
|
|
170
|
+
RSI: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface AVMACDEntry {
|
|
174
|
+
MACD: string;
|
|
175
|
+
MACD_Hist: string;
|
|
176
|
+
MACD_Signal: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface AVBBandsEntry {
|
|
180
|
+
'Real Upper Band': string;
|
|
181
|
+
'Real Middle Band': string;
|
|
182
|
+
'Real Lower Band': string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface AVStochEntry {
|
|
186
|
+
SlowK: string;
|
|
187
|
+
SlowD: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface AVADXEntry {
|
|
191
|
+
ADX: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface AVIndicatorResponse<T> {
|
|
195
|
+
'Meta Data': Record<string, string>;
|
|
196
|
+
'Technical Analysis: RSI'?: Record<string, T>;
|
|
197
|
+
'Technical Analysis: MACD'?: Record<string, T>;
|
|
198
|
+
'Technical Analysis: BBANDS'?: Record<string, T>;
|
|
199
|
+
'Technical Analysis: STOCH'?: Record<string, T>;
|
|
200
|
+
'Technical Analysis: ADX'?: Record<string, T>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface AVNewsTickerSentiment {
|
|
204
|
+
ticker: string;
|
|
205
|
+
relevance_score: string;
|
|
206
|
+
ticker_sentiment_score: string;
|
|
207
|
+
ticker_sentiment_label: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface AVNewsArticle {
|
|
211
|
+
title: string;
|
|
212
|
+
url: string;
|
|
213
|
+
time_published: string;
|
|
214
|
+
summary: string;
|
|
215
|
+
overall_sentiment_score: string;
|
|
216
|
+
overall_sentiment_label: string;
|
|
217
|
+
ticker_sentiment: AVNewsTickerSentiment[];
|
|
218
|
+
topics: Array<{ topic: string; relevance_score: string }>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface AVNewsSentiment {
|
|
222
|
+
feed: AVNewsArticle[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface AllAVData {
|
|
226
|
+
overview: AVOverview;
|
|
227
|
+
income: AVIncomeStatement;
|
|
228
|
+
balance: AVBalanceSheet;
|
|
229
|
+
cashflow: AVCashFlow;
|
|
230
|
+
earnings: AVEarnings;
|
|
231
|
+
daily: AVDailyPrice[];
|
|
232
|
+
rsi: AVRSIEntry[];
|
|
233
|
+
macd: AVMACDEntry[];
|
|
234
|
+
bbands: AVBBandsEntry[];
|
|
235
|
+
stoch: AVStochEntry[];
|
|
236
|
+
adx: AVADXEntry[];
|
|
237
|
+
news: AVNewsSentiment;
|
|
238
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for deep-stock-analyst.
|
|
4
|
+
* Usage: node dist/index.js --ticker IBM --depth standard --style hybrid
|
|
5
|
+
* Output: JSON to stdout (genesis-bot reads this)
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 = success, JSON on stdout
|
|
9
|
+
* 1 = invalid args or API error
|
|
10
|
+
* 2 = rate limit / daily limit reached
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { runAnalysis } from './orchestrator.js';
|
|
14
|
+
import type { InvestmentStyle } from './analysis/signal.js';
|
|
15
|
+
|
|
16
|
+
function parseArgs(): { ticker: string; depth: string; style: InvestmentStyle } | null {
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const get = (flag: string): string | undefined => {
|
|
19
|
+
const i = args.indexOf(flag);
|
|
20
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ticker = get('--ticker');
|
|
24
|
+
const depth = get('--depth') ?? 'standard';
|
|
25
|
+
const style = (get('--style') ?? 'hybrid') as InvestmentStyle;
|
|
26
|
+
|
|
27
|
+
if (!ticker) {
|
|
28
|
+
console.error('Usage: node dist/index.js --ticker <TICKER> [--depth quick|standard|deep] [--style growth|value|momentum|hybrid]');
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const validDepths = ['quick', 'standard', 'deep'];
|
|
33
|
+
const validStyles = ['growth', 'value', 'momentum', 'hybrid'];
|
|
34
|
+
|
|
35
|
+
if (!validDepths.includes(depth)) {
|
|
36
|
+
console.error(`Invalid depth: ${depth}. Must be one of: ${validDepths.join(', ')}`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!validStyles.includes(style)) {
|
|
40
|
+
console.error(`Invalid style: ${style}. Must be one of: ${validStyles.join(', ')}`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ticker, depth, style };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main(): Promise<void> {
|
|
48
|
+
const args = parseArgs();
|
|
49
|
+
if (!args) {
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const apiKey = process.env['ALPHA_VANTAGE_API_KEY'];
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
console.error('Missing ALPHA_VANTAGE_API_KEY environment variable');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = await runAnalysis({
|
|
61
|
+
ticker: args.ticker,
|
|
62
|
+
depth: args.depth as 'quick' | 'standard' | 'deep',
|
|
63
|
+
style: args.style,
|
|
64
|
+
apiKey,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Output clean JSON to stdout (genesis-bot reads this)
|
|
68
|
+
process.stdout.write(JSON.stringify(result, null, 2));
|
|
69
|
+
process.stdout.write('\n');
|
|
70
|
+
process.exit(0);
|
|
71
|
+
} catch (err: unknown) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
|
|
74
|
+
if (msg.includes('rate limit')) {
|
|
75
|
+
console.error(`[rate-limit] ${msg}`);
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.error(`[error] ${msg}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thesis generation via Gemini Flash.
|
|
3
|
+
* Uses genesis-bot's existing GOOGLE_API_KEY.
|
|
4
|
+
* Input: pre-computed CompositeSignal + raw data → structured thesis JSON.
|
|
5
|
+
* LLM interprets numbers — does NOT recalculate anything.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
9
|
+
import type { ValuationScore } from '../analysis/valuation.js';
|
|
10
|
+
import type { TechnicalScore } from '../analysis/technicals.js';
|
|
11
|
+
import type { FinancialHealth } from '../analysis/financial-health.js';
|
|
12
|
+
import type { SentimentScore } from '../analysis/sentiment.js';
|
|
13
|
+
import type { CompositeSignal } from '../analysis/signal.js';
|
|
14
|
+
import type { AVOverview } from '../api/types.js';
|
|
15
|
+
|
|
16
|
+
export interface InvestmentThesis {
|
|
17
|
+
bull_case: string;
|
|
18
|
+
bear_case: string;
|
|
19
|
+
catalysts: string[];
|
|
20
|
+
risks: string[];
|
|
21
|
+
time_horizon: 'short_term' | 'medium_term' | 'long_term';
|
|
22
|
+
entry_strategy: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FALLBACK_THESIS: InvestmentThesis = {
|
|
26
|
+
bull_case: 'Quantitative metrics indicate favorable risk/reward. See scores for details.',
|
|
27
|
+
bear_case: 'Monitor red flags and technical weakness signals before committing capital.',
|
|
28
|
+
catalysts: ['Earnings surprise', 'Sector rotation', 'Macro tailwinds'],
|
|
29
|
+
risks: ['Market volatility', 'Rate sensitivity', 'Execution risk'],
|
|
30
|
+
time_horizon: 'medium_term',
|
|
31
|
+
entry_strategy: 'Consider scaling in near support levels identified in technical analysis.',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function generateThesis(
|
|
35
|
+
ticker: string,
|
|
36
|
+
overview: AVOverview,
|
|
37
|
+
composite: CompositeSignal,
|
|
38
|
+
valuation: ValuationScore,
|
|
39
|
+
technicals: TechnicalScore,
|
|
40
|
+
financials: FinancialHealth,
|
|
41
|
+
sentiment: SentimentScore,
|
|
42
|
+
): Promise<InvestmentThesis> {
|
|
43
|
+
const apiKey = process.env['GOOGLE_API_KEY'];
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
console.error('[thesis] GOOGLE_API_KEY not set — returning fallback thesis');
|
|
46
|
+
return FALLBACK_THESIS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const prompt = `You are a senior equity analyst. Based on the pre-computed analysis below, write a concise investment thesis. CRITICAL: Do NOT recalculate any numbers. All numbers have been verified. Your job is to INTERPRET, not COMPUTE.
|
|
50
|
+
|
|
51
|
+
Ticker: ${ticker}
|
|
52
|
+
Company: ${overview.Name ?? ticker}
|
|
53
|
+
Sector: ${overview.Sector ?? 'N/A'}
|
|
54
|
+
Industry: ${overview.Industry ?? 'N/A'}
|
|
55
|
+
|
|
56
|
+
Signal: ${composite.signal} (confidence: ${(composite.confidence * 100).toFixed(0)}%)
|
|
57
|
+
Composite Score: ${composite.composite_score}/100
|
|
58
|
+
|
|
59
|
+
Valuation: ${valuation.verdict} (score: ${valuation.composite.toFixed(0)}/100)
|
|
60
|
+
- P/E: ${overview.PERatio}, PEG: ${overview.PEGRatio}
|
|
61
|
+
- FCF Yield score: ${valuation.fcf_yield_score.toFixed(0)}/100
|
|
62
|
+
|
|
63
|
+
Technical Regime: ${technicals.regime}
|
|
64
|
+
- Trend: ${technicals.trend_score.toFixed(0)}/100, Momentum: ${technicals.momentum_score.toFixed(0)}/100
|
|
65
|
+
- Active Signals: ${JSON.stringify(technicals.signals.map((s) => s.name))}
|
|
66
|
+
|
|
67
|
+
Financial Health: ${financials.composite.toFixed(0)}/100
|
|
68
|
+
- Growth score: ${financials.growth_score.toFixed(0)}/100, Revenue growth: ${financials.raw.revenueGrowthPct.toFixed(1)}% YoY
|
|
69
|
+
- Red Flags: ${JSON.stringify(financials.red_flags)}
|
|
70
|
+
- Green Flags: ${JSON.stringify(financials.green_flags)}
|
|
71
|
+
|
|
72
|
+
Sentiment: ${sentiment.composite.toFixed(0)}/100 (${sentiment.news_volume} articles, ${(sentiment.bullish_ratio * 100).toFixed(0)}% bullish)
|
|
73
|
+
|
|
74
|
+
Support: ${composite.support_levels.join(', ')}
|
|
75
|
+
Resistance: ${composite.resistance_levels.join(', ')}
|
|
76
|
+
|
|
77
|
+
Respond ONLY with valid JSON (no markdown fences):
|
|
78
|
+
{
|
|
79
|
+
"bull_case": "3-4 sentences with specific numbers only from the data above",
|
|
80
|
+
"bear_case": "3-4 sentences with specific numbers only from the data above",
|
|
81
|
+
"catalysts": ["upcoming event or condition 1", "event 2", "event 3"],
|
|
82
|
+
"risks": ["risk 1", "risk 2", "risk 3"],
|
|
83
|
+
"time_horizon": "short_term | medium_term | long_term",
|
|
84
|
+
"entry_strategy": "Specific entry approach given support/resistance levels above"
|
|
85
|
+
}`;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const genAI = new GoogleGenerativeAI(apiKey);
|
|
89
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' });
|
|
90
|
+
const result = await model.generateContent(prompt);
|
|
91
|
+
const text = result.response.text().trim();
|
|
92
|
+
|
|
93
|
+
// Strip markdown fences if present
|
|
94
|
+
const clean = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
|
|
95
|
+
const parsed = JSON.parse(clean) as InvestmentThesis;
|
|
96
|
+
return parsed;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('[thesis] Gemini call failed:', err);
|
|
99
|
+
return FALLBACK_THESIS;
|
|
100
|
+
}
|
|
101
|
+
}
|