Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ ACLED_EMAIL=
# OAuth2 password grant (API keys deprecated Sept 2025)
ACLED_PASSWORD=

# Stock social sentiment from Reddit, X/Twitter (free: api.adanos.org/docs)
ADANOS_API_KEY=

# === Server Configuration ===

# Dashboard server port
Expand Down
8 changes: 7 additions & 1 deletion apis/briefing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import { briefing as space } from './sources/space.mjs';
// === Tier 5: Live Market Data ===
import { briefing as yfinance } from './sources/yfinance.mjs';

// === Tier 6: Social Sentiment ===
import { briefing as adanos } from './sources/adanos.mjs';

export async function runSource(name, fn, ...args) {
const start = Date.now();
try {
Expand All @@ -54,7 +57,7 @@ export async function runSource(name, fn, ...args) {
}

export async function fullBriefing() {
console.error('[Crucix] Starting intelligence sweep — 27 sources...');
console.error('[Crucix] Starting intelligence sweep — 28 sources...');
const start = Date.now();

const results = await Promise.allSettled([
Expand Down Expand Up @@ -94,6 +97,9 @@ export async function fullBriefing() {

// Tier 5: Live Market Data
runSource('YFinance', yfinance),

// Tier 6: Social Sentiment
runSource('Adanos', adanos),
]);

const sources = results.map(r => r.status === 'fulfilled' ? r.value : { status: 'failed', error: r.reason?.message });
Expand Down
136 changes: 136 additions & 0 deletions apis/sources/adanos.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Adanos — Social Sentiment Intelligence for Stocks
// Structured stock sentiment from Reddit and X/Twitter, plus Reddit sector analysis
// Provides trending stocks, buzz scores, sentiment breakdowns, and sector signals
// Free API key required: https://api.adanos.org/docs
// Updates every ~15 minutes (Reddit), ~2 hours (X/Twitter)

import { safeFetch } from '../utils/fetch.mjs';
import '../utils/env.mjs';

const BASE = 'https://api.adanos.org';

async function apiFetch(path, apiKey) {
return safeFetch(`${BASE}${path}`, {
timeout: 12000,
headers: { 'X-API-Key': apiKey },
});
}

function toFiniteNumber(value, fallback = 0) {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
}

function toNullableNumber(value) {
const n = Number(value);
return Number.isFinite(n) ? n : null;
}

// Fetch trending stocks from a platform
async function fetchTrending(platform, apiKey, limit = 20) {
const data = await apiFetch(`/${platform}/stocks/v1/trending?days=7&limit=${limit}`, apiKey);
if (data?.error) return [];
return (Array.isArray(data) ? data : []).map(s => ({
ticker: s.ticker,
name: s.company_name || null,
buzz: toFiniteNumber(s.buzz_score),
trend: s.trend,
mentions: toFiniteNumber(s.mentions),
sentiment: toNullableNumber(s.sentiment_score),
bullishPct: toNullableNumber(s.bullish_pct),
bearishPct: toNullableNumber(s.bearish_pct),
upvotes: toFiniteNumber(s.total_upvotes),
}));
}

// Fetch sector-level aggregation
async function fetchSectors(apiKey) {
const data = await apiFetch('/reddit/stocks/v1/trending/sectors?days=7', apiKey);
if (data?.error) return [];
return (Array.isArray(data) ? data : []).map(s => ({
sector: s.sector,
buzz: toFiniteNumber(s.buzz_score),
trend: s.trend,
mentions: toFiniteNumber(s.mentions),
sentiment: toNullableNumber(s.sentiment_score),
bullishPct: toNullableNumber(s.bullish_pct),
bearishPct: toNullableNumber(s.bearish_pct),
topTickers: s.top_tickers || [],
}));
}

export async function briefing() {
const apiKey = process.env.ADANOS_API_KEY;
if (!apiKey) {
return {
source: 'Adanos',
timestamp: new Date().toISOString(),
status: 'no_key',
message: 'Adanos API key required. Register free at https://api.adanos.org/docs and set ADANOS_API_KEY in .env',
};
}

const [reddit, x, sectors] = await Promise.all([
fetchTrending('reddit', apiKey),
fetchTrending('x', apiKey),
fetchSectors(apiKey),
]);

// Compute aggregate market sentiment from Reddit (larger sample)
const allStocks = reddit.length ? reddit : x;
const weightedStocks = allStocks.filter(t => t.mentions > 0);
const totalMentions = weightedStocks.reduce((sum, ticker) => sum + ticker.mentions, 0);
let avgSentiment = 0;
let avgBullish = 0;
let avgBearish = 0;
if (totalMentions > 0) {
for (const t of weightedStocks) {
const w = t.mentions / totalMentions;
avgSentiment += toFiniteNumber(t.sentiment) * w;
avgBullish += toFiniteNumber(t.bullishPct) * w;
avgBearish += toFiniteNumber(t.bearishPct) * w;
}
}

const scoredStocks = allStocks.filter(t => t.sentiment != null);
const sorted = [...scoredStocks].sort((a, b) => b.sentiment - a.sentiment);
const topBullish = sorted.slice(0, 5).filter(t => t.sentiment > 0);
const topBearish = sorted.slice(-5).reverse().filter(t => t.sentiment < 0);

// Generate signals
const signals = [];
if (avgBullish > 65) signals.push(`Extreme bullish consensus at ${avgBullish.toFixed(0)}% — contrarian caution`);
if (avgBearish > 45) signals.push(`Elevated bearish sentiment at ${avgBearish.toFixed(0)}% — fear may be peaking`);

const risingCount = allStocks.filter(t => t.trend === 'rising').length;
const fallingCount = allStocks.filter(t => t.trend === 'falling').length;
if (risingCount > allStocks.length * 0.6) signals.push(`Broad momentum: ${risingCount}/${allStocks.length} tickers rising`);
if (fallingCount > allStocks.length * 0.5) signals.push(`Broad weakness: ${fallingCount}/${allStocks.length} tickers falling`);

const hotSectors = [...sectors].sort((a, b) => b.buzz - a.buzz).slice(0, 3);
for (const s of hotSectors) {
if (s.buzz > 60) signals.push(`${s.sector} sector buzz elevated at ${s.buzz.toFixed(0)} — ${s.trend}`);
}

return {
source: 'Adanos',
timestamp: new Date().toISOString(),
reddit: { trending: reddit, sectors },
x: { trending: x },
aggregate: {
totalTickers: allStocks.length,
avgSentiment: parseFloat(avgSentiment.toFixed(3)),
bullishPct: Math.round(avgBullish),
bearishPct: Math.round(avgBearish),
topBullish,
topBearish,
hotSectors,
},
signals,
};
}

if (process.argv[1]?.endsWith('adanos.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}
48 changes: 48 additions & 0 deletions dashboard/inject.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,33 @@ export function generateIdeas(V2) {
}
}

// Adanos Social Sentiment Signals
const sentAgg = V2.sentiment?.aggregate || {};
if (sentAgg.bullishPct > 65 && vix && vix.value < 18) {
ideas.push({
title: 'Retail Euphoria vs Low Vol',
text: `Social sentiment ${sentAgg.bullishPct}% bullish but VIX at ${vix.value.toFixed(0)} — retail exuberance with low hedging. Contrarian caution warranted.`,
type: 'watch', confidence: 'Medium', horizon: 'tactical'
});
}
if (sentAgg.bearishPct > 40 && vix && vix.value > 22) {
ideas.push({
title: 'Social Fear Confirming Vol Spike',
text: `Social sentiment ${sentAgg.bearishPct}% bearish + VIX ${vix.value.toFixed(0)}. Retail and institutional fear aligned — capitulation may be near.`,
type: 'watch', confidence: 'Medium', horizon: 'swing'
});
}
const sentTrending = V2.sentiment?.trending || [];
const risingTickers = sentTrending.filter(t => t.trend === 'rising' && t.buzz > 50);
if (risingTickers.length >= 3) {
const names = risingTickers.slice(0, 3).map(t => t.ticker).join(', ');
ideas.push({
title: 'Social Momentum Cluster',
text: `${risingTickers.length} tickers with rising buzz >50: ${names}. Social momentum often precedes volume spikes.`,
type: 'long', confidence: 'Medium', horizon: 'swing'
});
}

return ideas.slice(0, 8);
}

Expand Down Expand Up @@ -410,6 +437,26 @@ export async function synthesize(data) {
n: name, err: Boolean(src.error), stale: Boolean(src.stale)
}));

// === Adanos Social Sentiment ===
const adanosData = data.sources.Adanos || {};
const sentiment = adanosData.error ? { trending: [], sectors: [], signals: [] } : {
trending: (adanosData.reddit?.trending || []).slice(0, 15).map(t => ({
ticker: t.ticker, name: t.name, buzz: t.buzz, trend: t.trend,
mentions: t.mentions, sentiment: t.sentiment,
bullishPct: t.bullishPct, bearishPct: t.bearishPct,
})),
xTrending: (adanosData.x?.trending || []).slice(0, 10).map(t => ({
ticker: t.ticker, name: t.name, buzz: t.buzz, trend: t.trend,
mentions: t.mentions, sentiment: t.sentiment,
})),
sectors: (adanosData.reddit?.sectors || []).map(s => ({
sector: s.sector, buzz: s.buzz, trend: s.trend,
sentiment: s.sentiment, topTickers: s.topTickers,
})),
aggregate: adanosData.aggregate || {},
signals: adanosData.signals || [],
};

// === Yahoo Finance live market data ===
const yfData = data.sources.YFinance || {};
const yfQuotes = yfData.quotes || {};
Expand Down Expand Up @@ -455,6 +502,7 @@ export async function synthesize(data) {
sdr: { total: sdrNet.totalReceivers || 0, online: sdrNet.online || 0, zones: sdrZones },
tg: { posts: tgData.totalPosts || 0, urgent: tgUrgent, topPosts: tgTop },
who, fred, energy, bls, treasury, gscpi, defense, noaa, acled, gdelt, space, health, news,
sentiment, // Adanos social sentiment data
markets, // Live Yahoo Finance market data
ideas: [], ideasSource: 'disabled',
// newsFeed for ticker (merged RSS + GDELT + Telegram)
Expand Down
4 changes: 4 additions & 0 deletions lib/delta/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const DEFAULT_NUMERIC_THRESHOLDS = {
'10y_yield': 3,
usd_index: 1,
mortgage: 2,
social_sentiment: 15, // % swing in aggregate social sentiment
};

const DEFAULT_COUNT_THRESHOLDS = {
Expand All @@ -30,6 +31,7 @@ const DEFAULT_COUNT_THRESHOLDS = {
sdr_online: 3, // ±3 receivers
news_count: 5, // ±5 news items
sources_ok: 1, // any source going down matters
sentiment_trending: 3, // ±3 trending tickers
};

// ─── Metric Definitions ──────────────────────────────────────────────────────
Expand All @@ -46,6 +48,7 @@ const NUMERIC_METRICS = [
{ key: '10y_yield', extract: d => d.fred?.find(f => f.id === 'DGS10')?.value, label: '10Y Yield' },
{ key: 'usd_index', extract: d => d.fred?.find(f => f.id === 'DTWEXBGS')?.value, label: 'USD Index' },
{ key: 'mortgage', extract: d => d.fred?.find(f => f.id === 'MORTGAGE30US')?.value, label: '30Y Mortgage' },
{ key: 'social_sentiment', extract: d => d.sentiment?.aggregate?.avgSentiment, label: 'Social Sentiment (Avg)' },
];

const COUNT_METRICS = [
Expand All @@ -58,6 +61,7 @@ const COUNT_METRICS = [
{ key: 'sdr_online', extract: d => d.sdr?.online || 0, label: 'SDR Receivers' },
{ key: 'news_count', extract: d => (d.news?.length ?? d.news?.count) || 0, label: 'News Items' },
{ key: 'sources_ok', extract: d => d.meta?.sourcesOk || 0, label: 'Sources OK' },
{ key: 'sentiment_trending', extract: d => d.sentiment?.trending?.length || 0, label: 'Trending Tickers (Sentiment)' },
];

// Risk-sensitive keys: used for determining overall direction
Expand Down