@the-situation/indexer 0.17.1 → 0.18.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 (122) hide show
  1. package/dist/aggregator/cohort.d.ts +30 -0
  2. package/dist/aggregator/cohort.d.ts.map +1 -0
  3. package/dist/aggregator/cohort.js +153 -0
  4. package/dist/aggregator/cohort.js.map +1 -0
  5. package/dist/aggregator/daily.d.ts +71 -0
  6. package/dist/aggregator/daily.d.ts.map +1 -0
  7. package/dist/aggregator/daily.js +249 -0
  8. package/dist/aggregator/daily.js.map +1 -0
  9. package/dist/aggregator/domains.d.ts +20 -0
  10. package/dist/aggregator/domains.d.ts.map +1 -0
  11. package/dist/aggregator/domains.js +38 -0
  12. package/dist/aggregator/domains.js.map +1 -0
  13. package/dist/aggregator/index.d.ts +31 -0
  14. package/dist/aggregator/index.d.ts.map +1 -0
  15. package/dist/aggregator/index.js +100 -0
  16. package/dist/aggregator/index.js.map +1 -0
  17. package/dist/aggregator/lifetime.d.ts +52 -0
  18. package/dist/aggregator/lifetime.d.ts.map +1 -0
  19. package/dist/aggregator/lifetime.js +222 -0
  20. package/dist/aggregator/lifetime.js.map +1 -0
  21. package/dist/aggregator/roi.d.ts +42 -0
  22. package/dist/aggregator/roi.d.ts.map +1 -0
  23. package/dist/aggregator/roi.js +153 -0
  24. package/dist/aggregator/roi.js.map +1 -0
  25. package/dist/aggregator/sources.d.ts +59 -0
  26. package/dist/aggregator/sources.d.ts.map +1 -0
  27. package/dist/aggregator/sources.js +53 -0
  28. package/dist/aggregator/sources.js.map +1 -0
  29. package/dist/aggregator/writers.d.ts +22 -0
  30. package/dist/aggregator/writers.d.ts.map +1 -0
  31. package/dist/aggregator/writers.js +147 -0
  32. package/dist/aggregator/writers.js.map +1 -0
  33. package/dist/api/app.d.ts +177 -13
  34. package/dist/api/app.d.ts.map +1 -1
  35. package/dist/api/app.js +4 -3
  36. package/dist/api/app.js.map +1 -1
  37. package/dist/api/routes/admin-subscriptions.d.ts +10 -10
  38. package/dist/api/routes/analytics.d.ts +205 -0
  39. package/dist/api/routes/analytics.d.ts.map +1 -0
  40. package/dist/api/routes/analytics.js +122 -0
  41. package/dist/api/routes/analytics.js.map +1 -0
  42. package/dist/api/routes/index.d.ts +1 -0
  43. package/dist/api/routes/index.d.ts.map +1 -1
  44. package/dist/api/routes/index.js +1 -0
  45. package/dist/api/routes/index.js.map +1 -1
  46. package/dist/api/routes/lp-history.d.ts +1 -1
  47. package/dist/api/routes/rankings.d.ts +17 -3
  48. package/dist/api/routes/rankings.d.ts.map +1 -1
  49. package/dist/api/routes/rankings.js +44 -5
  50. package/dist/api/routes/rankings.js.map +1 -1
  51. package/dist/api/routes/trader-stats.d.ts +11 -2
  52. package/dist/api/routes/trader-stats.d.ts.map +1 -1
  53. package/dist/api/routes/trader-stats.js +72 -2
  54. package/dist/api/routes/trader-stats.js.map +1 -1
  55. package/dist/client/IndexerClient.d.ts +30 -1
  56. package/dist/client/IndexerClient.d.ts.map +1 -1
  57. package/dist/client/IndexerClient.js.map +1 -1
  58. package/dist/client/IndexerClientLive.d.ts.map +1 -1
  59. package/dist/client/IndexerClientLive.js +50 -1
  60. package/dist/client/IndexerClientLive.js.map +1 -1
  61. package/dist/client/convenience.d.ts +9 -2
  62. package/dist/client/convenience.d.ts.map +1 -1
  63. package/dist/client/convenience.js +9 -1
  64. package/dist/client/convenience.js.map +1 -1
  65. package/dist/client/index.d.ts +3 -2
  66. package/dist/client/index.d.ts.map +1 -1
  67. package/dist/client/index.js +1 -1
  68. package/dist/client/index.js.map +1 -1
  69. package/dist/config.d.ts +9 -0
  70. package/dist/config.d.ts.map +1 -1
  71. package/dist/config.js +2 -0
  72. package/dist/config.js.map +1 -1
  73. package/dist/etl/event-indexer.d.ts +1 -1
  74. package/dist/etl/lp-position-refresher.d.ts +2 -1
  75. package/dist/etl/lp-position-refresher.d.ts.map +1 -1
  76. package/dist/etl/lp-position-refresher.js +24 -11
  77. package/dist/etl/lp-position-refresher.js.map +1 -1
  78. package/dist/etl/position-refresher.d.ts +1 -1
  79. package/dist/etl/state-refresher.d.ts +1 -1
  80. package/dist/index.js +54 -5
  81. package/dist/index.js.map +1 -1
  82. package/dist/layers/ChainReaderLive.d.ts.map +1 -1
  83. package/dist/layers/ChainReaderLive.js +15 -0
  84. package/dist/layers/ChainReaderLive.js.map +1 -1
  85. package/dist/services/ChainReader.d.ts +10 -0
  86. package/dist/services/ChainReader.d.ts.map +1 -1
  87. package/dist/services/ChainReader.js.map +1 -1
  88. package/dist/types/analytics.d.ts +194 -0
  89. package/dist/types/analytics.d.ts.map +1 -0
  90. package/dist/types/analytics.js +10 -0
  91. package/dist/types/analytics.js.map +1 -0
  92. package/dist/types/index.d.ts +1 -0
  93. package/dist/types/index.d.ts.map +1 -1
  94. package/dist/warehouse/analytics-helpers.d.ts +36 -0
  95. package/dist/warehouse/analytics-helpers.d.ts.map +1 -0
  96. package/dist/warehouse/analytics-helpers.js +142 -0
  97. package/dist/warehouse/analytics-helpers.js.map +1 -0
  98. package/dist/warehouse/backfill.d.ts +26 -0
  99. package/dist/warehouse/backfill.d.ts.map +1 -0
  100. package/dist/warehouse/backfill.js +77 -0
  101. package/dist/warehouse/backfill.js.map +1 -0
  102. package/dist/warehouse/connection.d.ts +18 -0
  103. package/dist/warehouse/connection.d.ts.map +1 -0
  104. package/dist/warehouse/connection.js +24 -0
  105. package/dist/warehouse/connection.js.map +1 -0
  106. package/dist/warehouse/index.d.ts +14 -0
  107. package/dist/warehouse/index.d.ts.map +1 -0
  108. package/dist/warehouse/index.js +14 -0
  109. package/dist/warehouse/index.js.map +1 -0
  110. package/dist/warehouse/repositories/analytics.d.ts +61 -0
  111. package/dist/warehouse/repositories/analytics.d.ts.map +1 -0
  112. package/dist/warehouse/repositories/analytics.js +418 -0
  113. package/dist/warehouse/repositories/analytics.js.map +1 -0
  114. package/dist/warehouse/schema.d.ts +9 -0
  115. package/dist/warehouse/schema.d.ts.map +1 -0
  116. package/dist/warehouse/schema.js +219 -0
  117. package/dist/warehouse/schema.js.map +1 -0
  118. package/dist/warehouse/seed-domains.d.ts +29 -0
  119. package/dist/warehouse/seed-domains.d.ts.map +1 -0
  120. package/dist/warehouse/seed-domains.js +223 -0
  121. package/dist/warehouse/seed-domains.js.map +1 -0
  122. package/package.json +1 -1
@@ -0,0 +1,418 @@
1
+ import { computeCohortHalfLife, dateToUnixSeconds, parseNumeric, shapeCohortRows, toDateString, } from '../analytics-helpers';
2
+ export class PgAnalyticsRepository {
3
+ sql;
4
+ constructor(sql) {
5
+ this.sql = sql;
6
+ }
7
+ async getOverview() {
8
+ const [totals] = (await this.sql `
9
+ SELECT
10
+ (SELECT COUNT(*) FROM trader_lifetime) AS traders,
11
+ (SELECT COUNT(*) FROM lp_lifetime) AS lps,
12
+ (SELECT COALESCE(SUM(total_trades), 0) FROM trader_lifetime) AS trades,
13
+ (SELECT COALESCE(SUM(total_volume), 0) FROM trader_lifetime) AS volume,
14
+ (SELECT COUNT(DISTINCT market_address) FROM daily_market_stats) AS active_markets
15
+ `);
16
+ const [posRoi] = (await this.sql `
17
+ SELECT total_users, positive_roi_pct
18
+ FROM roi_summary
19
+ WHERE domain_slug = 'all' AND entity_type = 'trader'
20
+ `);
21
+ const sevenDaysAgo = new Date(Date.now() - 7 * 86_400_000)
22
+ .toISOString()
23
+ .slice(0, 10);
24
+ const [newUsers] = (await this.sql `
25
+ SELECT
26
+ COALESCE(SUM(new_traders), 0) AS new_traders,
27
+ COALESCE(SUM(new_lps), 0) AS new_lps
28
+ FROM daily_platform_stats
29
+ WHERE date >= ${sevenDaysAgo}::date
30
+ `);
31
+ const top = (await this.sql `
32
+ SELECT
33
+ d.slug, d.name,
34
+ COALESCE(SUM(s.volume_collateral), 0) AS volume,
35
+ COALESCE(SUM(s.trade_count), 0) AS trades,
36
+ COALESCE(MAX(s.unique_traders), 0) AS unique_traders
37
+ FROM domains d
38
+ LEFT JOIN daily_domain_stats s ON s.domain_slug = d.slug
39
+ WHERE d.slug <> 'other' OR EXISTS (SELECT 1 FROM daily_domain_stats x WHERE x.domain_slug = 'other')
40
+ GROUP BY d.slug, d.name
41
+ ORDER BY volume DESC
42
+ LIMIT 10
43
+ `);
44
+ return {
45
+ totals: {
46
+ traders: totals?.traders ?? 0,
47
+ lps: totals?.lps ?? 0,
48
+ trades: parseNumeric(totals?.trades ?? 0) ?? 0,
49
+ volume: parseNumeric(totals?.volume ?? 0) ?? 0,
50
+ activeMarkets: totals?.active_markets ?? 0,
51
+ },
52
+ positiveRoiPct: parseNumeric(posRoi?.positive_roi_pct) ?? 0,
53
+ newTradersLast7d: parseNumeric(newUsers?.new_traders) ?? 0,
54
+ newLpsLast7d: parseNumeric(newUsers?.new_lps) ?? 0,
55
+ topDomainsByVolume: top.map((r) => ({
56
+ slug: r.slug,
57
+ name: r.name,
58
+ volume: parseNumeric(r.volume) ?? 0,
59
+ trades: parseNumeric(r.trades) ?? 0,
60
+ uniqueTraders: parseNumeric(r.unique_traders) ?? 0,
61
+ })),
62
+ computedAt: new Date().toISOString(),
63
+ };
64
+ }
65
+ async getDomains() {
66
+ const rows = (await this.sql `
67
+ SELECT
68
+ d.slug,
69
+ d.name,
70
+ d.description,
71
+ COALESCE(md.active_markets, 0) AS active_markets,
72
+ COALESCE(s.unique_traders, 0) AS unique_traders,
73
+ COALESCE(s.unique_lps, 0) AS unique_lps,
74
+ COALESCE(s.total_volume, 0) AS total_volume,
75
+ COALESCE(s.total_trades, 0) AS total_trades,
76
+ COALESCE(r.positive_roi_pct, 0) AS positive_roi_pct,
77
+ r.median_roi_pct
78
+ FROM domains d
79
+ LEFT JOIN (
80
+ SELECT domain_slug,
81
+ SUM(volume_collateral) AS total_volume,
82
+ SUM(trade_count) AS total_trades,
83
+ MAX(unique_traders) AS unique_traders,
84
+ MAX(unique_lps) AS unique_lps
85
+ FROM daily_domain_stats
86
+ GROUP BY domain_slug
87
+ ) s ON s.domain_slug = d.slug
88
+ LEFT JOIN (
89
+ SELECT domain_slug, COUNT(DISTINCT market_address) AS active_markets
90
+ FROM market_domains
91
+ GROUP BY domain_slug
92
+ ) md ON md.domain_slug = d.slug
93
+ LEFT JOIN roi_summary r ON r.domain_slug = d.slug AND r.entity_type = 'trader'
94
+ ORDER BY d.sort_order ASC, d.slug ASC
95
+ `);
96
+ const domains = rows.map((r) => ({
97
+ slug: r.slug,
98
+ name: r.name,
99
+ description: r.description,
100
+ activeMarkets: r.active_markets,
101
+ uniqueTraders: parseNumeric(r.unique_traders) ?? 0,
102
+ uniqueLps: parseNumeric(r.unique_lps) ?? 0,
103
+ totalVolume: parseNumeric(r.total_volume) ?? 0,
104
+ totalTrades: parseNumeric(r.total_trades) ?? 0,
105
+ positiveRoiPct: parseNumeric(r.positive_roi_pct) ?? 0,
106
+ medianRoiPct: parseNumeric(r.median_roi_pct),
107
+ }));
108
+ return { domains };
109
+ }
110
+ async getDomainDetail(slug) {
111
+ const summaries = await this.getDomains();
112
+ const summary = summaries.domains.find((d) => d.slug === slug);
113
+ if (!summary)
114
+ return null;
115
+ const ts = (await this.sql `
116
+ SELECT date, trade_count, volume_collateral, unique_traders, unique_lps, liquidity_net
117
+ FROM daily_domain_stats
118
+ WHERE domain_slug = ${slug}
119
+ ORDER BY date ASC
120
+ `);
121
+ const topTraders = (await this.sql `
122
+ SELECT
123
+ t.trader,
124
+ t.total_pnl,
125
+ t.total_trades,
126
+ t.total_volume,
127
+ t.roi_pct
128
+ FROM trader_lifetime t
129
+ WHERE t.domains_json @> ${JSON.stringify([slug])}::jsonb
130
+ ORDER BY t.total_pnl DESC NULLS LAST
131
+ LIMIT 25
132
+ `);
133
+ const topMarkets = (await this.sql `
134
+ SELECT
135
+ m.market_address,
136
+ SUM(m.volume_collateral) AS volume,
137
+ SUM(m.trade_count) AS trades,
138
+ MAX(m.unique_traders) AS unique_traders
139
+ FROM daily_market_stats m
140
+ JOIN market_domains md ON md.market_address = m.market_address
141
+ WHERE md.domain_slug = ${slug}
142
+ GROUP BY m.market_address
143
+ ORDER BY volume DESC
144
+ LIMIT 25
145
+ `);
146
+ return {
147
+ ...summary,
148
+ timeSeries: ts.map((r) => ({
149
+ date: toDateString(r.date),
150
+ trades: parseNumeric(r.trade_count) ?? 0,
151
+ volume: parseNumeric(r.volume_collateral) ?? 0,
152
+ uniqueTraders: parseNumeric(r.unique_traders) ?? 0,
153
+ uniqueLps: parseNumeric(r.unique_lps) ?? 0,
154
+ liquidityNet: parseNumeric(r.liquidity_net) ?? 0,
155
+ })),
156
+ topTraders: topTraders.map((r) => ({
157
+ trader: r.trader,
158
+ totalPnl: parseNumeric(r.total_pnl) ?? 0,
159
+ totalTrades: r.total_trades,
160
+ volume: parseNumeric(r.total_volume) ?? 0,
161
+ roiPct: parseNumeric(r.roi_pct),
162
+ })),
163
+ topMarkets: topMarkets.map((r) => ({
164
+ marketAddress: r.market_address,
165
+ volume: parseNumeric(r.volume) ?? 0,
166
+ trades: parseNumeric(r.trades) ?? 0,
167
+ uniqueTraders: parseNumeric(r.unique_traders) ?? 0,
168
+ })),
169
+ };
170
+ }
171
+ async getCohorts(filters) {
172
+ const rows = (await this.sql `
173
+ SELECT cohort_week, week_offset, cohort_size, retained
174
+ FROM cohort_retention
175
+ WHERE entity_type = ${filters.entityType}
176
+ AND domain_slug = ${filters.domain}
177
+ ORDER BY cohort_week ASC, week_offset ASC
178
+ `);
179
+ const cohorts = shapeCohortRows(rows);
180
+ const halfLives = [];
181
+ for (const c of cohorts) {
182
+ const hl = computeCohortHalfLife(c);
183
+ if (hl != null)
184
+ halfLives.push(hl);
185
+ }
186
+ halfLives.sort((a, b) => a - b);
187
+ const median = halfLives.length === 0
188
+ ? null
189
+ : halfLives[Math.floor(halfLives.length / 2)] ?? null;
190
+ return {
191
+ entityType: filters.entityType,
192
+ domain: filters.domain,
193
+ cohorts,
194
+ medianHalfLifeWeeks: median,
195
+ };
196
+ }
197
+ async getRoi(filters) {
198
+ const [summary] = (await this.sql `
199
+ SELECT total_users, positive_roi_users, positive_roi_pct,
200
+ median_roi_pct, p25_roi_pct, p75_roi_pct, p90_roi_pct, mean_roi_pct
201
+ FROM roi_summary
202
+ WHERE entity_type = ${filters.entityType} AND domain_slug = ${filters.domain}
203
+ `);
204
+ const histogram = (await this.sql `
205
+ SELECT bucket_index, bucket_lower_pct, bucket_upper_pct, user_count
206
+ FROM roi_distribution
207
+ WHERE entity_type = ${filters.entityType} AND domain_slug = ${filters.domain}
208
+ ORDER BY bucket_index ASC
209
+ `);
210
+ return {
211
+ entityType: filters.entityType,
212
+ domain: filters.domain,
213
+ summary: {
214
+ totalUsers: summary?.total_users ?? 0,
215
+ positiveRoiUsers: summary?.positive_roi_users ?? 0,
216
+ positiveRoiPct: parseNumeric(summary?.positive_roi_pct) ?? 0,
217
+ medianRoiPct: parseNumeric(summary?.median_roi_pct ?? null),
218
+ p25RoiPct: parseNumeric(summary?.p25_roi_pct ?? null),
219
+ p75RoiPct: parseNumeric(summary?.p75_roi_pct ?? null),
220
+ p90RoiPct: parseNumeric(summary?.p90_roi_pct ?? null),
221
+ meanRoiPct: parseNumeric(summary?.mean_roi_pct ?? null),
222
+ },
223
+ histogram: histogram.map((b) => ({
224
+ bucketIndex: b.bucket_index,
225
+ bucketLowerPct: parseNumeric(b.bucket_lower_pct) ?? 0,
226
+ bucketUpperPct: parseNumeric(b.bucket_upper_pct) ?? 0,
227
+ userCount: b.user_count,
228
+ })),
229
+ };
230
+ }
231
+ async getLeaderboard(filters) {
232
+ // Two paths: lifetime (no time-window) vs windowed (sums over daily facts).
233
+ const isWindowed = filters.from != null || filters.to != null;
234
+ const isDomainAll = filters.domain === 'all';
235
+ let entries;
236
+ if (!isWindowed && isDomainAll) {
237
+ // Fast path — lifetime totals.
238
+ const rows = (await this.sql `
239
+ SELECT trader, total_pnl, total_volume, total_trades,
240
+ markets_traded, domains_traded, roi_pct
241
+ FROM trader_lifetime
242
+ ORDER BY total_pnl DESC NULLS LAST
243
+ LIMIT ${filters.limit}
244
+ `);
245
+ entries = rows.map((r, i) => ({
246
+ rank: i + 1,
247
+ trader: r.trader,
248
+ totalPnl: parseNumeric(r.total_pnl) ?? 0,
249
+ totalVolume: parseNumeric(r.total_volume) ?? 0,
250
+ totalTrades: r.total_trades,
251
+ marketsTraded: r.markets_traded,
252
+ domainsTraded: r.domains_traded,
253
+ roiPct: parseNumeric(r.roi_pct),
254
+ }));
255
+ }
256
+ else if (!isWindowed && !isDomainAll) {
257
+ // Domain filter, lifetime — use daily_trader_stats to filter, then join lifetime.
258
+ const rows = (await this.sql `
259
+ SELECT
260
+ t.trader,
261
+ t.total_pnl,
262
+ SUM(s.volume_collateral) AS total_volume,
263
+ SUM(s.trade_count) AS total_trades,
264
+ COUNT(DISTINCT s.date) AS active_days,
265
+ t.markets_traded,
266
+ t.domains_traded,
267
+ t.roi_pct
268
+ FROM daily_trader_stats s
269
+ JOIN trader_lifetime t ON t.trader = s.trader
270
+ WHERE s.domain_slug = ${filters.domain}
271
+ GROUP BY t.trader, t.total_pnl, t.markets_traded, t.domains_traded, t.roi_pct
272
+ ORDER BY t.total_pnl DESC NULLS LAST
273
+ LIMIT ${filters.limit}
274
+ `);
275
+ entries = rows.map((r, i) => ({
276
+ rank: i + 1,
277
+ trader: r.trader,
278
+ totalPnl: parseNumeric(r.total_pnl) ?? 0,
279
+ totalVolume: parseNumeric(r.total_volume) ?? 0,
280
+ totalTrades: parseNumeric(r.total_trades) ?? 0,
281
+ marketsTraded: r.markets_traded,
282
+ domainsTraded: r.domains_traded,
283
+ roiPct: parseNumeric(r.roi_pct),
284
+ }));
285
+ }
286
+ else {
287
+ // Windowed — sum daily facts within (from, to). Per-trader P&L isn't
288
+ // computable from daily facts (no per-day P&L bucket), so we sort by
289
+ // windowed VOLUME and report the trader's lifetime P&L alongside.
290
+ const rows = (await this.sql `
291
+ SELECT
292
+ s.trader,
293
+ SUM(s.volume_collateral) AS total_volume,
294
+ SUM(s.trade_count) AS total_trades,
295
+ COALESCE(t.total_pnl, 0) AS total_pnl,
296
+ t.roi_pct AS roi_pct,
297
+ COALESCE(t.markets_traded, 0) AS markets_traded,
298
+ COALESCE(t.domains_traded, 0) AS domains_traded
299
+ FROM daily_trader_stats s
300
+ LEFT JOIN trader_lifetime t ON t.trader = s.trader
301
+ WHERE (${filters.domain}::text = 'all' OR s.domain_slug = ${filters.domain})
302
+ AND (${filters.from}::date IS NULL OR s.date >= ${filters.from}::date)
303
+ AND (${filters.to}::date IS NULL OR s.date < ${filters.to}::date)
304
+ GROUP BY s.trader, t.total_pnl, t.roi_pct, t.markets_traded, t.domains_traded
305
+ ORDER BY total_volume DESC
306
+ LIMIT ${filters.limit}
307
+ `);
308
+ entries = rows.map((r, i) => ({
309
+ rank: i + 1,
310
+ trader: r.trader,
311
+ totalPnl: parseNumeric(r.total_pnl) ?? 0,
312
+ totalVolume: parseNumeric(r.total_volume) ?? 0,
313
+ totalTrades: parseNumeric(r.total_trades) ?? 0,
314
+ marketsTraded: r.markets_traded,
315
+ domainsTraded: r.domains_traded,
316
+ roiPct: parseNumeric(r.roi_pct),
317
+ }));
318
+ }
319
+ return {
320
+ entries,
321
+ domain: filters.domain,
322
+ windowFrom: filters.from,
323
+ windowTo: filters.to,
324
+ };
325
+ }
326
+ async getTraderAnalytics(trader) {
327
+ const traderLower = trader.toLowerCase();
328
+ const [lifetime] = (await this.sql `
329
+ SELECT trader, first_trade_at, last_trade_at, cohort_week,
330
+ total_trades, buy_count, sell_count, markets_traded,
331
+ domains_traded, domains_json, total_volume, total_collateral_at_risk,
332
+ realized_pnl, unrealized_pnl, total_pnl, roi_pct, active_days
333
+ FROM trader_lifetime
334
+ WHERE LOWER(trader) = ${traderLower}
335
+ `);
336
+ const perDomain = (await this.sql `
337
+ SELECT
338
+ s.domain_slug AS slug,
339
+ d.name,
340
+ SUM(s.trade_count) AS trade_count,
341
+ SUM(s.volume_collateral) AS volume,
342
+ MAX(s.markets_traded) AS markets_traded,
343
+ COUNT(DISTINCT s.date) AS days_active
344
+ FROM daily_trader_stats s
345
+ JOIN domains d ON d.slug = s.domain_slug
346
+ WHERE LOWER(s.trader) = ${traderLower}
347
+ GROUP BY s.domain_slug, d.name, d.sort_order
348
+ ORDER BY d.sort_order ASC
349
+ `);
350
+ let cohort = null;
351
+ if (lifetime?.cohort_week) {
352
+ const cohortWeek = toDateString(lifetime.cohort_week);
353
+ const cohortRows = (await this.sql `
354
+ SELECT cohort_week, week_offset, cohort_size, retained
355
+ FROM cohort_retention
356
+ WHERE entity_type = 'trader' AND domain_slug = 'all'
357
+ AND cohort_week = ${cohortWeek}::date
358
+ ORDER BY week_offset ASC
359
+ `);
360
+ const shaped = shapeCohortRows(cohortRows);
361
+ const halfLife = shaped[0] ? computeCohortHalfLife(shaped[0]) : null;
362
+ const latest = cohortRows[cohortRows.length - 1];
363
+ cohort = {
364
+ cohortWeek,
365
+ halfLifeWeeks: halfLife,
366
+ cohortSize: cohortRows[0]?.cohort_size ?? null,
367
+ retainedThisWeek: latest?.retained ?? null,
368
+ };
369
+ }
370
+ return {
371
+ trader,
372
+ lifetime: lifetime
373
+ ? {
374
+ firstTradeAt: dateToUnixSeconds(lifetime.first_trade_at),
375
+ lastTradeAt: dateToUnixSeconds(lifetime.last_trade_at),
376
+ cohortWeek: lifetime.cohort_week ? toDateString(lifetime.cohort_week) : null,
377
+ totalTrades: lifetime.total_trades,
378
+ buyCount: lifetime.buy_count,
379
+ sellCount: lifetime.sell_count,
380
+ marketsTraded: lifetime.markets_traded,
381
+ domainsTraded: lifetime.domains_traded,
382
+ domains: parseDomainsJson(lifetime.domains_json),
383
+ totalVolume: parseNumeric(lifetime.total_volume) ?? 0,
384
+ totalCollateralAtRisk: parseNumeric(lifetime.total_collateral_at_risk) ?? 0,
385
+ realizedPnl: parseNumeric(lifetime.realized_pnl) ?? 0,
386
+ unrealizedPnl: parseNumeric(lifetime.unrealized_pnl) ?? 0,
387
+ totalPnl: parseNumeric(lifetime.total_pnl) ?? 0,
388
+ roiPct: parseNumeric(lifetime.roi_pct),
389
+ activeDays: lifetime.active_days,
390
+ }
391
+ : null,
392
+ perDomain: perDomain.map((r) => ({
393
+ slug: r.slug,
394
+ name: r.name,
395
+ tradeCount: parseNumeric(r.trade_count) ?? 0,
396
+ volume: parseNumeric(r.volume) ?? 0,
397
+ marketsTraded: parseNumeric(r.markets_traded) ?? 0,
398
+ daysActive: parseNumeric(r.days_active) ?? 0,
399
+ })),
400
+ cohort,
401
+ };
402
+ }
403
+ }
404
+ function parseDomainsJson(raw) {
405
+ if (Array.isArray(raw))
406
+ return raw;
407
+ if (typeof raw === 'string') {
408
+ try {
409
+ const parsed = JSON.parse(raw);
410
+ return Array.isArray(parsed) ? parsed : [];
411
+ }
412
+ catch {
413
+ return [];
414
+ }
415
+ }
416
+ return [];
417
+ }
418
+ //# sourceMappingURL=analytics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../../src/warehouse/repositories/analytics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Warehouse DDL — domain taxonomy + analytics fact / lifetime / cohort / ROI tables.
3
+ *
4
+ * Idempotent: safe to run on every boot. Postgres handles `IF NOT EXISTS` cleanly,
5
+ * so this acts as both initial creation and a lightweight migration runner.
6
+ */
7
+ import type { SQL } from 'bun';
8
+ export declare function initializeWarehouseSchema(sql: SQL): Promise<void>;
9
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/warehouse/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AA8O/B,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvE"}
@@ -0,0 +1,219 @@
1
+ const SCHEMA_DDL = [
2
+ // ──────────────────────────────────────────────────────────────────────
3
+ // Domains — canonical taxonomy
4
+ // ──────────────────────────────────────────────────────────────────────
5
+ `CREATE TABLE IF NOT EXISTS domains (
6
+ slug TEXT PRIMARY KEY,
7
+ name TEXT NOT NULL,
8
+ description TEXT NOT NULL DEFAULT '',
9
+ sort_order INTEGER NOT NULL DEFAULT 0,
10
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
11
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
12
+ )`,
13
+ `CREATE TABLE IF NOT EXISTS market_domains (
14
+ market_address TEXT NOT NULL,
15
+ domain_slug TEXT NOT NULL REFERENCES domains(slug) ON DELETE CASCADE,
16
+ is_primary BOOLEAN NOT NULL DEFAULT FALSE,
17
+ source TEXT NOT NULL DEFAULT 'auto',
18
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
19
+ PRIMARY KEY (market_address, domain_slug)
20
+ )`,
21
+ `CREATE INDEX IF NOT EXISTS idx_market_domains_domain
22
+ ON market_domains(domain_slug)`,
23
+ `CREATE INDEX IF NOT EXISTS idx_market_domains_market
24
+ ON market_domains(market_address)`,
25
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_market_domains_one_primary
26
+ ON market_domains(market_address) WHERE is_primary`,
27
+ // ──────────────────────────────────────────────────────────────────────
28
+ // Daily fact tables — date × dimension × metrics
29
+ // ──────────────────────────────────────────────────────────────────────
30
+ `CREATE TABLE IF NOT EXISTS daily_trader_stats (
31
+ date DATE NOT NULL,
32
+ trader TEXT NOT NULL,
33
+ domain_slug TEXT NOT NULL REFERENCES domains(slug),
34
+ trade_count INTEGER NOT NULL DEFAULT 0,
35
+ buy_count INTEGER NOT NULL DEFAULT 0,
36
+ sell_count INTEGER NOT NULL DEFAULT 0,
37
+ volume_collateral NUMERIC(38, 6) NOT NULL DEFAULT 0,
38
+ realized_pnl NUMERIC(38, 6) NOT NULL DEFAULT 0,
39
+ unrealized_pnl NUMERIC(38, 6),
40
+ markets_traded INTEGER NOT NULL DEFAULT 0,
41
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
42
+ PRIMARY KEY (date, trader, domain_slug)
43
+ )`,
44
+ `CREATE INDEX IF NOT EXISTS idx_daily_trader_stats_trader
45
+ ON daily_trader_stats(trader, date DESC)`,
46
+ `CREATE INDEX IF NOT EXISTS idx_daily_trader_stats_domain_date
47
+ ON daily_trader_stats(domain_slug, date DESC)`,
48
+ `CREATE TABLE IF NOT EXISTS daily_lp_stats (
49
+ date DATE NOT NULL,
50
+ provider TEXT NOT NULL,
51
+ domain_slug TEXT NOT NULL REFERENCES domains(slug),
52
+ deposit_count INTEGER NOT NULL DEFAULT 0,
53
+ withdraw_count INTEGER NOT NULL DEFAULT 0,
54
+ deposited NUMERIC(38, 6) NOT NULL DEFAULT 0,
55
+ withdrawn NUMERIC(38, 6) NOT NULL DEFAULT 0,
56
+ net_position NUMERIC(38, 6) NOT NULL DEFAULT 0,
57
+ unrealized_pnl NUMERIC(38, 6),
58
+ markets_active INTEGER NOT NULL DEFAULT 0,
59
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
60
+ PRIMARY KEY (date, provider, domain_slug)
61
+ )`,
62
+ `CREATE INDEX IF NOT EXISTS idx_daily_lp_stats_provider
63
+ ON daily_lp_stats(provider, date DESC)`,
64
+ `CREATE INDEX IF NOT EXISTS idx_daily_lp_stats_domain_date
65
+ ON daily_lp_stats(domain_slug, date DESC)`,
66
+ `CREATE TABLE IF NOT EXISTS daily_market_stats (
67
+ date DATE NOT NULL,
68
+ market_address TEXT NOT NULL,
69
+ trade_count INTEGER NOT NULL DEFAULT 0,
70
+ unique_traders INTEGER NOT NULL DEFAULT 0,
71
+ volume_collateral NUMERIC(38, 6) NOT NULL DEFAULT 0,
72
+ liquidity_added NUMERIC(38, 6) NOT NULL DEFAULT 0,
73
+ liquidity_removed NUMERIC(38, 6) NOT NULL DEFAULT 0,
74
+ unique_lps INTEGER NOT NULL DEFAULT 0,
75
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
76
+ PRIMARY KEY (date, market_address)
77
+ )`,
78
+ `CREATE INDEX IF NOT EXISTS idx_daily_market_stats_market
79
+ ON daily_market_stats(market_address, date DESC)`,
80
+ `CREATE TABLE IF NOT EXISTS daily_domain_stats (
81
+ date DATE NOT NULL,
82
+ domain_slug TEXT NOT NULL REFERENCES domains(slug),
83
+ trade_count INTEGER NOT NULL DEFAULT 0,
84
+ unique_traders INTEGER NOT NULL DEFAULT 0,
85
+ unique_lps INTEGER NOT NULL DEFAULT 0,
86
+ volume_collateral NUMERIC(38, 6) NOT NULL DEFAULT 0,
87
+ liquidity_net NUMERIC(38, 6) NOT NULL DEFAULT 0,
88
+ active_markets INTEGER NOT NULL DEFAULT 0,
89
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
90
+ PRIMARY KEY (date, domain_slug)
91
+ )`,
92
+ `CREATE INDEX IF NOT EXISTS idx_daily_domain_stats_date
93
+ ON daily_domain_stats(date DESC)`,
94
+ `CREATE TABLE IF NOT EXISTS daily_platform_stats (
95
+ date DATE PRIMARY KEY,
96
+ trade_count INTEGER NOT NULL DEFAULT 0,
97
+ unique_traders INTEGER NOT NULL DEFAULT 0,
98
+ unique_lps INTEGER NOT NULL DEFAULT 0,
99
+ new_traders INTEGER NOT NULL DEFAULT 0,
100
+ new_lps INTEGER NOT NULL DEFAULT 0,
101
+ volume_collateral NUMERIC(38, 6) NOT NULL DEFAULT 0,
102
+ liquidity_net NUMERIC(38, 6) NOT NULL DEFAULT 0,
103
+ active_markets INTEGER NOT NULL DEFAULT 0,
104
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
105
+ )`,
106
+ // ──────────────────────────────────────────────────────────────────────
107
+ // Lifetime / CLTV — one row per entity
108
+ // ──────────────────────────────────────────────────────────────────────
109
+ `CREATE TABLE IF NOT EXISTS trader_lifetime (
110
+ trader TEXT PRIMARY KEY,
111
+ first_trade_at TIMESTAMPTZ,
112
+ last_trade_at TIMESTAMPTZ,
113
+ cohort_week DATE,
114
+ total_trades INTEGER NOT NULL DEFAULT 0,
115
+ buy_count INTEGER NOT NULL DEFAULT 0,
116
+ sell_count INTEGER NOT NULL DEFAULT 0,
117
+ markets_traded INTEGER NOT NULL DEFAULT 0,
118
+ domains_traded INTEGER NOT NULL DEFAULT 0,
119
+ domains_json JSONB NOT NULL DEFAULT '[]'::jsonb,
120
+ total_volume NUMERIC(38, 6) NOT NULL DEFAULT 0,
121
+ total_collateral_at_risk NUMERIC(38, 6) NOT NULL DEFAULT 0,
122
+ realized_pnl NUMERIC(38, 6) NOT NULL DEFAULT 0,
123
+ unrealized_pnl NUMERIC(38, 6) NOT NULL DEFAULT 0,
124
+ total_pnl NUMERIC(38, 6) NOT NULL DEFAULT 0,
125
+ roi_pct NUMERIC(10, 4),
126
+ active_days INTEGER NOT NULL DEFAULT 0,
127
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
128
+ )`,
129
+ `CREATE INDEX IF NOT EXISTS idx_trader_lifetime_pnl
130
+ ON trader_lifetime(total_pnl DESC)`,
131
+ `CREATE INDEX IF NOT EXISTS idx_trader_lifetime_roi
132
+ ON trader_lifetime(roi_pct DESC NULLS LAST)`,
133
+ `CREATE INDEX IF NOT EXISTS idx_trader_lifetime_cohort
134
+ ON trader_lifetime(cohort_week)`,
135
+ `CREATE INDEX IF NOT EXISTS idx_trader_lifetime_volume
136
+ ON trader_lifetime(total_volume DESC)`,
137
+ `CREATE TABLE IF NOT EXISTS lp_lifetime (
138
+ provider TEXT PRIMARY KEY,
139
+ first_deposit_at TIMESTAMPTZ,
140
+ last_activity_at TIMESTAMPTZ,
141
+ cohort_week DATE,
142
+ deposit_count INTEGER NOT NULL DEFAULT 0,
143
+ withdraw_count INTEGER NOT NULL DEFAULT 0,
144
+ total_deposited NUMERIC(38, 6) NOT NULL DEFAULT 0,
145
+ total_withdrawn NUMERIC(38, 6) NOT NULL DEFAULT 0,
146
+ net_position NUMERIC(38, 6) NOT NULL DEFAULT 0,
147
+ current_value NUMERIC(38, 6),
148
+ unrealized_pnl NUMERIC(38, 6),
149
+ realized_pnl NUMERIC(38, 6) NOT NULL DEFAULT 0,
150
+ markets_provided INTEGER NOT NULL DEFAULT 0,
151
+ domains_provided INTEGER NOT NULL DEFAULT 0,
152
+ domains_json JSONB NOT NULL DEFAULT '[]'::jsonb,
153
+ active_days INTEGER NOT NULL DEFAULT 0,
154
+ roi_pct NUMERIC(10, 4),
155
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
156
+ )`,
157
+ `CREATE INDEX IF NOT EXISTS idx_lp_lifetime_pnl
158
+ ON lp_lifetime(unrealized_pnl DESC)`,
159
+ `CREATE INDEX IF NOT EXISTS idx_lp_lifetime_cohort
160
+ ON lp_lifetime(cohort_week)`,
161
+ // ──────────────────────────────────────────────────────────────────────
162
+ // Cohort retention — week × week_offset × domain
163
+ // ──────────────────────────────────────────────────────────────────────
164
+ `CREATE TABLE IF NOT EXISTS cohort_retention (
165
+ cohort_week DATE NOT NULL,
166
+ week_offset INTEGER NOT NULL,
167
+ domain_slug TEXT NOT NULL,
168
+ entity_type TEXT NOT NULL,
169
+ cohort_size INTEGER NOT NULL,
170
+ retained INTEGER NOT NULL,
171
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
172
+ PRIMARY KEY (cohort_week, week_offset, domain_slug, entity_type)
173
+ )`,
174
+ `CREATE INDEX IF NOT EXISTS idx_cohort_retention_lookup
175
+ ON cohort_retention(domain_slug, entity_type, cohort_week)`,
176
+ // ──────────────────────────────────────────────────────────────────────
177
+ // ROI distribution — bucketed histograms + summary stats
178
+ // ──────────────────────────────────────────────────────────────────────
179
+ `CREATE TABLE IF NOT EXISTS roi_distribution (
180
+ domain_slug TEXT NOT NULL,
181
+ entity_type TEXT NOT NULL,
182
+ bucket_index INTEGER NOT NULL,
183
+ bucket_lower_pct NUMERIC(10, 4) NOT NULL,
184
+ bucket_upper_pct NUMERIC(10, 4) NOT NULL,
185
+ user_count INTEGER NOT NULL DEFAULT 0,
186
+ computed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
187
+ PRIMARY KEY (domain_slug, entity_type, bucket_index)
188
+ )`,
189
+ `CREATE TABLE IF NOT EXISTS roi_summary (
190
+ domain_slug TEXT NOT NULL,
191
+ entity_type TEXT NOT NULL,
192
+ total_users INTEGER NOT NULL DEFAULT 0,
193
+ positive_roi_users INTEGER NOT NULL DEFAULT 0,
194
+ positive_roi_pct NUMERIC(10, 4) NOT NULL DEFAULT 0,
195
+ median_roi_pct NUMERIC(10, 4),
196
+ p25_roi_pct NUMERIC(10, 4),
197
+ p75_roi_pct NUMERIC(10, 4),
198
+ p90_roi_pct NUMERIC(10, 4),
199
+ mean_roi_pct NUMERIC(10, 4),
200
+ computed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
201
+ PRIMARY KEY (domain_slug, entity_type)
202
+ )`,
203
+ // ──────────────────────────────────────────────────────────────────────
204
+ // Aggregator bookkeeping
205
+ // ──────────────────────────────────────────────────────────────────────
206
+ `CREATE TABLE IF NOT EXISTS aggregator_state (
207
+ job_name TEXT PRIMARY KEY,
208
+ last_event_id BIGINT NOT NULL DEFAULT 0,
209
+ last_run_at TIMESTAMPTZ,
210
+ last_success_at TIMESTAMPTZ,
211
+ last_error TEXT
212
+ )`,
213
+ ];
214
+ export async function initializeWarehouseSchema(sql) {
215
+ for (const statement of SCHEMA_DDL) {
216
+ await sql.unsafe(statement);
217
+ }
218
+ }
219
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/warehouse/schema.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,GAAsB;IACpC,yEAAyE;IACzE,+BAA+B;IAC/B,yEAAyE;IACzE;;;;;;;IAOE;IAEF;;;;;;;IAOE;IAEF;mCACiC;IAEjC;sCACoC;IAEpC;uDACqD;IAErD,yEAAyE;IACzE,iDAAiD;IACjD,yEAAyE;IACzE;;;;;;;;;;;;;IAaE;IAEF;6CAC2C;IAC3C;kDACgD;IAEhD;;;;;;;;;;;;;IAaE;IAEF;2CACyC;IACzC;8CAC4C;IAE5C;;;;;;;;;;;IAWE;IAEF;qDACmD;IAEnD;;;;;;;;;;;IAWE;IAEF;qCACmC;IAEnC;;;;;;;;;;;IAWE;IAEF,yEAAyE;IACzE,uCAAuC;IACvC,yEAAyE;IACzE;;;;;;;;;;;;;;;;;;;IAmBE;IAEF;uCACqC;IACrC;gDAC8C;IAC9C;oCACkC;IAClC;0CACwC;IAExC;;;;;;;;;;;;;;;;;;;IAmBE;IAEF;wCACsC;IACtC;gCAC8B;IAE9B,yEAAyE;IACzE,iDAAiD;IACjD,yEAAyE;IACzE;;;;;;;;;IASE;IAEF;+DAC6D;IAE7D,yEAAyE;IACzE,yDAAyD;IACzD,yEAAyE;IACzE;;;;;;;;;IASE;IAEF;;;;;;;;;;;;;IAaE;IAEF,yEAAyE;IACzE,yBAAyB;IACzB,yEAAyE;IACzE;;;;;;IAME;CACH,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAQ;IACtD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC"}