opencandle 0.2.0 → 0.3.0
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/README.md +110 -87
- package/dist/analysts/orchestrator.js +1 -2
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/config.d.ts +25 -5
- package/dist/config.js +16 -8
- package/dist/config.js.map +1 -1
- package/dist/infra/cache.d.ts +4 -0
- package/dist/infra/cache.js +4 -0
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/rate-limiter.js +6 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/onboarding/connect.d.ts +23 -0
- package/dist/onboarding/connect.js +107 -0
- package/dist/onboarding/connect.js.map +1 -0
- package/dist/onboarding/credential-interceptor.d.ts +44 -0
- package/dist/onboarding/credential-interceptor.js +72 -0
- package/dist/onboarding/credential-interceptor.js.map +1 -0
- package/dist/onboarding/degradation-accumulator.d.ts +21 -0
- package/dist/onboarding/degradation-accumulator.js +55 -0
- package/dist/onboarding/degradation-accumulator.js.map +1 -0
- package/dist/onboarding/prompt-user.d.ts +23 -0
- package/dist/onboarding/prompt-user.js +61 -0
- package/dist/onboarding/prompt-user.js.map +1 -0
- package/dist/onboarding/providers.d.ts +109 -0
- package/dist/onboarding/providers.js +236 -0
- package/dist/onboarding/providers.js.map +1 -0
- package/dist/onboarding/state.d.ts +31 -2
- package/dist/onboarding/state.js +141 -13
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +34 -0
- package/dist/onboarding/tool-helpers.js +80 -0
- package/dist/onboarding/tool-helpers.js.map +1 -0
- package/dist/onboarding/tool-tags.d.ts +37 -0
- package/dist/onboarding/tool-tags.js +149 -0
- package/dist/onboarding/tool-tags.js.map +1 -0
- package/dist/onboarding/validation.d.ts +19 -0
- package/dist/onboarding/validation.js +117 -0
- package/dist/onboarding/validation.js.map +1 -0
- package/dist/pi/opencandle-extension.js +303 -4
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/setup.d.ts +0 -1
- package/dist/pi/setup.js +66 -119
- package/dist/pi/setup.js.map +1 -1
- package/dist/prompts/context-builder.js +2 -1
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/providers/alpha-vantage.js +20 -1
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/exa-search.d.ts +39 -0
- package/dist/providers/exa-search.js +276 -0
- package/dist/providers/exa-search.js.map +1 -0
- package/dist/providers/finnhub.d.ts +17 -0
- package/dist/providers/finnhub.js +94 -0
- package/dist/providers/finnhub.js.map +1 -0
- package/dist/providers/fred.js +13 -1
- package/dist/providers/fred.js.map +1 -1
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider-credential-error.d.ts +8 -0
- package/dist/providers/provider-credential-error.js +22 -0
- package/dist/providers/provider-credential-error.js.map +1 -0
- package/dist/providers/reddit.d.ts +8 -0
- package/dist/providers/reddit.js +36 -9
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/twitter.js +2 -8
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/web-search.d.ts +17 -0
- package/dist/providers/web-search.js +224 -0
- package/dist/providers/web-search.js.map +1 -0
- package/dist/providers/wrap-provider.d.ts +7 -0
- package/dist/providers/wrap-provider.js +15 -0
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/routing/classify-intent.js +22 -0
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +0 -1
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/sentiment/adapters/finnhub.d.ts +7 -0
- package/dist/sentiment/adapters/finnhub.js +39 -0
- package/dist/sentiment/adapters/finnhub.js.map +1 -0
- package/dist/sentiment/adapters/reddit.d.ts +11 -0
- package/dist/sentiment/adapters/reddit.js +54 -0
- package/dist/sentiment/adapters/reddit.js.map +1 -0
- package/dist/sentiment/adapters/twitter.d.ts +9 -0
- package/dist/sentiment/adapters/twitter.js +32 -0
- package/dist/sentiment/adapters/twitter.js.map +1 -0
- package/dist/sentiment/adapters/web.d.ts +9 -0
- package/dist/sentiment/adapters/web.js +40 -0
- package/dist/sentiment/adapters/web.js.map +1 -0
- package/dist/sentiment/index.d.ts +16 -0
- package/dist/sentiment/index.js +44 -0
- package/dist/sentiment/index.js.map +1 -0
- package/dist/sentiment/keywords.d.ts +2 -0
- package/dist/sentiment/keywords.js +9 -0
- package/dist/sentiment/keywords.js.map +1 -0
- package/dist/sentiment/pipeline.d.ts +9 -0
- package/dist/sentiment/pipeline.js +57 -0
- package/dist/sentiment/pipeline.js.map +1 -0
- package/dist/sentiment/scorer.d.ts +9 -0
- package/dist/sentiment/scorer.js +64 -0
- package/dist/sentiment/scorer.js.map +1 -0
- package/dist/sentiment/store.d.ts +24 -0
- package/dist/sentiment/store.js +177 -0
- package/dist/sentiment/store.js.map +1 -0
- package/dist/sentiment/trends.d.ts +13 -0
- package/dist/sentiment/trends.js +73 -0
- package/dist/sentiment/trends.js.map +1 -0
- package/dist/sentiment/types.d.ts +66 -0
- package/dist/sentiment/types.js +54 -0
- package/dist/sentiment/types.js.map +1 -0
- package/dist/system-prompt.js +9 -1
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/fundamentals/company-overview.d.ts +3 -1
- package/dist/tools/fundamentals/company-overview.js +27 -27
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.js +45 -45
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.js +82 -82
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +3 -1
- package/dist/tools/fundamentals/earnings.js +25 -25
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.d.ts +3 -1
- package/dist/tools/fundamentals/financials.js +23 -23
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/index.js +8 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.js +28 -64
- package/dist/tools/interaction/ask-user.js.map +1 -1
- package/dist/tools/macro/fred-data.d.ts +3 -1
- package/dist/tools/macro/fred-data.js +26 -26
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +3 -1
- package/dist/tools/sentiment/reddit-sentiment.js +107 -22
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
- package/dist/tools/sentiment/sentiment-summary.js +230 -0
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
- package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
- package/dist/tools/sentiment/sentiment-trend.js +39 -0
- package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
- package/dist/tools/sentiment/twitter-sentiment.js +17 -0
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-search.d.ts +11 -0
- package/dist/tools/sentiment/web-search.js +115 -0
- package/dist/tools/sentiment/web-search.js.map +1 -0
- package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
- package/dist/tools/sentiment/web-sentiment.js +66 -0
- package/dist/tools/sentiment/web-sentiment.js.map +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/sentiment.d.ts +21 -0
- package/package.json +19 -3
- package/dist/tools/sentiment/news-sentiment.d.ts +0 -7
- package/dist/tools/sentiment/news-sentiment.js +0 -55
- package/dist/tools/sentiment/news-sentiment.js.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export class WebAdapter {
|
|
3
|
+
source = "web";
|
|
4
|
+
mapToRecords(envelope, query) {
|
|
5
|
+
const fetchedAt = envelope.fetchedAt;
|
|
6
|
+
return envelope.results.map((result) => ({
|
|
7
|
+
id: randomUUID(),
|
|
8
|
+
source: this.source,
|
|
9
|
+
sourceId: canonicalizeUrl(result.url),
|
|
10
|
+
query,
|
|
11
|
+
title: result.title,
|
|
12
|
+
text: result.snippet,
|
|
13
|
+
author: result.source,
|
|
14
|
+
url: result.url,
|
|
15
|
+
publishedAt: result.published,
|
|
16
|
+
fetchedAt,
|
|
17
|
+
engagement: {
|
|
18
|
+
score: 0,
|
|
19
|
+
replies: null,
|
|
20
|
+
shares: null,
|
|
21
|
+
views: null,
|
|
22
|
+
},
|
|
23
|
+
sentiment: { score: 0, confidence: 0, method: "keyword", tickers: [] },
|
|
24
|
+
metadata: { category: result.category, provider: envelope.provider },
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
async fetch(_query, _options) {
|
|
28
|
+
throw new Error("Use pipeline.run() instead of adapter.fetch() directly");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function canonicalizeUrl(url) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(url);
|
|
34
|
+
return parsed.origin + parsed.pathname;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../../src/sentiment/adapters/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,MAAM,OAAO,UAAU;IACZ,MAAM,GAAG,KAAc,CAAC;IAEjC,YAAY,CAAC,QAA2B,EAAE,KAAa;QACrD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;QACrC,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACvC,EAAE,EAAE,UAAU,EAAE;YAChB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,QAAQ,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC;YACrC,KAAK;YACL,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,IAAI,EAAE,MAAM,CAAC,OAAO;YACpB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,WAAW,EAAE,MAAM,CAAC,SAAS;YAC7B,SAAS;YACT,UAAU,EAAE;gBACV,KAAK,EAAE,CAAC;gBACR,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,IAAI;gBACZ,KAAK,EAAE,IAAI;aACZ;YACD,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,SAAkB,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/E,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE;SACrE,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,MAAc,EAAE,QAA6B;QACvD,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;IAC5E,CAAC;CACF;AAED,SAAS,eAAe,CAAC,GAAW;IAClC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { SentinelRecord, SentinelEngagement, SentinelSentiment, SentimentAdapter, ScorerOptions, TrendBucket, TrendResult, DivergenceResult, SentimentSummary, SentimentSource, } from "./types.js";
|
|
2
|
+
export { isSentinelRecord, SENTIMENT_SOURCES } from "./types.js";
|
|
3
|
+
export { SentimentStore } from "./store.js";
|
|
4
|
+
export { scoreRecords, keywordScore } from "./scorer.js";
|
|
5
|
+
export { SentimentPipeline } from "./pipeline.js";
|
|
6
|
+
export { renderSparkline, computeTrend, computeDivergence } from "./trends.js";
|
|
7
|
+
export { BULLISH_TERMS, BEARISH_TERMS } from "./keywords.js";
|
|
8
|
+
export { TwitterAdapter } from "./adapters/twitter.js";
|
|
9
|
+
export { RedditAdapter } from "./adapters/reddit.js";
|
|
10
|
+
export { WebAdapter } from "./adapters/web.js";
|
|
11
|
+
import { SentimentStore } from "./store.js";
|
|
12
|
+
import { SentimentPipeline } from "./pipeline.js";
|
|
13
|
+
export declare function getSentimentStore(): SentimentStore;
|
|
14
|
+
export declare function getSentimentPipeline(): SentimentPipeline;
|
|
15
|
+
/** For testing: reset singletons */
|
|
16
|
+
export declare function _resetSentimentSingletons(): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export { isSentinelRecord, SENTIMENT_SOURCES } from "./types.js";
|
|
2
|
+
export { SentimentStore } from "./store.js";
|
|
3
|
+
export { scoreRecords, keywordScore } from "./scorer.js";
|
|
4
|
+
export { SentimentPipeline } from "./pipeline.js";
|
|
5
|
+
export { renderSparkline, computeTrend, computeDivergence } from "./trends.js";
|
|
6
|
+
export { BULLISH_TERMS, BEARISH_TERMS } from "./keywords.js";
|
|
7
|
+
export { TwitterAdapter } from "./adapters/twitter.js";
|
|
8
|
+
export { RedditAdapter } from "./adapters/reddit.js";
|
|
9
|
+
export { WebAdapter } from "./adapters/web.js";
|
|
10
|
+
import { SentimentStore } from "./store.js";
|
|
11
|
+
import { SentimentPipeline } from "./pipeline.js";
|
|
12
|
+
import { getConfig } from "../config.js";
|
|
13
|
+
import { resolveOpenCandlePath } from "../infra/opencandle-paths.js";
|
|
14
|
+
let _pipeline = null;
|
|
15
|
+
let _store = null;
|
|
16
|
+
export function getSentimentStore() {
|
|
17
|
+
if (!_store) {
|
|
18
|
+
const dbPath = resolveOpenCandlePath("sentinel.db");
|
|
19
|
+
_store = new SentimentStore(dbPath);
|
|
20
|
+
const config = getConfig();
|
|
21
|
+
_store.prune(config.sentiment?.retentionDays ?? 30);
|
|
22
|
+
}
|
|
23
|
+
return _store;
|
|
24
|
+
}
|
|
25
|
+
export function getSentimentPipeline() {
|
|
26
|
+
if (!_pipeline) {
|
|
27
|
+
const store = getSentimentStore();
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
_pipeline = new SentimentPipeline(store, config.sentiment);
|
|
30
|
+
}
|
|
31
|
+
return _pipeline;
|
|
32
|
+
}
|
|
33
|
+
/** For testing: reset singletons */
|
|
34
|
+
export function _resetSentimentSingletons() {
|
|
35
|
+
if (_store) {
|
|
36
|
+
try {
|
|
37
|
+
_store.close();
|
|
38
|
+
}
|
|
39
|
+
catch { /* ignore */ }
|
|
40
|
+
}
|
|
41
|
+
_pipeline = null;
|
|
42
|
+
_store = null;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sentiment/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAErE,IAAI,SAAS,GAA6B,IAAI,CAAC;AAC/C,IAAI,MAAM,GAA0B,IAAI,CAAC;AAEzC,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAC;QACpD,MAAM,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,KAAK,GAAG,iBAAiB,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,SAAS,GAAG,IAAI,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,SAAU,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,yBAAyB;IACvC,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC;YAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAChD,CAAC;IACD,SAAS,GAAG,IAAI,CAAC;IACjB,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const BULLISH_TERMS: readonly ["moon", "buy", "undervalued", "breakout", "calls", "bullish", "rocket", "diamond hands", "accumulate", "dip buy", "long", "rip", "squeeze"];
|
|
2
|
+
export declare const BEARISH_TERMS: readonly ["crash", "overvalued", "sell", "puts", "bearish", "bubble", "dump", "short", "bagholding", "exit", "drill", "tank", "rug"];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const BULLISH_TERMS = [
|
|
2
|
+
"moon", "buy", "undervalued", "breakout", "calls", "bullish",
|
|
3
|
+
"rocket", "diamond hands", "accumulate", "dip buy", "long", "rip", "squeeze",
|
|
4
|
+
];
|
|
5
|
+
export const BEARISH_TERMS = [
|
|
6
|
+
"crash", "overvalued", "sell", "puts", "bearish", "bubble",
|
|
7
|
+
"dump", "short", "bagholding", "exit", "drill", "tank", "rug",
|
|
8
|
+
];
|
|
9
|
+
//# sourceMappingURL=keywords.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keywords.js","sourceRoot":"","sources":["../../src/sentiment/keywords.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS;IAC5D,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS;CACpE,CAAC;AAEX,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ;IAC1D,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK;CACrD,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SentinelRecord, SentimentSummary } from "./types.js";
|
|
2
|
+
import type { SentimentConfig } from "../config.js";
|
|
3
|
+
import { SentimentStore } from "./store.js";
|
|
4
|
+
export declare class SentimentPipeline {
|
|
5
|
+
private store;
|
|
6
|
+
private config;
|
|
7
|
+
constructor(store: SentimentStore, config: SentimentConfig);
|
|
8
|
+
processRecords(records: SentinelRecord[], query: string): Promise<SentimentSummary>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { scoreRecords } from "./scorer.js";
|
|
2
|
+
import { computeTrend, computeDivergence } from "./trends.js";
|
|
3
|
+
export class SentimentPipeline {
|
|
4
|
+
store;
|
|
5
|
+
config;
|
|
6
|
+
constructor(store, config) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
async processRecords(records, query) {
|
|
11
|
+
const warnings = [];
|
|
12
|
+
if (records.length === 0) {
|
|
13
|
+
return { fresh: [], trend: null, divergence: null, warnings };
|
|
14
|
+
}
|
|
15
|
+
// Check if store had prior data before this fetch
|
|
16
|
+
const priorSeries = this.store.getTimeSeries(query, { days: 30, bucketHours: 24 });
|
|
17
|
+
const hadPriorData = priorSeries.length >= 2;
|
|
18
|
+
// Score all records
|
|
19
|
+
const scored = scoreRecords(records);
|
|
20
|
+
// Insert into store
|
|
21
|
+
this.store.insert(scored);
|
|
22
|
+
// Compute trend from historical data (only if we had prior data)
|
|
23
|
+
let trend = null;
|
|
24
|
+
if (hadPriorData) {
|
|
25
|
+
const series = this.store.getTimeSeries(query, { days: 7, bucketHours: 24 });
|
|
26
|
+
if (series.length >= 2) {
|
|
27
|
+
trend = [computeTrend(series, "aggregate")];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Compute divergence from fresh records
|
|
31
|
+
let divergence = null;
|
|
32
|
+
const sourceGroups = groupBySource(scored);
|
|
33
|
+
const sourceStats = {};
|
|
34
|
+
for (const [source, recs] of Object.entries(sourceGroups)) {
|
|
35
|
+
// Exclude comments from divergence calculation
|
|
36
|
+
const postLevel = recs.filter((r) => !r.metadata.isComment);
|
|
37
|
+
if (postLevel.length > 0) {
|
|
38
|
+
const avg = postLevel.reduce((sum, r) => sum + r.sentiment.score, 0) / postLevel.length;
|
|
39
|
+
sourceStats[source] = { avg, count: postLevel.length };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (Object.keys(sourceStats).length >= 2) {
|
|
43
|
+
divergence = computeDivergence(sourceStats, this.config.divergenceThreshold);
|
|
44
|
+
}
|
|
45
|
+
return { fresh: scored, trend, divergence, warnings };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function groupBySource(records) {
|
|
49
|
+
const groups = {};
|
|
50
|
+
for (const r of records) {
|
|
51
|
+
if (!groups[r.source])
|
|
52
|
+
groups[r.source] = [];
|
|
53
|
+
groups[r.source].push(r);
|
|
54
|
+
}
|
|
55
|
+
return groups;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/sentiment/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SentinelRecord } from "./types.js";
|
|
2
|
+
interface ScoreResult {
|
|
3
|
+
score: number;
|
|
4
|
+
confidence: number;
|
|
5
|
+
tickers: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function keywordScore(record: SentinelRecord): ScoreResult;
|
|
8
|
+
export declare function scoreRecords(records: SentinelRecord[]): SentinelRecord[];
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { BULLISH_TERMS, BEARISH_TERMS } from "./keywords.js";
|
|
2
|
+
const TICKER_REGEX = /\$([A-Z]{1,5})\b/g;
|
|
3
|
+
export function keywordScore(record) {
|
|
4
|
+
const lower = record.text.toLowerCase();
|
|
5
|
+
const engagement = 1 + (record.engagement.score ?? 0);
|
|
6
|
+
let bullishWeight = 0;
|
|
7
|
+
let bearishWeight = 0;
|
|
8
|
+
let bullishCount = 0;
|
|
9
|
+
let bearishCount = 0;
|
|
10
|
+
for (const term of BULLISH_TERMS) {
|
|
11
|
+
if (lower.includes(term)) {
|
|
12
|
+
bullishCount++;
|
|
13
|
+
bullishWeight += engagement;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const term of BEARISH_TERMS) {
|
|
17
|
+
if (lower.includes(term)) {
|
|
18
|
+
bearishCount++;
|
|
19
|
+
bearishWeight += engagement;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const totalWeight = bullishWeight + bearishWeight;
|
|
23
|
+
const score = totalWeight === 0 ? 0 : (bullishWeight - bearishWeight) / totalWeight;
|
|
24
|
+
const totalMatches = bullishCount + bearishCount;
|
|
25
|
+
let confidence = 0;
|
|
26
|
+
if (totalMatches > 0) {
|
|
27
|
+
// Base confidence from keyword matches
|
|
28
|
+
confidence = Math.min(totalMatches / 5, 1) * 0.6;
|
|
29
|
+
// Boost for longer text
|
|
30
|
+
const textLength = record.text.length;
|
|
31
|
+
confidence += Math.min(textLength / 500, 1) * 0.3;
|
|
32
|
+
// Multiple matches boost
|
|
33
|
+
if (totalMatches >= 3)
|
|
34
|
+
confidence += 0.1;
|
|
35
|
+
// Twitter penalty
|
|
36
|
+
if (record.source === "twitter")
|
|
37
|
+
confidence -= 0.1;
|
|
38
|
+
// Clamp
|
|
39
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
40
|
+
}
|
|
41
|
+
// Extract tickers
|
|
42
|
+
const tickers = [];
|
|
43
|
+
for (const match of record.text.matchAll(TICKER_REGEX)) {
|
|
44
|
+
if (!tickers.includes(match[1])) {
|
|
45
|
+
tickers.push(match[1]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { score, confidence, tickers };
|
|
49
|
+
}
|
|
50
|
+
export function scoreRecords(records) {
|
|
51
|
+
return records.map((record) => {
|
|
52
|
+
const result = keywordScore(record);
|
|
53
|
+
return {
|
|
54
|
+
...record,
|
|
55
|
+
sentiment: {
|
|
56
|
+
score: result.score,
|
|
57
|
+
confidence: result.confidence,
|
|
58
|
+
method: "keyword",
|
|
59
|
+
tickers: result.tickers,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=scorer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scorer.js","sourceRoot":"","sources":["../../src/sentiment/scorer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SentinelRecord, SentimentSource, TrendBucket } from "./types.js";
|
|
2
|
+
export interface SearchOptions {
|
|
3
|
+
source?: SentimentSource;
|
|
4
|
+
since?: string;
|
|
5
|
+
until?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TimeSeriesOptions {
|
|
8
|
+
days: number;
|
|
9
|
+
bucketHours: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class SentimentStore {
|
|
12
|
+
private db;
|
|
13
|
+
constructor(pathOrMemory: string);
|
|
14
|
+
private initSchema;
|
|
15
|
+
getSchemaVersion(): number;
|
|
16
|
+
insert(records: SentinelRecord[]): void;
|
|
17
|
+
search(query: string, options?: SearchOptions): SentinelRecord[];
|
|
18
|
+
getByTicker(ticker: string, options?: {
|
|
19
|
+
since?: string;
|
|
20
|
+
}): SentinelRecord[];
|
|
21
|
+
getTimeSeries(query: string, options: TimeSeriesOptions): TrendBucket[];
|
|
22
|
+
prune(retentionDays: number): number;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
const SCHEMA_VERSION = 1;
|
|
3
|
+
const CREATE_SCHEMA = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
5
|
+
version INTEGER NOT NULL
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS sentinel_records (
|
|
9
|
+
id TEXT NOT NULL,
|
|
10
|
+
source TEXT NOT NULL,
|
|
11
|
+
source_id TEXT NOT NULL,
|
|
12
|
+
query TEXT NOT NULL,
|
|
13
|
+
title TEXT,
|
|
14
|
+
text TEXT NOT NULL,
|
|
15
|
+
author TEXT,
|
|
16
|
+
url TEXT NOT NULL,
|
|
17
|
+
published_at TEXT,
|
|
18
|
+
fetched_at TEXT NOT NULL,
|
|
19
|
+
engagement_json TEXT NOT NULL,
|
|
20
|
+
sentiment_score REAL NOT NULL,
|
|
21
|
+
sentiment_confidence REAL NOT NULL,
|
|
22
|
+
sentiment_method TEXT NOT NULL,
|
|
23
|
+
tickers_json TEXT NOT NULL,
|
|
24
|
+
metadata_json TEXT NOT NULL,
|
|
25
|
+
UNIQUE(source, source_id, fetched_at)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_source ON sentinel_records(source);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_fetched_at ON sentinel_records(fetched_at);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_sentinel_query ON sentinel_records(query);
|
|
31
|
+
|
|
32
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sentinel_fts USING fts5(
|
|
33
|
+
text, title, author, query, source,
|
|
34
|
+
content=sentinel_records,
|
|
35
|
+
content_rowid=rowid
|
|
36
|
+
);
|
|
37
|
+
`;
|
|
38
|
+
const TRIGGERS = `
|
|
39
|
+
CREATE TRIGGER IF NOT EXISTS sentinel_ai AFTER INSERT ON sentinel_records BEGIN
|
|
40
|
+
INSERT INTO sentinel_fts(rowid, text, title, author, query, source)
|
|
41
|
+
VALUES (new.rowid, new.text, new.title, new.author, new.query, new.source);
|
|
42
|
+
END;
|
|
43
|
+
|
|
44
|
+
CREATE TRIGGER IF NOT EXISTS sentinel_ad AFTER DELETE ON sentinel_records BEGIN
|
|
45
|
+
INSERT INTO sentinel_fts(sentinel_fts, rowid, text, title, author, query, source)
|
|
46
|
+
VALUES ('delete', old.rowid, old.text, old.title, old.author, old.query, old.source);
|
|
47
|
+
END;
|
|
48
|
+
`;
|
|
49
|
+
export class SentimentStore {
|
|
50
|
+
db;
|
|
51
|
+
constructor(pathOrMemory) {
|
|
52
|
+
this.db = new Database(pathOrMemory);
|
|
53
|
+
if (pathOrMemory !== ":memory:") {
|
|
54
|
+
this.db.pragma("journal_mode = WAL");
|
|
55
|
+
}
|
|
56
|
+
this.initSchema();
|
|
57
|
+
}
|
|
58
|
+
initSchema() {
|
|
59
|
+
this.db.exec(CREATE_SCHEMA);
|
|
60
|
+
this.db.exec(TRIGGERS);
|
|
61
|
+
const row = this.db.prepare("SELECT version FROM schema_version").get();
|
|
62
|
+
if (!row) {
|
|
63
|
+
this.db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
getSchemaVersion() {
|
|
67
|
+
const row = this.db.prepare("SELECT version FROM schema_version").get();
|
|
68
|
+
return row.version;
|
|
69
|
+
}
|
|
70
|
+
insert(records) {
|
|
71
|
+
const stmt = this.db.prepare(`
|
|
72
|
+
INSERT OR IGNORE INTO sentinel_records
|
|
73
|
+
(id, source, source_id, query, title, text, author, url,
|
|
74
|
+
published_at, fetched_at, engagement_json,
|
|
75
|
+
sentiment_score, sentiment_confidence, sentiment_method,
|
|
76
|
+
tickers_json, metadata_json)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
78
|
+
`);
|
|
79
|
+
const tx = this.db.transaction((recs) => {
|
|
80
|
+
for (const r of recs) {
|
|
81
|
+
stmt.run(r.id, r.source, r.sourceId, r.query, r.title, r.text, r.author, r.url, r.publishedAt, r.fetchedAt, JSON.stringify(r.engagement), r.sentiment.score, r.sentiment.confidence, r.sentiment.method, JSON.stringify(r.sentiment.tickers), JSON.stringify(r.metadata));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
tx(records);
|
|
85
|
+
}
|
|
86
|
+
search(query, options) {
|
|
87
|
+
let sql = `
|
|
88
|
+
SELECT sr.* FROM sentinel_records sr
|
|
89
|
+
JOIN sentinel_fts fts ON sr.rowid = fts.rowid
|
|
90
|
+
WHERE sentinel_fts MATCH ?
|
|
91
|
+
`;
|
|
92
|
+
const params = [query];
|
|
93
|
+
if (options?.source) {
|
|
94
|
+
sql += " AND sr.source = ?";
|
|
95
|
+
params.push(options.source);
|
|
96
|
+
}
|
|
97
|
+
if (options?.since) {
|
|
98
|
+
sql += " AND sr.fetched_at >= ?";
|
|
99
|
+
params.push(options.since);
|
|
100
|
+
}
|
|
101
|
+
if (options?.until) {
|
|
102
|
+
sql += " AND sr.fetched_at <= ?";
|
|
103
|
+
params.push(options.until);
|
|
104
|
+
}
|
|
105
|
+
sql += " ORDER BY bm25(sentinel_fts) LIMIT 100";
|
|
106
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
107
|
+
return rows.map(rowToRecord);
|
|
108
|
+
}
|
|
109
|
+
getByTicker(ticker, options) {
|
|
110
|
+
let sql = `SELECT * FROM sentinel_records WHERE tickers_json LIKE ?`;
|
|
111
|
+
const pattern = `%"${ticker}"%`;
|
|
112
|
+
const params = [pattern];
|
|
113
|
+
if (options?.since) {
|
|
114
|
+
sql += " AND fetched_at >= ?";
|
|
115
|
+
params.push(options.since);
|
|
116
|
+
}
|
|
117
|
+
sql += " ORDER BY fetched_at DESC LIMIT 100";
|
|
118
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
119
|
+
return rows.map(rowToRecord);
|
|
120
|
+
}
|
|
121
|
+
getTimeSeries(query, options) {
|
|
122
|
+
const since = new Date();
|
|
123
|
+
since.setDate(since.getDate() - options.days);
|
|
124
|
+
const sinceStr = since.toISOString();
|
|
125
|
+
const bucketSeconds = options.bucketHours * 3600;
|
|
126
|
+
const sql = `
|
|
127
|
+
SELECT
|
|
128
|
+
(CAST(strftime('%s', fetched_at) AS INTEGER) / ?) * ? AS bucket_ts,
|
|
129
|
+
AVG(sentiment_score) AS avg_score,
|
|
130
|
+
COUNT(*) AS cnt
|
|
131
|
+
FROM sentinel_records
|
|
132
|
+
WHERE query = ? AND fetched_at >= ?
|
|
133
|
+
GROUP BY bucket_ts
|
|
134
|
+
ORDER BY bucket_ts
|
|
135
|
+
`;
|
|
136
|
+
const rows = this.db.prepare(sql).all(bucketSeconds, bucketSeconds, query, sinceStr);
|
|
137
|
+
return rows.map((r) => ({
|
|
138
|
+
timestamp: new Date(r.bucket_ts * 1000).toISOString(),
|
|
139
|
+
avgScore: r.avg_score,
|
|
140
|
+
count: r.cnt,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
prune(retentionDays) {
|
|
144
|
+
const cutoff = new Date();
|
|
145
|
+
cutoff.setDate(cutoff.getDate() - retentionDays);
|
|
146
|
+
const result = this.db
|
|
147
|
+
.prepare("DELETE FROM sentinel_records WHERE fetched_at < ?")
|
|
148
|
+
.run(cutoff.toISOString());
|
|
149
|
+
return result.changes;
|
|
150
|
+
}
|
|
151
|
+
close() {
|
|
152
|
+
this.db.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function rowToRecord(row) {
|
|
156
|
+
return {
|
|
157
|
+
id: row.id,
|
|
158
|
+
source: row.source,
|
|
159
|
+
sourceId: row.source_id,
|
|
160
|
+
query: row.query,
|
|
161
|
+
title: row.title,
|
|
162
|
+
text: row.text,
|
|
163
|
+
author: row.author,
|
|
164
|
+
url: row.url,
|
|
165
|
+
publishedAt: row.published_at,
|
|
166
|
+
fetchedAt: row.fetched_at,
|
|
167
|
+
engagement: JSON.parse(row.engagement_json),
|
|
168
|
+
sentiment: {
|
|
169
|
+
score: row.sentiment_score,
|
|
170
|
+
confidence: row.sentiment_confidence,
|
|
171
|
+
method: row.sentiment_method,
|
|
172
|
+
tickers: JSON.parse(row.tickers_json),
|
|
173
|
+
},
|
|
174
|
+
metadata: JSON.parse(row.metadata_json),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/sentiment/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TrendBucket, TrendResult, DivergenceResult, SentimentSource } from "./types.js";
|
|
2
|
+
export declare function renderSparkline(values: number[]): string;
|
|
3
|
+
export declare function computeTrend(buckets: TrendBucket[], source: SentimentSource | "aggregate"): TrendResult;
|
|
4
|
+
export interface SourceStats {
|
|
5
|
+
avg: number;
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function computeDivergence(sources: {
|
|
9
|
+
twitter?: SourceStats;
|
|
10
|
+
reddit?: SourceStats;
|
|
11
|
+
web?: SourceStats;
|
|
12
|
+
finnhub?: SourceStats;
|
|
13
|
+
}, threshold: number): DivergenceResult;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const SPARKLINE_CHARS = "▁▂▃▄▅▆▇█";
|
|
2
|
+
export function renderSparkline(values) {
|
|
3
|
+
if (values.length === 0)
|
|
4
|
+
return "";
|
|
5
|
+
const min = Math.min(...values);
|
|
6
|
+
const max = Math.max(...values);
|
|
7
|
+
const range = max - min;
|
|
8
|
+
return values
|
|
9
|
+
.map((v) => {
|
|
10
|
+
if (range === 0)
|
|
11
|
+
return SPARKLINE_CHARS[3]; // middle block for flat
|
|
12
|
+
const idx = Math.round(((v - min) / range) * 7);
|
|
13
|
+
return SPARKLINE_CHARS[idx];
|
|
14
|
+
})
|
|
15
|
+
.join("");
|
|
16
|
+
}
|
|
17
|
+
export function computeTrend(buckets, source) {
|
|
18
|
+
const scores = buckets.map((b) => b.avgScore);
|
|
19
|
+
const totalCount = buckets.reduce((sum, b) => sum + b.count, 0);
|
|
20
|
+
const avgScore = totalCount === 0 ? 0 : buckets.reduce((sum, b) => sum + b.avgScore * b.count, 0) / totalCount;
|
|
21
|
+
const sparkline = renderSparkline(scores);
|
|
22
|
+
// Delta: last value minus first value
|
|
23
|
+
const delta = scores.length >= 2 ? scores[scores.length - 1] - scores[0] : 0;
|
|
24
|
+
let direction;
|
|
25
|
+
if (delta > 0.1)
|
|
26
|
+
direction = "rising";
|
|
27
|
+
else if (delta < -0.1)
|
|
28
|
+
direction = "falling";
|
|
29
|
+
else
|
|
30
|
+
direction = "stable";
|
|
31
|
+
return { source, sparkline, avgScore, count: totalCount, direction, delta };
|
|
32
|
+
}
|
|
33
|
+
export function computeDivergence(sources, threshold) {
|
|
34
|
+
const retailSources = [];
|
|
35
|
+
if (sources.twitter && sources.twitter.count >= 5)
|
|
36
|
+
retailSources.push(sources.twitter);
|
|
37
|
+
if (sources.reddit && sources.reddit.count >= 5)
|
|
38
|
+
retailSources.push(sources.reddit);
|
|
39
|
+
const newsSources = [];
|
|
40
|
+
if (sources.web && sources.web.count >= 5)
|
|
41
|
+
newsSources.push(sources.web);
|
|
42
|
+
if (sources.finnhub && sources.finnhub.count >= 5)
|
|
43
|
+
newsSources.push(sources.finnhub);
|
|
44
|
+
if (retailSources.length === 0 || newsSources.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
detected: false,
|
|
47
|
+
retailAvg: null,
|
|
48
|
+
newsAvg: null,
|
|
49
|
+
gap: null,
|
|
50
|
+
message: "Insufficient data for divergence analysis",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const retailAvg = retailSources.reduce((sum, s) => sum + s.avg, 0) / retailSources.length;
|
|
54
|
+
const newsAvg = newsSources.reduce((sum, s) => sum + s.avg, 0) / newsSources.length;
|
|
55
|
+
const gap = Math.abs(retailAvg - newsAvg);
|
|
56
|
+
if (gap > threshold) {
|
|
57
|
+
return {
|
|
58
|
+
detected: true,
|
|
59
|
+
retailAvg,
|
|
60
|
+
newsAvg,
|
|
61
|
+
gap,
|
|
62
|
+
message: `⚠ DIVERGENCE: Retail sentiment (${retailAvg >= 0 ? "+" : ""}${retailAvg.toFixed(2)}) vs news sentiment (${newsAvg >= 0 ? "+" : ""}${newsAvg.toFixed(2)}) — gap of ${gap.toFixed(2)}.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
detected: false,
|
|
67
|
+
retailAvg,
|
|
68
|
+
newsAvg,
|
|
69
|
+
gap,
|
|
70
|
+
message: "Sources broadly aligned",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=trends.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trends.js","sourceRoot":"","sources":["../../src/sentiment/trends.ts"],"names":[],"mappings":"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"}
|