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.
Files changed (154) hide show
  1. package/README.md +110 -87
  2. package/dist/analysts/orchestrator.js +1 -2
  3. package/dist/analysts/orchestrator.js.map +1 -1
  4. package/dist/config.d.ts +25 -5
  5. package/dist/config.js +16 -8
  6. package/dist/config.js.map +1 -1
  7. package/dist/infra/cache.d.ts +4 -0
  8. package/dist/infra/cache.js +4 -0
  9. package/dist/infra/cache.js.map +1 -1
  10. package/dist/infra/rate-limiter.js +6 -0
  11. package/dist/infra/rate-limiter.js.map +1 -1
  12. package/dist/onboarding/connect.d.ts +23 -0
  13. package/dist/onboarding/connect.js +107 -0
  14. package/dist/onboarding/connect.js.map +1 -0
  15. package/dist/onboarding/credential-interceptor.d.ts +44 -0
  16. package/dist/onboarding/credential-interceptor.js +72 -0
  17. package/dist/onboarding/credential-interceptor.js.map +1 -0
  18. package/dist/onboarding/degradation-accumulator.d.ts +21 -0
  19. package/dist/onboarding/degradation-accumulator.js +55 -0
  20. package/dist/onboarding/degradation-accumulator.js.map +1 -0
  21. package/dist/onboarding/prompt-user.d.ts +23 -0
  22. package/dist/onboarding/prompt-user.js +61 -0
  23. package/dist/onboarding/prompt-user.js.map +1 -0
  24. package/dist/onboarding/providers.d.ts +109 -0
  25. package/dist/onboarding/providers.js +236 -0
  26. package/dist/onboarding/providers.js.map +1 -0
  27. package/dist/onboarding/state.d.ts +31 -2
  28. package/dist/onboarding/state.js +141 -13
  29. package/dist/onboarding/state.js.map +1 -1
  30. package/dist/onboarding/tool-helpers.d.ts +34 -0
  31. package/dist/onboarding/tool-helpers.js +80 -0
  32. package/dist/onboarding/tool-helpers.js.map +1 -0
  33. package/dist/onboarding/tool-tags.d.ts +37 -0
  34. package/dist/onboarding/tool-tags.js +149 -0
  35. package/dist/onboarding/tool-tags.js.map +1 -0
  36. package/dist/onboarding/validation.d.ts +19 -0
  37. package/dist/onboarding/validation.js +117 -0
  38. package/dist/onboarding/validation.js.map +1 -0
  39. package/dist/pi/opencandle-extension.js +303 -4
  40. package/dist/pi/opencandle-extension.js.map +1 -1
  41. package/dist/pi/setup.d.ts +0 -1
  42. package/dist/pi/setup.js +66 -119
  43. package/dist/pi/setup.js.map +1 -1
  44. package/dist/prompts/context-builder.js +2 -1
  45. package/dist/prompts/context-builder.js.map +1 -1
  46. package/dist/providers/alpha-vantage.js +20 -1
  47. package/dist/providers/alpha-vantage.js.map +1 -1
  48. package/dist/providers/exa-search.d.ts +39 -0
  49. package/dist/providers/exa-search.js +276 -0
  50. package/dist/providers/exa-search.js.map +1 -0
  51. package/dist/providers/finnhub.d.ts +17 -0
  52. package/dist/providers/finnhub.js +94 -0
  53. package/dist/providers/finnhub.js.map +1 -0
  54. package/dist/providers/fred.js +13 -1
  55. package/dist/providers/fred.js.map +1 -1
  56. package/dist/providers/index.d.ts +2 -0
  57. package/dist/providers/index.js +1 -0
  58. package/dist/providers/index.js.map +1 -1
  59. package/dist/providers/provider-credential-error.d.ts +8 -0
  60. package/dist/providers/provider-credential-error.js +22 -0
  61. package/dist/providers/provider-credential-error.js.map +1 -0
  62. package/dist/providers/reddit.d.ts +8 -0
  63. package/dist/providers/reddit.js +36 -9
  64. package/dist/providers/reddit.js.map +1 -1
  65. package/dist/providers/twitter.js +2 -8
  66. package/dist/providers/twitter.js.map +1 -1
  67. package/dist/providers/web-search.d.ts +17 -0
  68. package/dist/providers/web-search.js +224 -0
  69. package/dist/providers/web-search.js.map +1 -0
  70. package/dist/providers/wrap-provider.d.ts +7 -0
  71. package/dist/providers/wrap-provider.js +15 -0
  72. package/dist/providers/wrap-provider.js.map +1 -1
  73. package/dist/routing/classify-intent.js +22 -0
  74. package/dist/routing/classify-intent.js.map +1 -1
  75. package/dist/runtime/session-coordinator.d.ts +0 -1
  76. package/dist/runtime/session-coordinator.js.map +1 -1
  77. package/dist/sentiment/adapters/finnhub.d.ts +7 -0
  78. package/dist/sentiment/adapters/finnhub.js +39 -0
  79. package/dist/sentiment/adapters/finnhub.js.map +1 -0
  80. package/dist/sentiment/adapters/reddit.d.ts +11 -0
  81. package/dist/sentiment/adapters/reddit.js +54 -0
  82. package/dist/sentiment/adapters/reddit.js.map +1 -0
  83. package/dist/sentiment/adapters/twitter.d.ts +9 -0
  84. package/dist/sentiment/adapters/twitter.js +32 -0
  85. package/dist/sentiment/adapters/twitter.js.map +1 -0
  86. package/dist/sentiment/adapters/web.d.ts +9 -0
  87. package/dist/sentiment/adapters/web.js +40 -0
  88. package/dist/sentiment/adapters/web.js.map +1 -0
  89. package/dist/sentiment/index.d.ts +16 -0
  90. package/dist/sentiment/index.js +44 -0
  91. package/dist/sentiment/index.js.map +1 -0
  92. package/dist/sentiment/keywords.d.ts +2 -0
  93. package/dist/sentiment/keywords.js +9 -0
  94. package/dist/sentiment/keywords.js.map +1 -0
  95. package/dist/sentiment/pipeline.d.ts +9 -0
  96. package/dist/sentiment/pipeline.js +57 -0
  97. package/dist/sentiment/pipeline.js.map +1 -0
  98. package/dist/sentiment/scorer.d.ts +9 -0
  99. package/dist/sentiment/scorer.js +64 -0
  100. package/dist/sentiment/scorer.js.map +1 -0
  101. package/dist/sentiment/store.d.ts +24 -0
  102. package/dist/sentiment/store.js +177 -0
  103. package/dist/sentiment/store.js.map +1 -0
  104. package/dist/sentiment/trends.d.ts +13 -0
  105. package/dist/sentiment/trends.js +73 -0
  106. package/dist/sentiment/trends.js.map +1 -0
  107. package/dist/sentiment/types.d.ts +66 -0
  108. package/dist/sentiment/types.js +54 -0
  109. package/dist/sentiment/types.js.map +1 -0
  110. package/dist/system-prompt.js +9 -1
  111. package/dist/system-prompt.js.map +1 -1
  112. package/dist/tools/fundamentals/company-overview.d.ts +3 -1
  113. package/dist/tools/fundamentals/company-overview.js +27 -27
  114. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  115. package/dist/tools/fundamentals/comps.js +45 -45
  116. package/dist/tools/fundamentals/comps.js.map +1 -1
  117. package/dist/tools/fundamentals/dcf.js +82 -82
  118. package/dist/tools/fundamentals/dcf.js.map +1 -1
  119. package/dist/tools/fundamentals/earnings.d.ts +3 -1
  120. package/dist/tools/fundamentals/earnings.js +25 -25
  121. package/dist/tools/fundamentals/earnings.js.map +1 -1
  122. package/dist/tools/fundamentals/financials.d.ts +3 -1
  123. package/dist/tools/fundamentals/financials.js +23 -23
  124. package/dist/tools/fundamentals/financials.js.map +1 -1
  125. package/dist/tools/index.js +8 -2
  126. package/dist/tools/index.js.map +1 -1
  127. package/dist/tools/interaction/ask-user.js +28 -64
  128. package/dist/tools/interaction/ask-user.js.map +1 -1
  129. package/dist/tools/macro/fred-data.d.ts +3 -1
  130. package/dist/tools/macro/fred-data.js +26 -26
  131. package/dist/tools/macro/fred-data.js.map +1 -1
  132. package/dist/tools/sentiment/reddit-sentiment.d.ts +3 -1
  133. package/dist/tools/sentiment/reddit-sentiment.js +107 -22
  134. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  135. package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
  136. package/dist/tools/sentiment/sentiment-summary.js +230 -0
  137. package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
  138. package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
  139. package/dist/tools/sentiment/sentiment-trend.js +39 -0
  140. package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
  141. package/dist/tools/sentiment/twitter-sentiment.js +17 -0
  142. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  143. package/dist/tools/sentiment/web-search.d.ts +11 -0
  144. package/dist/tools/sentiment/web-search.js +115 -0
  145. package/dist/tools/sentiment/web-search.js.map +1 -0
  146. package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
  147. package/dist/tools/sentiment/web-sentiment.js +66 -0
  148. package/dist/tools/sentiment/web-sentiment.js.map +1 -0
  149. package/dist/types/index.d.ts +1 -1
  150. package/dist/types/sentiment.d.ts +21 -0
  151. package/package.json +19 -3
  152. package/dist/tools/sentiment/news-sentiment.d.ts +0 -7
  153. package/dist/tools/sentiment/news-sentiment.js +0 -55
  154. 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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"}