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.
@@ -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
+ }