agentbnb 4.0.4 → 5.1.1
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/chunk-AUBHR7HH.js +25 -0
- package/dist/chunk-B5FTAGFN.js +393 -0
- package/dist/{chunk-GGYC5U2Z.js → chunk-BTTL24TZ.js} +29 -91
- package/dist/chunk-C6KPAFCC.js +387 -0
- package/dist/{chunk-JXEOE7HX.js → chunk-CRFCWD6V.js} +163 -92
- package/dist/chunk-CSATDXZC.js +89 -0
- package/dist/{chunk-T7NS2J2B.js → chunk-DFBX3BBD.js} +84 -1
- package/dist/{chunk-DNWT5FZQ.js → chunk-EANI2N2V.js} +98 -1
- package/dist/{chunk-HH24WMFN.js → chunk-FLY3WIQR.js} +1 -1
- package/dist/{chunk-EVBX22YU.js → chunk-HLUEOLSZ.js} +11 -17
- package/dist/chunk-IVOYM3WG.js +25 -0
- package/dist/chunk-LCAIAAG2.js +916 -0
- package/dist/chunk-MLS6IGGG.js +294 -0
- package/dist/{chunk-4P3EMGL4.js → chunk-MNO4COST.js} +5 -3
- package/dist/chunk-NH2FIERR.js +138 -0
- package/dist/chunk-UKT6H7YT.js +29 -0
- package/dist/{chunk-BH6WGYFB.js → chunk-VE3E4AMH.js} +8 -8
- package/dist/{chunk-5QGXARLJ.js → chunk-W5BZMKMF.js} +159 -27
- package/dist/{chunk-FF226TIV.js → chunk-ZX5623ER.js} +0 -57
- package/dist/cli/index.js +362 -4633
- package/dist/{conduct-N52JX7RT.js → conduct-KM6ZNJGE.js} +10 -8
- package/dist/{conduct-GZQNFTRP.js → conduct-WGTMQND5.js} +10 -8
- package/dist/{conductor-mode-XUWGR4ZE.js → conductor-mode-OL2FNOYY.js} +6 -4
- package/dist/{conductor-mode-ESGFZ6T5.js → conductor-mode-VRO7TYW2.js} +20 -167
- package/dist/execute-CPFSOOO3.js +13 -0
- package/dist/execute-IP2QHALV.js +10 -0
- package/dist/index.d.ts +14 -8
- package/dist/index.js +190 -36
- package/dist/{peers-E4MKNNDN.js → peers-CJ7T4RJO.js} +2 -1
- package/dist/process-guard-CC7CNRQJ.js +176 -0
- package/dist/{request-4GQSSM4B.js → request-YOWPXVLQ.js} +13 -10
- package/dist/schema-7BSSLZ4S.js +8 -0
- package/dist/{serve-skill-Q6NHX2RA.js → serve-skill-JHFNR7BW.js} +8 -7
- package/dist/{server-B5E566CI.js → server-HKJJWFRG.js} +10 -8
- package/dist/service-coordinator-UTKI4FRI.js +4922 -0
- package/dist/skills/agentbnb/bootstrap.js +5034 -849
- package/dist/websocket-client-WRN3HO73.js +6 -0
- package/package.json +4 -1
- package/skills/agentbnb/SKILL.md +87 -70
- package/skills/agentbnb/bootstrap.test.ts +142 -242
- package/skills/agentbnb/bootstrap.ts +88 -95
- package/skills/agentbnb/install.sh +97 -27
- 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/card-RNEWSAQ6.js +0 -88
- package/dist/chunk-UB2NPFC7.js +0 -165
- package/dist/execute-QH6F54D7.js +0 -10
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { AVDailyPrice } from '../api/types.js';
|
|
2
|
+
import type { ValuationScore } from './valuation.js';
|
|
3
|
+
import type { TechnicalScore } from './technicals.js';
|
|
4
|
+
import type { FinancialHealth } from './financial-health.js';
|
|
5
|
+
import type { SentimentScore } from './sentiment.js';
|
|
6
|
+
import { sp } from './utils.js';
|
|
7
|
+
|
|
8
|
+
export type InvestmentStyle = 'growth' | 'value' | 'momentum' | 'hybrid';
|
|
9
|
+
export type SignalVerdict = 'strong_buy' | 'buy' | 'hold' | 'sell' | 'strong_sell';
|
|
10
|
+
|
|
11
|
+
interface StyleWeights {
|
|
12
|
+
valuation: number;
|
|
13
|
+
technicals: number;
|
|
14
|
+
financials: number;
|
|
15
|
+
sentiment: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getStyleWeights(style: InvestmentStyle): StyleWeights {
|
|
19
|
+
const presets: Record<InvestmentStyle, StyleWeights> = {
|
|
20
|
+
growth: { valuation: 0.15, technicals: 0.25, financials: 0.40, sentiment: 0.20 },
|
|
21
|
+
value: { valuation: 0.40, technicals: 0.15, financials: 0.30, sentiment: 0.15 },
|
|
22
|
+
momentum: { valuation: 0.10, technicals: 0.45, financials: 0.15, sentiment: 0.30 },
|
|
23
|
+
hybrid: { valuation: 0.25, technicals: 0.30, financials: 0.25, sentiment: 0.20 },
|
|
24
|
+
};
|
|
25
|
+
return presets[style];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Find local support levels from price data */
|
|
29
|
+
function findSupportLevels(daily: AVDailyPrice[], count: number): number[] {
|
|
30
|
+
const prices = daily.slice(0, 90).map((d) => sp(d.low));
|
|
31
|
+
const levels: number[] = [];
|
|
32
|
+
|
|
33
|
+
for (let i = 2; i < prices.length - 2; i++) {
|
|
34
|
+
const p = prices[i] ?? 0;
|
|
35
|
+
if (
|
|
36
|
+
p <= (prices[i - 1] ?? 0) &&
|
|
37
|
+
p <= (prices[i - 2] ?? 0) &&
|
|
38
|
+
p <= (prices[i + 1] ?? 0) &&
|
|
39
|
+
p <= (prices[i + 2] ?? 0)
|
|
40
|
+
) {
|
|
41
|
+
levels.push(parseFloat(p.toFixed(2)));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Cluster nearby levels (within 1%)
|
|
46
|
+
const clustered: number[] = [];
|
|
47
|
+
for (const lvl of levels.sort((a, b) => b - a)) {
|
|
48
|
+
const isDuplicate = clustered.some((c) => Math.abs(c - lvl) / lvl < 0.01);
|
|
49
|
+
if (!isDuplicate) clustered.push(lvl);
|
|
50
|
+
if (clustered.length >= count) break;
|
|
51
|
+
}
|
|
52
|
+
return clustered;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Find local resistance levels from price data */
|
|
56
|
+
function findResistanceLevels(daily: AVDailyPrice[], count: number): number[] {
|
|
57
|
+
const prices = daily.slice(0, 90).map((d) => sp(d.high));
|
|
58
|
+
const levels: number[] = [];
|
|
59
|
+
|
|
60
|
+
for (let i = 2; i < prices.length - 2; i++) {
|
|
61
|
+
const p = prices[i] ?? 0;
|
|
62
|
+
if (
|
|
63
|
+
p >= (prices[i - 1] ?? 0) &&
|
|
64
|
+
p >= (prices[i - 2] ?? 0) &&
|
|
65
|
+
p >= (prices[i + 1] ?? 0) &&
|
|
66
|
+
p >= (prices[i + 2] ?? 0)
|
|
67
|
+
) {
|
|
68
|
+
levels.push(parseFloat(p.toFixed(2)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const clustered: number[] = [];
|
|
73
|
+
for (const lvl of levels.sort((a, b) => b - a)) {
|
|
74
|
+
const isDuplicate = clustered.some((c) => Math.abs(c - lvl) / lvl < 0.01);
|
|
75
|
+
if (!isDuplicate) clustered.push(lvl);
|
|
76
|
+
if (clustered.length >= count) break;
|
|
77
|
+
}
|
|
78
|
+
return clustered;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** How much do the four sub-scores agree? 0–1 */
|
|
82
|
+
function calculateAgreement(
|
|
83
|
+
valuation: ValuationScore,
|
|
84
|
+
technicals: TechnicalScore,
|
|
85
|
+
financials: FinancialHealth,
|
|
86
|
+
sentiment: SentimentScore,
|
|
87
|
+
): number {
|
|
88
|
+
const scores = [
|
|
89
|
+
valuation.composite,
|
|
90
|
+
technicals.composite,
|
|
91
|
+
financials.composite,
|
|
92
|
+
sentiment.composite,
|
|
93
|
+
];
|
|
94
|
+
const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
95
|
+
const variance = scores.reduce((acc, s) => acc + (s - mean) ** 2, 0) / scores.length;
|
|
96
|
+
const stdDev = Math.sqrt(variance);
|
|
97
|
+
// Low stdDev = high agreement → high confidence
|
|
98
|
+
return Math.max(0, 1 - stdDev / 30);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Data completeness: 0–1 based on whether modules have real data */
|
|
102
|
+
function calculateCompleteness(
|
|
103
|
+
valuation: ValuationScore,
|
|
104
|
+
technicals: TechnicalScore,
|
|
105
|
+
financials: FinancialHealth,
|
|
106
|
+
sentiment: SentimentScore,
|
|
107
|
+
): number {
|
|
108
|
+
let score = 0;
|
|
109
|
+
if (valuation.raw.pe > 0) score += 0.25;
|
|
110
|
+
if (technicals.raw.price > 0) score += 0.25;
|
|
111
|
+
if (financials.raw.revenueGrowthPct !== 0) score += 0.25;
|
|
112
|
+
if (sentiment.news_volume > 3) score += 0.25;
|
|
113
|
+
return score;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface CompositeSignal {
|
|
117
|
+
signal: SignalVerdict;
|
|
118
|
+
confidence: number; // 0–1
|
|
119
|
+
composite_score: number; // 0–100
|
|
120
|
+
support_levels: number[];
|
|
121
|
+
resistance_levels: number[];
|
|
122
|
+
key_factors: string[];
|
|
123
|
+
risk_factors: string[];
|
|
124
|
+
data_completeness: number; // 0–1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function generateCompositeSignal(
|
|
128
|
+
valuation: ValuationScore,
|
|
129
|
+
technicals: TechnicalScore,
|
|
130
|
+
financials: FinancialHealth,
|
|
131
|
+
sentiment: SentimentScore,
|
|
132
|
+
daily: AVDailyPrice[],
|
|
133
|
+
style: InvestmentStyle,
|
|
134
|
+
): CompositeSignal {
|
|
135
|
+
const w = getStyleWeights(style);
|
|
136
|
+
|
|
137
|
+
const composite_score =
|
|
138
|
+
valuation.composite * w.valuation +
|
|
139
|
+
technicals.composite * w.technicals +
|
|
140
|
+
financials.composite * w.financials +
|
|
141
|
+
sentiment.composite * w.sentiment;
|
|
142
|
+
|
|
143
|
+
const signal: SignalVerdict =
|
|
144
|
+
composite_score > 80 ? 'strong_buy'
|
|
145
|
+
: composite_score > 62 ? 'buy'
|
|
146
|
+
: composite_score > 42 ? 'hold'
|
|
147
|
+
: composite_score > 25 ? 'sell'
|
|
148
|
+
: 'strong_sell';
|
|
149
|
+
|
|
150
|
+
const agreementScore = calculateAgreement(valuation, technicals, financials, sentiment);
|
|
151
|
+
const data_completeness = calculateCompleteness(valuation, technicals, financials, sentiment);
|
|
152
|
+
const confidence = parseFloat((agreementScore * 0.6 + data_completeness * 0.4).toFixed(2));
|
|
153
|
+
|
|
154
|
+
const support_levels = findSupportLevels(daily, 3);
|
|
155
|
+
const resistance_levels = findResistanceLevels(daily, 3);
|
|
156
|
+
|
|
157
|
+
// Key positive factors
|
|
158
|
+
const key_factors: string[] = [];
|
|
159
|
+
if (valuation.verdict === 'undervalued') key_factors.push(`Undervalued: composite valuation ${valuation.composite.toFixed(0)}/100`);
|
|
160
|
+
if (financials.growth_score > 70) key_factors.push(`Strong growth: revenue +${financials.raw.revenueGrowthPct.toFixed(1)}% YoY`);
|
|
161
|
+
if (technicals.signals.some((s) => s.type === 'bullish' && s.strength >= 4)) {
|
|
162
|
+
const sig = technicals.signals.find((s) => s.type === 'bullish' && s.strength >= 4);
|
|
163
|
+
if (sig) key_factors.push(`Technical: ${sig.name}`);
|
|
164
|
+
}
|
|
165
|
+
if (sentiment.bullish_ratio > 0.7) key_factors.push(`Bullish sentiment: ${(sentiment.bullish_ratio * 100).toFixed(0)}% positive coverage`);
|
|
166
|
+
for (const flag of financials.green_flags.slice(0, 2)) key_factors.push(flag);
|
|
167
|
+
|
|
168
|
+
// Key risk factors
|
|
169
|
+
const risk_factors: string[] = [];
|
|
170
|
+
if (valuation.verdict === 'expensive') risk_factors.push(`Expensive valuation: composite ${valuation.composite.toFixed(0)}/100`);
|
|
171
|
+
if (technicals.signals.some((s) => s.type === 'bearish' && s.strength >= 4)) {
|
|
172
|
+
const sig = technicals.signals.find((s) => s.type === 'bearish' && s.strength >= 4);
|
|
173
|
+
if (sig) risk_factors.push(`Technical: ${sig.name}`);
|
|
174
|
+
}
|
|
175
|
+
for (const flag of financials.red_flags.slice(0, 2)) risk_factors.push(flag);
|
|
176
|
+
if (sentiment.bullish_ratio < 0.3) risk_factors.push(`Negative sentiment: only ${(sentiment.bullish_ratio * 100).toFixed(0)}% positive coverage`);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
signal,
|
|
180
|
+
confidence,
|
|
181
|
+
composite_score: parseFloat(composite_score.toFixed(1)),
|
|
182
|
+
support_levels,
|
|
183
|
+
resistance_levels,
|
|
184
|
+
key_factors: key_factors.slice(0, 3),
|
|
185
|
+
risk_factors: risk_factors.slice(0, 3),
|
|
186
|
+
data_completeness,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AVDailyPrice,
|
|
3
|
+
AVRSIEntry,
|
|
4
|
+
AVMACDEntry,
|
|
5
|
+
AVBBandsEntry,
|
|
6
|
+
AVStochEntry,
|
|
7
|
+
AVADXEntry,
|
|
8
|
+
} from '../api/types.js';
|
|
9
|
+
import { calcSMA, calcAvgVolume, formatNumber, mapRange, weightedAvg, sp } from './utils.js';
|
|
10
|
+
|
|
11
|
+
export interface TechnicalSignal {
|
|
12
|
+
type: 'bullish' | 'bearish' | 'neutral';
|
|
13
|
+
name: string;
|
|
14
|
+
strength: number; // 1–5
|
|
15
|
+
description: string;
|
|
16
|
+
detected_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TechnicalScore {
|
|
20
|
+
trend_score: number;
|
|
21
|
+
momentum_score: number;
|
|
22
|
+
volatility_score: number;
|
|
23
|
+
strength_score: number;
|
|
24
|
+
composite: number;
|
|
25
|
+
regime: 'strong_uptrend' | 'uptrend' | 'consolidation' | 'downtrend' | 'strong_downtrend';
|
|
26
|
+
signals: TechnicalSignal[];
|
|
27
|
+
raw: {
|
|
28
|
+
rsi: number;
|
|
29
|
+
macdHist: number;
|
|
30
|
+
adx: number;
|
|
31
|
+
bbPosition: number;
|
|
32
|
+
bandWidth: number;
|
|
33
|
+
price: number;
|
|
34
|
+
sma20: number;
|
|
35
|
+
sma50: number;
|
|
36
|
+
sma200: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function calcSMAAlignment(
|
|
41
|
+
price: number,
|
|
42
|
+
sma20: number,
|
|
43
|
+
sma50: number,
|
|
44
|
+
sma100: number,
|
|
45
|
+
sma200: number,
|
|
46
|
+
): number {
|
|
47
|
+
let score = 0;
|
|
48
|
+
// Perfect bull: price > SMA20 > SMA50 > SMA100 > SMA200 = 100
|
|
49
|
+
if (price > sma20) score += 25;
|
|
50
|
+
if (sma20 > sma50) score += 25;
|
|
51
|
+
if (sma50 > sma100) score += 25;
|
|
52
|
+
if (sma100 > sma200) score += 25;
|
|
53
|
+
return score;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function detectRSIDivergence(
|
|
57
|
+
daily: AVDailyPrice[],
|
|
58
|
+
rsi: AVRSIEntry[],
|
|
59
|
+
lookback: number,
|
|
60
|
+
): TechnicalSignal | null {
|
|
61
|
+
if (daily.length < lookback || rsi.length < lookback) return null;
|
|
62
|
+
|
|
63
|
+
// Check last `lookback` bars for price making new high but RSI declining (bearish divergence)
|
|
64
|
+
// or price making new low but RSI rising (bullish divergence)
|
|
65
|
+
const prices = daily.slice(0, lookback).map((d) => sp(d.close));
|
|
66
|
+
const rsiVals = rsi.slice(0, lookback).map((r) => sp(r.RSI));
|
|
67
|
+
|
|
68
|
+
const priceMin = Math.min(...prices);
|
|
69
|
+
const priceMax = Math.max(...prices);
|
|
70
|
+
const rsiMin = Math.min(...rsiVals);
|
|
71
|
+
const rsiMax = Math.max(...rsiVals);
|
|
72
|
+
|
|
73
|
+
const latestPrice = prices[0] ?? 0;
|
|
74
|
+
const latestRSI = rsiVals[0] ?? 50;
|
|
75
|
+
|
|
76
|
+
// Bullish divergence: price near low but RSI well above its low
|
|
77
|
+
if (
|
|
78
|
+
latestPrice <= priceMin * 1.02 &&
|
|
79
|
+
latestRSI >= rsiMin * 1.15 &&
|
|
80
|
+
latestRSI < 50
|
|
81
|
+
) {
|
|
82
|
+
return {
|
|
83
|
+
type: 'bullish',
|
|
84
|
+
name: 'RSI Bullish Divergence',
|
|
85
|
+
strength: 4,
|
|
86
|
+
description: `Price near ${lookback}-day low but RSI (${latestRSI.toFixed(1)}) is diverging upward`,
|
|
87
|
+
detected_at: daily[0]?.date ?? '',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Bearish divergence: price near high but RSI well below its high
|
|
92
|
+
if (
|
|
93
|
+
latestPrice >= priceMax * 0.98 &&
|
|
94
|
+
latestRSI <= rsiMax * 0.85 &&
|
|
95
|
+
latestRSI > 50
|
|
96
|
+
) {
|
|
97
|
+
return {
|
|
98
|
+
type: 'bearish',
|
|
99
|
+
name: 'RSI Bearish Divergence',
|
|
100
|
+
strength: 4,
|
|
101
|
+
description: `Price near ${lookback}-day high but RSI (${latestRSI.toFixed(1)}) is diverging downward`,
|
|
102
|
+
detected_at: daily[0]?.date ?? '',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function crossedAbove(
|
|
110
|
+
daily: AVDailyPrice[],
|
|
111
|
+
fastPeriod: number,
|
|
112
|
+
slowPeriod: number,
|
|
113
|
+
withinDays: number,
|
|
114
|
+
): boolean {
|
|
115
|
+
for (let i = 0; i < withinDays; i++) {
|
|
116
|
+
const slice = daily.slice(i);
|
|
117
|
+
const fastNow = calcSMA(slice, fastPeriod);
|
|
118
|
+
const slowNow = calcSMA(slice, slowPeriod);
|
|
119
|
+
const fastPrev = calcSMA(daily.slice(i + 1), fastPeriod);
|
|
120
|
+
const slowPrev = calcSMA(daily.slice(i + 1), slowPeriod);
|
|
121
|
+
if (fastNow > slowNow && fastPrev <= slowPrev) return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function crossedBelow(
|
|
127
|
+
daily: AVDailyPrice[],
|
|
128
|
+
fastPeriod: number,
|
|
129
|
+
slowPeriod: number,
|
|
130
|
+
withinDays: number,
|
|
131
|
+
): boolean {
|
|
132
|
+
for (let i = 0; i < withinDays; i++) {
|
|
133
|
+
const slice = daily.slice(i);
|
|
134
|
+
const fastNow = calcSMA(slice, fastPeriod);
|
|
135
|
+
const slowNow = calcSMA(slice, slowPeriod);
|
|
136
|
+
const fastPrev = calcSMA(daily.slice(i + 1), fastPeriod);
|
|
137
|
+
const slowPrev = calcSMA(daily.slice(i + 1), slowPeriod);
|
|
138
|
+
if (fastNow < slowNow && fastPrev >= slowPrev) return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function determineRegime(
|
|
144
|
+
composite: number,
|
|
145
|
+
trendScore: number,
|
|
146
|
+
strengthScore: number,
|
|
147
|
+
): TechnicalScore['regime'] {
|
|
148
|
+
if (composite > 80 && strengthScore > 70) return 'strong_uptrend';
|
|
149
|
+
if (composite > 60) return 'uptrend';
|
|
150
|
+
if (composite > 40) return 'consolidation';
|
|
151
|
+
if (composite > 20 && strengthScore > 70) return 'strong_downtrend';
|
|
152
|
+
return 'downtrend';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function analyzeTechnicals(
|
|
156
|
+
daily: AVDailyPrice[],
|
|
157
|
+
rsi: AVRSIEntry[],
|
|
158
|
+
macd: AVMACDEntry[],
|
|
159
|
+
bbands: AVBBandsEntry[],
|
|
160
|
+
stoch: AVStochEntry[],
|
|
161
|
+
adx: AVADXEntry[],
|
|
162
|
+
): TechnicalScore {
|
|
163
|
+
if (daily.length === 0) {
|
|
164
|
+
return {
|
|
165
|
+
trend_score: 50, momentum_score: 50, volatility_score: 50, strength_score: 50,
|
|
166
|
+
composite: 50, regime: 'consolidation', signals: [],
|
|
167
|
+
raw: { rsi: 50, macdHist: 0, adx: 20, bbPosition: 0.5, bandWidth: 0.05, price: 0, sma20: 0, sma50: 0, sma200: 0 },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const price = sp(daily[0]?.close);
|
|
172
|
+
|
|
173
|
+
// === Trend Score: SMA alignment ===
|
|
174
|
+
const sma20 = calcSMA(daily, 20);
|
|
175
|
+
const sma50 = calcSMA(daily, 50);
|
|
176
|
+
const sma100 = calcSMA(daily, 100);
|
|
177
|
+
const sma200 = calcSMA(daily, 200);
|
|
178
|
+
const trend_score = calcSMAAlignment(price, sma20, sma50, sma100, sma200);
|
|
179
|
+
|
|
180
|
+
// === Momentum Score: RSI + MACD ===
|
|
181
|
+
const latestRSI = sp(rsi[0]?.RSI ?? '50');
|
|
182
|
+
const macdHist = sp(macd[0]?.MACD_Hist ?? '0');
|
|
183
|
+
|
|
184
|
+
// RSI: oversold (<30) = bullish setup; overbought (>70) = bearish
|
|
185
|
+
const rsi_component =
|
|
186
|
+
latestRSI < 30 ? 80
|
|
187
|
+
: latestRSI > 70 ? 30
|
|
188
|
+
: mapRange(latestRSI, 30, 70, 40, 70);
|
|
189
|
+
|
|
190
|
+
// MACD histogram positive and growing = bullish momentum
|
|
191
|
+
const macd_component =
|
|
192
|
+
macdHist > 0
|
|
193
|
+
? mapRange(macdHist, 0, 2, 50, 90)
|
|
194
|
+
: mapRange(macdHist, -2, 0, 10, 50);
|
|
195
|
+
|
|
196
|
+
const momentum_score = rsi_component * 0.5 + macd_component * 0.5;
|
|
197
|
+
|
|
198
|
+
// === Volatility Score: Bollinger position ===
|
|
199
|
+
const upperBand = sp(bbands[0]?.['Real Upper Band'] ?? '0');
|
|
200
|
+
const lowerBand = sp(bbands[0]?.['Real Lower Band'] ?? '0');
|
|
201
|
+
const middleBand = sp(bbands[0]?.['Real Middle Band'] ?? '0');
|
|
202
|
+
const bandRange = upperBand - lowerBand;
|
|
203
|
+
const bandWidth = middleBand > 0 ? bandRange / middleBand : 0;
|
|
204
|
+
const bbPosition = bandRange > 0 ? (price - lowerBand) / bandRange : 0.5;
|
|
205
|
+
const volatility_score = bbPosition * 100;
|
|
206
|
+
|
|
207
|
+
// === Strength Score: ADX ===
|
|
208
|
+
const latestADX = sp(adx[0]?.ADX ?? '20');
|
|
209
|
+
const strength_score = mapRange(latestADX, 10, 50, 0, 100);
|
|
210
|
+
|
|
211
|
+
// === Signal Detection ===
|
|
212
|
+
const signals: TechnicalSignal[] = [];
|
|
213
|
+
const today = daily[0]?.date ?? '';
|
|
214
|
+
|
|
215
|
+
// Golden Cross / Death Cross (SMA50 vs SMA200)
|
|
216
|
+
if (crossedAbove(daily, 50, 200, 5)) {
|
|
217
|
+
signals.push({
|
|
218
|
+
type: 'bullish', name: 'Golden Cross', strength: 5,
|
|
219
|
+
description: 'SMA50 crossed above SMA200 within last 5 trading days',
|
|
220
|
+
detected_at: today,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (crossedBelow(daily, 50, 200, 5)) {
|
|
224
|
+
signals.push({
|
|
225
|
+
type: 'bearish', name: 'Death Cross', strength: 5,
|
|
226
|
+
description: 'SMA50 crossed below SMA200 within last 5 trading days',
|
|
227
|
+
detected_at: today,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// RSI Divergence
|
|
232
|
+
const rsiDiv = detectRSIDivergence(daily, rsi, 20);
|
|
233
|
+
if (rsiDiv) signals.push(rsiDiv);
|
|
234
|
+
|
|
235
|
+
// MACD Bullish Crossover (hist went positive)
|
|
236
|
+
const prevMacdHist = sp(macd[1]?.MACD_Hist ?? '0');
|
|
237
|
+
if (macdHist > 0 && prevMacdHist < 0) {
|
|
238
|
+
signals.push({
|
|
239
|
+
type: 'bullish', name: 'MACD Bullish Crossover', strength: 3,
|
|
240
|
+
description: 'MACD crossed above signal line',
|
|
241
|
+
detected_at: today,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (macdHist < 0 && prevMacdHist > 0) {
|
|
245
|
+
signals.push({
|
|
246
|
+
type: 'bearish', name: 'MACD Bearish Crossover', strength: 3,
|
|
247
|
+
description: 'MACD crossed below signal line',
|
|
248
|
+
detected_at: today,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Bollinger Squeeze (very narrow band → major move incoming)
|
|
253
|
+
if (bandWidth < 0.05) {
|
|
254
|
+
signals.push({
|
|
255
|
+
type: 'neutral', name: 'Bollinger Squeeze', strength: 4,
|
|
256
|
+
description: `Band width at ${(bandWidth * 100).toFixed(1)}% — extremely compressed. Major move incoming.`,
|
|
257
|
+
detected_at: today,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Stochastic oversold reversal
|
|
262
|
+
const stochK = sp(stoch[0]?.SlowK ?? '50');
|
|
263
|
+
const stochD = sp(stoch[0]?.SlowD ?? '50');
|
|
264
|
+
const prevStochK = sp(stoch[1]?.SlowK ?? '50');
|
|
265
|
+
if (stochK < 20 && stochK > stochD && prevStochK <= stochD) {
|
|
266
|
+
signals.push({
|
|
267
|
+
type: 'bullish', name: 'Stochastic Oversold Reversal', strength: 3,
|
|
268
|
+
description: `Stoch K(${stochK.toFixed(1)}) crossing above D(${stochD.toFixed(1)}) in oversold territory`,
|
|
269
|
+
detected_at: today,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Volume Spike (2x 20-day average)
|
|
274
|
+
const avgVol20 = calcAvgVolume(daily, 20);
|
|
275
|
+
const todayVol = sp(daily[0]?.volume ?? '0');
|
|
276
|
+
if (avgVol20 > 0 && todayVol > avgVol20 * 2) {
|
|
277
|
+
signals.push({
|
|
278
|
+
type: 'neutral', name: 'Volume Spike', strength: 4,
|
|
279
|
+
description: `Volume ${formatNumber(todayVol)} is ${(todayVol / avgVol20).toFixed(1)}x the 20-day average`,
|
|
280
|
+
detected_at: today,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Price above/below 200 SMA
|
|
285
|
+
if (sma200 > 0) {
|
|
286
|
+
if (price > sma200 * 1.10) {
|
|
287
|
+
signals.push({
|
|
288
|
+
type: 'bullish', name: 'Strong Uptrend', strength: 2,
|
|
289
|
+
description: `Price is ${(((price / sma200) - 1) * 100).toFixed(1)}% above 200-day SMA`,
|
|
290
|
+
detected_at: today,
|
|
291
|
+
});
|
|
292
|
+
} else if (price < sma200 * 0.90) {
|
|
293
|
+
signals.push({
|
|
294
|
+
type: 'bearish', name: 'Strong Downtrend', strength: 2,
|
|
295
|
+
description: `Price is ${(((sma200 / price) - 1) * 100).toFixed(1)}% below 200-day SMA`,
|
|
296
|
+
detected_at: today,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const composite = weightedAvg([
|
|
302
|
+
[trend_score, 0.30],
|
|
303
|
+
[momentum_score, 0.30],
|
|
304
|
+
[volatility_score, 0.15],
|
|
305
|
+
[strength_score, 0.25],
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
trend_score,
|
|
310
|
+
momentum_score,
|
|
311
|
+
volatility_score,
|
|
312
|
+
strength_score,
|
|
313
|
+
composite,
|
|
314
|
+
regime: determineRegime(composite, trend_score, strength_score),
|
|
315
|
+
signals,
|
|
316
|
+
raw: { rsi: latestRSI, macdHist, adx: latestADX, bbPosition, bandWidth, price, sma20, sma50, sma200 },
|
|
317
|
+
};
|
|
318
|
+
}
|