@signals-protocol/v1-sdk 1.4.1 → 1.5.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/dist/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export * as MathUtils from "./utils/math";
10
10
  export { toWAD, toMicroUSDC } from "./clmsr-sdk";
11
11
  export { createCLMSRSDK, createSignalsSDK } from "./clmsr-sdk";
12
12
  export * from "./share";
13
- export declare const VERSION = "1.4.1";
13
+ export declare const VERSION = "1.5.0";
package/dist/index.js CHANGED
@@ -67,4 +67,4 @@ Object.defineProperty(exports, "createSignalsSDK", { enumerable: true, get: func
67
67
  // Share image VNode templates
68
68
  __exportStar(require("./share"), exports);
69
69
  // Version (keep in sync with package.json)
70
- exports.VERSION = "1.4.1";
70
+ exports.VERSION = "1.5.0";
@@ -0,0 +1,9 @@
1
+ export type AvatarTheme = "light" | "dark";
2
+ export interface AvatarData {
3
+ pattern: boolean[][];
4
+ primary: string;
5
+ secondary: string;
6
+ background: string;
7
+ }
8
+ export declare const AVATAR_COLOR_PALETTE: readonly ["#1444c2", "#2d88ff", "#fdd979", "#8bcaff", "#00BF40", "#F7931A", "#FF4343", "#6366f1", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#fb7185", "#fc97f3", "#5eead4", "#c4b5fd"];
9
+ export declare function getAvatarData(address: string, theme: AvatarTheme): AvatarData;
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AVATAR_COLOR_PALETTE = void 0;
4
+ exports.getAvatarData = getAvatarData;
5
+ exports.AVATAR_COLOR_PALETTE = [
6
+ "#1444c2",
7
+ "#2d88ff",
8
+ "#fdd979",
9
+ "#8bcaff",
10
+ "#00BF40",
11
+ "#F7931A",
12
+ "#FF4343",
13
+ "#6366f1",
14
+ "#8b5cf6",
15
+ "#ec4899",
16
+ "#f59e0b",
17
+ "#10b981",
18
+ "#fb7185",
19
+ "#fc97f3",
20
+ "#5eead4",
21
+ "#c4b5fd",
22
+ ];
23
+ const AVATAR_BACKGROUNDS = {
24
+ light: "#f8fafc",
25
+ dark: "#1e1e22",
26
+ };
27
+ function simpleHash(str) {
28
+ let hash = 5381;
29
+ for (let i = 0; i < str.length; i += 1) {
30
+ const char = str.charCodeAt(i);
31
+ hash = (hash << 5) + hash + char;
32
+ }
33
+ return Math.abs(hash);
34
+ }
35
+ function getSecondaryHash(address) {
36
+ let hash = 0;
37
+ const suffix = address.slice(-8);
38
+ for (let i = 0; i < suffix.length; i += 1) {
39
+ hash = ((hash << 3) + hash + suffix.charCodeAt(i)) & 0xffffffff;
40
+ }
41
+ return Math.abs(hash);
42
+ }
43
+ function generateSeedValues(address) {
44
+ const hash1 = simpleHash(address);
45
+ const hash2 = getSecondaryHash(address);
46
+ const values = [];
47
+ let seed = hash1;
48
+ for (let i = 0; i < 10; i += 1) {
49
+ seed = (seed * 1103015245 + 12345 + hash2) & 0x7fffffff;
50
+ values.push(seed);
51
+ }
52
+ return values;
53
+ }
54
+ function generatePixelPattern(address) {
55
+ const seeds = generateSeedValues(address);
56
+ const pattern = Array(5)
57
+ .fill(null)
58
+ .map(() => Array(5).fill(false));
59
+ for (let y = 0; y < 5; y += 1) {
60
+ for (let x = 0; x < 2; x += 1) {
61
+ const index = y * 2 + x;
62
+ const seedIndex = index % seeds.length;
63
+ const shouldFill = seeds[seedIndex] % 100 > 40;
64
+ pattern[y][x] = shouldFill;
65
+ pattern[y][4 - x] = shouldFill;
66
+ }
67
+ const centerIndex = y + 10;
68
+ const centerSeedIndex = centerIndex % seeds.length;
69
+ pattern[y][2] = seeds[centerSeedIndex] % 100 > 40;
70
+ }
71
+ const countFilled = () => pattern.flat().filter(Boolean).length;
72
+ const ensureMinPixels = (minPixels) => {
73
+ let filledCount = countFilled();
74
+ if (filledCount >= minPixels)
75
+ return;
76
+ const candidates = [];
77
+ for (let y = 0; y < 5; y += 1) {
78
+ for (let x = 0; x < 3; x += 1) {
79
+ if (!pattern[y][x]) {
80
+ candidates.push([y, x]);
81
+ }
82
+ }
83
+ }
84
+ let i = 0;
85
+ while (filledCount < minPixels && candidates.length > 0) {
86
+ const idx = (seeds[i % seeds.length] + i) % candidates.length;
87
+ const [y, x] = candidates.splice(idx, 1)[0];
88
+ if (!pattern[y][x]) {
89
+ pattern[y][x] = true;
90
+ if (x !== 2) {
91
+ if (!pattern[y][4 - x]) {
92
+ pattern[y][4 - x] = true;
93
+ filledCount += 2;
94
+ }
95
+ else {
96
+ filledCount += 1;
97
+ }
98
+ }
99
+ else {
100
+ filledCount += 1;
101
+ }
102
+ }
103
+ i += 1;
104
+ }
105
+ };
106
+ ensureMinPixels(12);
107
+ const parityCounts = { even: 0, odd: 0 };
108
+ for (let y = 0; y < 5; y += 1) {
109
+ for (let x = 0; x < 5; x += 1) {
110
+ if (pattern[y][x]) {
111
+ if ((y + x) % 2 === 0) {
112
+ parityCounts.even += 1;
113
+ }
114
+ else {
115
+ parityCounts.odd += 1;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ if (parityCounts.even === 0 || parityCounts.odd === 0) {
121
+ const missingParity = parityCounts.even === 0 ? 0 : 1;
122
+ const candidates = [];
123
+ for (let y = 0; y < 5; y += 1) {
124
+ for (let x = 0; x < 3; x += 1) {
125
+ if (!pattern[y][x] && (y + x) % 2 === missingParity) {
126
+ candidates.push([y, x]);
127
+ }
128
+ }
129
+ }
130
+ if (candidates.length > 0) {
131
+ const idx = (seeds[(seeds.length - 1 + missingParity) % seeds.length] +
132
+ parityCounts.even +
133
+ parityCounts.odd) %
134
+ candidates.length;
135
+ const [y, x] = candidates[idx];
136
+ pattern[y][x] = true;
137
+ if (x !== 2) {
138
+ pattern[y][4 - x] = true;
139
+ }
140
+ }
141
+ }
142
+ return pattern;
143
+ }
144
+ function generateColors(address) {
145
+ const seeds = generateSeedValues(address);
146
+ const hash1 = simpleHash(address);
147
+ const hash2 = getSecondaryHash(address);
148
+ const primaryColorIndex = Math.abs(hash1 ^ seeds[3]) % exports.AVATAR_COLOR_PALETTE.length;
149
+ let secondaryColorIndex = Math.abs(hash2 ^ seeds[7]) % exports.AVATAR_COLOR_PALETTE.length;
150
+ if (secondaryColorIndex === primaryColorIndex) {
151
+ secondaryColorIndex =
152
+ (secondaryColorIndex + 1) % exports.AVATAR_COLOR_PALETTE.length;
153
+ }
154
+ return {
155
+ primary: exports.AVATAR_COLOR_PALETTE[primaryColorIndex],
156
+ secondary: exports.AVATAR_COLOR_PALETTE[secondaryColorIndex],
157
+ };
158
+ }
159
+ function getAvatarData(address, theme) {
160
+ const colors = generateColors(address);
161
+ return {
162
+ pattern: generatePixelPattern(address),
163
+ primary: colors.primary,
164
+ secondary: colors.secondary,
165
+ background: AVATAR_BACKGROUNDS[theme],
166
+ };
167
+ }
@@ -3,6 +3,8 @@ export { buildPositionVNode } from "./position-image";
3
3
  export { buildProfileVNode } from "./profile-image";
4
4
  export { buildDistributionVNode } from "./distribution-image";
5
5
  export { buildBrandVNode } from "./brand-image";
6
+ export { AVATAR_COLOR_PALETTE, getAvatarData } from "./avatar";
7
+ export type { AvatarData, AvatarTheme } from "./avatar";
6
8
  export { h } from "./h";
7
9
  export { SIGNALS_LOGO_URI, buildLogoStrokeSVG } from "./assets";
8
10
  export { formatUSDC, formatPrice, formatPriceRange, percForm, formatUsdPnl, getPnlColor, } from "./format";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDistributionPriceRange = exports.getPeakRange = exports.formatXTick = exports.generateXAxisTicks = exports.getSqrtHeight = exports.getBarCategory = exports.getPnlColor = exports.formatUsdPnl = exports.percForm = exports.formatPriceRange = exports.formatPrice = exports.formatUSDC = exports.buildLogoStrokeSVG = exports.SIGNALS_LOGO_URI = exports.h = exports.buildBrandVNode = exports.buildDistributionVNode = exports.buildProfileVNode = exports.buildPositionVNode = void 0;
3
+ exports.formatDistributionPriceRange = exports.getPeakRange = exports.formatXTick = exports.generateXAxisTicks = exports.getSqrtHeight = exports.getBarCategory = exports.getPnlColor = exports.formatUsdPnl = exports.percForm = exports.formatPriceRange = exports.formatPrice = exports.formatUSDC = exports.buildLogoStrokeSVG = exports.SIGNALS_LOGO_URI = exports.h = exports.getAvatarData = exports.AVATAR_COLOR_PALETTE = exports.buildBrandVNode = exports.buildDistributionVNode = exports.buildProfileVNode = exports.buildPositionVNode = void 0;
4
4
  // VNode builders
5
5
  var position_image_1 = require("./position-image");
6
6
  Object.defineProperty(exports, "buildPositionVNode", { enumerable: true, get: function () { return position_image_1.buildPositionVNode; } });
@@ -10,6 +10,9 @@ var distribution_image_1 = require("./distribution-image");
10
10
  Object.defineProperty(exports, "buildDistributionVNode", { enumerable: true, get: function () { return distribution_image_1.buildDistributionVNode; } });
11
11
  var brand_image_1 = require("./brand-image");
12
12
  Object.defineProperty(exports, "buildBrandVNode", { enumerable: true, get: function () { return brand_image_1.buildBrandVNode; } });
13
+ var avatar_1 = require("./avatar");
14
+ Object.defineProperty(exports, "AVATAR_COLOR_PALETTE", { enumerable: true, get: function () { return avatar_1.AVATAR_COLOR_PALETTE; } });
15
+ Object.defineProperty(exports, "getAvatarData", { enumerable: true, get: function () { return avatar_1.getAvatarData; } });
13
16
  // Helpers (used by v1-server for its own distribution rendering)
14
17
  var h_1 = require("./h");
15
18
  Object.defineProperty(exports, "h", { enumerable: true, get: function () { return h_1.h; } });
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildProfileVNode = buildProfileVNode;
4
4
  const h_1 = require("./h");
5
5
  const assets_1 = require("./assets");
6
+ const avatar_1 = require("./avatar");
6
7
  const CARD_WIDTH = 1200;
7
8
  const CARD_HEIGHT = 630;
8
9
  const OUTER_PADDING = 24;
@@ -15,6 +16,9 @@ const PLOT_RIGHT = 18;
15
16
  const PLOT_TOP = 14;
16
17
  const PLOT_BOTTOM = 24;
17
18
  const BAND_WIDTH = 18;
19
+ const DAY_MS = 86400000;
20
+ const AVATAR_SIZE = 96;
21
+ const MAX_RENDERED_POSITIONS = 1000;
18
22
  const OG_THEMES = {
19
23
  light: {
20
24
  pageBg: "#eef7ff", // bg-surface-soft
@@ -41,13 +45,83 @@ const OG_THEMES = {
41
45
  neutralBg: "#27272A", // surface-muted
42
46
  },
43
47
  };
44
- const TIER_ACCENT_COLORS = {
45
- BRONZE: "#C7742A",
46
- SILVER: "#BFC6CE",
47
- GOLD: "#F3BC3C",
48
- PLATINUM: "#63CBB2",
49
- DIAMOND: "#5EC0F2",
48
+ const TIER_CHIP_THEMES = {
49
+ BRONZE: {
50
+ light: {
51
+ from: "#FFE6CC",
52
+ to: "#FFC999",
53
+ border: "#C97E3D",
54
+ icon: "#8B4513",
55
+ },
56
+ dark: {
57
+ from: "#3D2B1A",
58
+ to: "#5C3A1E",
59
+ border: "#8B5E2B",
60
+ icon: "#F5E5D4",
61
+ },
62
+ },
63
+ SILVER: {
64
+ light: {
65
+ from: "#F2F5F8",
66
+ to: "#E2E7EE",
67
+ border: "#BFC6CE",
68
+ icon: "#6B7280",
69
+ },
70
+ dark: {
71
+ from: "#2A2D31",
72
+ to: "#363A40",
73
+ border: "#6B7280",
74
+ icon: "#F3F4F6",
75
+ },
76
+ },
77
+ GOLD: {
78
+ light: {
79
+ from: "#FFF0B8",
80
+ to: "#FFD875",
81
+ border: "#D9A441",
82
+ icon: "#DA9100",
83
+ },
84
+ dark: {
85
+ from: "#3D3520",
86
+ to: "#4A3D1A",
87
+ border: "#B8892E",
88
+ icon: "#FFE9B3",
89
+ },
90
+ },
91
+ PLATINUM: {
92
+ light: {
93
+ from: "#E4FFF7",
94
+ to: "#CDEFE5",
95
+ border: "#8FD7C6",
96
+ icon: "#059669",
97
+ },
98
+ dark: {
99
+ from: "#1A3D33",
100
+ to: "#1F3B31",
101
+ border: "#4DAE96",
102
+ icon: "#BAEBD9",
103
+ },
104
+ },
105
+ DIAMOND: {
106
+ light: {
107
+ from: "#E2F4FF",
108
+ to: "#C9E9FF",
109
+ border: "#89D0FF",
110
+ icon: "#0284C7",
111
+ },
112
+ dark: {
113
+ from: "#1A2F3D",
114
+ to: "#1E3545",
115
+ border: "#5AAFE0",
116
+ icon: "#5EC0F2",
117
+ },
118
+ },
50
119
  };
120
+ const PRICE_TICK_STEPS = [
121
+ 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000,
122
+ 250000, 500000, 1000000,
123
+ ];
124
+ const MAX_MATERIALIZED_PRICE_TICKS = 20;
51
125
  function shortenAddress(address) {
52
126
  return `${address.slice(0, 6)}…${address.slice(-4)}`;
53
127
  }
@@ -99,6 +173,17 @@ function normalizePositions(positions) {
99
173
  });
100
174
  return normalized;
101
175
  }
176
+ function capRenderablePositions(positions) {
177
+ if (positions.length <= MAX_RENDERED_POSITIONS) {
178
+ return positions;
179
+ }
180
+ return positions
181
+ .map((position, index) => ({ position, index }))
182
+ .sort((a, b) => b.position.marketEndTimestampMs - a.position.marketEndTimestampMs ||
183
+ a.index - b.index)
184
+ .slice(0, MAX_RENDERED_POSITIONS)
185
+ .map(({ position }) => position);
186
+ }
102
187
  function buildMainText(input) {
103
188
  const handle = resolveDisplayName(input.realHandle ?? null);
104
189
  if (handle)
@@ -107,60 +192,111 @@ function buildMainText(input) {
107
192
  return "A Signals trader";
108
193
  return resolveDisplayName(input.displayName) ?? shortenAddress(input.address);
109
194
  }
195
+ function formatCallCopy(totalCalls) {
196
+ return `${totalCalls} BTC range ${totalCalls === 1 ? "call" : "calls"}`;
197
+ }
110
198
  function buildSubline(input) {
111
- const callCopy = `${input.totalCalls} BTC range calls`;
199
+ const callCopy = formatCallCopy(input.totalCalls);
112
200
  if (input.hideWallet)
113
- return callCopy;
114
- const parts = [];
201
+ return `${callCopy} on Signals`;
115
202
  if (resolveDisplayName(input.realHandle ?? null) !== null) {
116
- parts.push(shortenAddress(input.address));
203
+ return `${shortenAddress(input.address)} with ${callCopy}`;
117
204
  }
118
205
  if (input.twitter) {
119
- parts.push(`@${input.twitter}`);
206
+ return `@${input.twitter} with ${callCopy}`;
207
+ }
208
+ return `${callCopy} on Signals`;
209
+ }
210
+ function buildPriceTickCandidate(min, max, step) {
211
+ const alignedMin = Math.floor(min / step) * step;
212
+ const alignedMax = Math.ceil(max / step) * step;
213
+ const count = Math.floor((alignedMax - alignedMin) / step) + 1;
214
+ const ticks = [];
215
+ if (count > MAX_MATERIALIZED_PRICE_TICKS) {
216
+ return {
217
+ min: alignedMin,
218
+ max: alignedMax,
219
+ step,
220
+ count,
221
+ ticks,
222
+ };
120
223
  }
121
- parts.push(callCopy);
122
- return parts.join(" · ");
224
+ for (let tick = alignedMin; tick <= alignedMax + step / 2; tick += step) {
225
+ ticks.push(tick);
226
+ }
227
+ return {
228
+ min: alignedMin,
229
+ max: alignedMax,
230
+ step,
231
+ count,
232
+ ticks,
233
+ };
123
234
  }
124
235
  function getPriceTickStep(span) {
125
236
  const target = span / 4;
126
- const candidates = [
127
- 500,
128
- 1000,
129
- 2500,
130
- 5000,
131
- 10000,
132
- 25000,
133
- 50000,
134
- 100000,
135
- 250000,
136
- 500000,
137
- 1000000,
138
- ];
139
- return candidates.find((step) => step >= target) ?? 1000000;
237
+ return (PRICE_TICK_STEPS.find((step) => step >= target) ??
238
+ PRICE_TICK_STEPS[PRICE_TICK_STEPS.length - 1]);
140
239
  }
141
240
  function getExpandedPriceBounds(min, max) {
142
241
  const span = max - min;
143
- const step = getPriceTickStep(span);
242
+ const desiredStep = getPriceTickStep(span);
243
+ const candidates = PRICE_TICK_STEPS.map((step) => buildPriceTickCandidate(min, max, step));
244
+ const validCandidates = candidates.filter((candidate) => candidate.ticks.length > 0 &&
245
+ candidate.count >= 4 &&
246
+ candidate.count <= 5);
247
+ const fallbackCandidates = candidates.filter((candidate) => candidate.ticks.length > 0);
248
+ const [selected] = (validCandidates.length > 0
249
+ ? validCandidates
250
+ : fallbackCandidates.length > 0
251
+ ? fallbackCandidates
252
+ : candidates)
253
+ .slice()
254
+ .sort((a, b) => {
255
+ const aCountPenalty = a.count >= 4 && a.count <= 5
256
+ ? 0
257
+ : Math.abs(a.count - 5) * 1000000;
258
+ const bCountPenalty = b.count >= 4 && b.count <= 5
259
+ ? 0
260
+ : Math.abs(b.count - 5) * 1000000;
261
+ return (aCountPenalty +
262
+ Math.abs(a.step - desiredStep) -
263
+ (bCountPenalty + Math.abs(b.step - desiredStep)));
264
+ });
144
265
  return {
145
- min: Math.floor(min / step) * step,
146
- max: Math.ceil(max / step) * step,
147
- step,
266
+ min: selected.min,
267
+ max: selected.max,
268
+ step: selected.step,
148
269
  };
149
270
  }
150
271
  function getPriceTicks(min, max, step) {
272
+ const count = Math.floor((max - min) / step) + 1;
151
273
  const ticks = [];
152
- for (let tick = min; tick <= max + step / 2; tick += step) {
153
- ticks.push(tick);
154
- if (ticks.length >= 6)
155
- break;
274
+ if (count <= MAX_MATERIALIZED_PRICE_TICKS) {
275
+ for (let tick = min; tick <= max + step / 2; tick += step) {
276
+ ticks.push(tick);
277
+ }
278
+ }
279
+ if (count >= 4 && count <= 5 && ticks.length > 0) {
280
+ return { ticks, step };
156
281
  }
157
- return ticks.length > 0 ? ticks : [min, max];
282
+ const fallbackCount = count < 4 ? 4 : 5;
283
+ const fallbackStep = (max - min) / (fallbackCount - 1);
284
+ return {
285
+ ticks: Array.from({ length: fallbackCount }, (_, index) => Math.round(min + fallbackStep * index)),
286
+ step: fallbackStep,
287
+ };
158
288
  }
159
- function formatAxisLabel(value) {
289
+ function formatAxisLabel(value, step) {
290
+ if (step < 100) {
291
+ return `$${Math.round(value).toLocaleString("en-US")}`;
292
+ }
160
293
  if (Math.abs(value) >= 1000) {
294
+ if (step >= 100 && step < 1000) {
295
+ return `$${(value / 1000).toFixed(1)}k`;
296
+ }
161
297
  return `$${Math.round(value / 1000)}k`;
162
298
  }
163
- return `$${Math.round(value)}`;
299
+ return `$${Math.round(value).toLocaleString("en-US")}`;
164
300
  }
165
301
  function computePriceBounds(btcHistory, positions) {
166
302
  let rawMin = Infinity;
@@ -232,9 +368,239 @@ function buildHistoryPath(btcHistory, scaleX, scaleY) {
232
368
  })
233
369
  .join(" ");
234
370
  }
371
+ function clamp(value, min, max) {
372
+ return Math.min(Math.max(value, min), max);
373
+ }
374
+ function formatTierName(tier) {
375
+ return `${tier[0]}${tier.slice(1).toLowerCase()}`;
376
+ }
377
+ function buildTierChip(tier, themeName, theme) {
378
+ const tierTheme = TIER_CHIP_THEMES[tier][themeName];
379
+ return (0, h_1.h)("div", {
380
+ style: {
381
+ display: "flex",
382
+ alignItems: "center",
383
+ gap: 8,
384
+ padding: "8px 14px",
385
+ borderRadius: 999,
386
+ border: `1px solid ${tierTheme.border}`,
387
+ background: `linear-gradient(135deg, ${tierTheme.from}, ${tierTheme.to})`,
388
+ color: theme.ink,
389
+ fontSize: 18,
390
+ fontWeight: 700,
391
+ lineHeight: 1,
392
+ flexShrink: 0,
393
+ },
394
+ }, (0, h_1.h)("svg", {
395
+ width: 18,
396
+ height: 18,
397
+ viewBox: "0 0 16 16",
398
+ style: { display: "flex", flexShrink: 0 },
399
+ }, (0, h_1.h)("polygon", {
400
+ points: "8 1 15 8 8 15 1 8",
401
+ fill: tierTheme.icon,
402
+ })), (0, h_1.h)("div", { style: { display: "flex" } }, formatTierName(tier)));
403
+ }
404
+ function buildAddressAvatar(address, themeName, theme) {
405
+ const avatar = (0, avatar_1.getAvatarData)(address, themeName);
406
+ return (0, h_1.h)("div", {
407
+ style: {
408
+ width: AVATAR_SIZE,
409
+ height: AVATAR_SIZE,
410
+ display: "flex",
411
+ flexDirection: "column",
412
+ overflow: "hidden",
413
+ borderRadius: 22,
414
+ border: `1px solid ${theme.border}`,
415
+ background: avatar.background,
416
+ flexShrink: 0,
417
+ },
418
+ }, ...avatar.pattern.map((row, y) => (0, h_1.h)("div", {
419
+ style: {
420
+ display: "flex",
421
+ width: "100%",
422
+ height: "20%",
423
+ },
424
+ }, ...row.map((isFilled, x) => (0, h_1.h)("div", {
425
+ style: {
426
+ width: "20%",
427
+ height: "100%",
428
+ backgroundColor: isFilled
429
+ ? (y + x) % 2 === 0
430
+ ? avatar.primary
431
+ : avatar.secondary
432
+ : "transparent",
433
+ },
434
+ })))));
435
+ }
436
+ function buildGenericAvatar(theme) {
437
+ return (0, h_1.h)("div", {
438
+ style: {
439
+ width: AVATAR_SIZE,
440
+ height: AVATAR_SIZE,
441
+ display: "flex",
442
+ alignItems: "center",
443
+ justifyContent: "center",
444
+ overflow: "hidden",
445
+ borderRadius: 22,
446
+ border: `1px solid ${theme.border}`,
447
+ background: theme.primary,
448
+ flexShrink: 0,
449
+ },
450
+ }, (0, h_1.h)("div", {
451
+ style: {
452
+ width: 58,
453
+ height: 58,
454
+ display: "flex",
455
+ alignItems: "center",
456
+ justifyContent: "center",
457
+ borderRadius: 999,
458
+ background: theme.card,
459
+ },
460
+ }, (0, h_1.h)("img", {
461
+ src: assets_1.SIGNALS_LOGO_URI,
462
+ width: 34,
463
+ height: 32,
464
+ style: { display: "flex" },
465
+ })));
466
+ }
467
+ function buildAvatar(input, theme) {
468
+ if (input.hideWallet) {
469
+ return buildGenericAvatar(theme);
470
+ }
471
+ return buildAddressAvatar(input.address, input.theme, theme);
472
+ }
473
+ function buildBrandWordmark(theme) {
474
+ return (0, h_1.h)("div", {
475
+ style: {
476
+ display: "flex",
477
+ alignItems: "center",
478
+ gap: 10,
479
+ flexShrink: 0,
480
+ },
481
+ }, (0, h_1.h)("img", {
482
+ src: assets_1.SIGNALS_LOGO_URI,
483
+ width: 34,
484
+ height: 32,
485
+ style: { display: "flex" },
486
+ }), (0, h_1.h)("div", {
487
+ style: {
488
+ display: "flex",
489
+ fontSize: 28,
490
+ fontWeight: 700,
491
+ color: theme.primary,
492
+ },
493
+ }, "Signals"));
494
+ }
495
+ function buildHeader(input, theme, mainText, subline) {
496
+ return (0, h_1.h)("div", {
497
+ style: {
498
+ display: "flex",
499
+ alignItems: "center",
500
+ justifyContent: "space-between",
501
+ gap: 24,
502
+ minHeight: AVATAR_SIZE,
503
+ },
504
+ }, (0, h_1.h)("div", {
505
+ style: {
506
+ flex: 1,
507
+ minWidth: 0,
508
+ display: "flex",
509
+ alignItems: "center",
510
+ gap: 18,
511
+ },
512
+ }, buildAvatar(input, theme), (0, h_1.h)("div", {
513
+ style: {
514
+ flex: 1,
515
+ minWidth: 0,
516
+ display: "flex",
517
+ flexDirection: "column",
518
+ },
519
+ }, (0, h_1.h)("div", {
520
+ style: {
521
+ display: "flex",
522
+ alignItems: "center",
523
+ gap: 14,
524
+ minWidth: 0,
525
+ },
526
+ }, (0, h_1.h)("div", {
527
+ style: {
528
+ display: "flex",
529
+ fontSize: 52,
530
+ lineHeight: 1.05,
531
+ fontWeight: 700,
532
+ color: theme.ink,
533
+ maxWidth: 620,
534
+ overflow: "hidden",
535
+ textOverflow: "ellipsis",
536
+ whiteSpace: "nowrap",
537
+ },
538
+ }, mainText), buildTierChip(input.tier, input.theme, theme)), (0, h_1.h)("div", {
539
+ style: {
540
+ display: "flex",
541
+ fontSize: 20,
542
+ fontWeight: 500,
543
+ color: theme.inkMuted,
544
+ marginTop: 8,
545
+ overflow: "hidden",
546
+ textOverflow: "ellipsis",
547
+ whiteSpace: "nowrap",
548
+ },
549
+ }, subline))), buildBrandWordmark(theme));
550
+ }
551
+ function buildStatItem(label, value, valueColor, theme) {
552
+ return (0, h_1.h)("div", {
553
+ style: {
554
+ flex: 1,
555
+ minWidth: 0,
556
+ display: "flex",
557
+ flexDirection: "column",
558
+ },
559
+ }, (0, h_1.h)("div", {
560
+ style: {
561
+ display: "flex",
562
+ fontSize: 14,
563
+ fontWeight: 500,
564
+ color: theme.inkMuted,
565
+ marginBottom: 8,
566
+ },
567
+ }, label), (0, h_1.h)("div", {
568
+ style: {
569
+ display: "flex",
570
+ fontSize: 30,
571
+ lineHeight: 1.1,
572
+ fontWeight: 700,
573
+ color: valueColor,
574
+ },
575
+ }, value));
576
+ }
577
+ function buildStatSeparator(theme) {
578
+ return (0, h_1.h)("div", {
579
+ style: {
580
+ width: 1,
581
+ height: 28,
582
+ display: "flex",
583
+ background: theme.border,
584
+ margin: "0 18px",
585
+ flexShrink: 0,
586
+ },
587
+ });
588
+ }
589
+ function buildStatRow(input, theme, pnlValue, roiValue, bestMultValue) {
590
+ return (0, h_1.h)("div", {
591
+ style: {
592
+ display: "flex",
593
+ alignItems: "center",
594
+ marginTop: 20,
595
+ padding: "0 4px",
596
+ },
597
+ }, buildStatItem("P&L", pnlValue, input.hideAmounts
598
+ ? theme.inkMuted
599
+ : getSignedValueColor(input.totalPnl, theme), theme), buildStatSeparator(theme), buildStatItem("ROI", roiValue, getSignedValueColor(input.roi, theme), theme), buildStatSeparator(theme), buildStatItem("Best call", bestMultValue, theme.primary, theme));
600
+ }
235
601
  function buildChartArea(theme, btcHistory, positions) {
236
602
  const normalizedPositions = normalizePositions(positions);
237
- const renderablePositions = normalizedPositions.filter((position) => position.status !== "CLOSED");
603
+ const renderablePositions = capRenderablePositions(normalizedPositions);
238
604
  const sortedHistory = [...btcHistory].sort((a, b) => a.t - b.t);
239
605
  if (btcHistory.length === 0 && renderablePositions.length === 0) {
240
606
  return (0, h_1.h)("div", {
@@ -284,12 +650,40 @@ function buildChartArea(theme, btcHistory, positions) {
284
650
  return PLOT_TOP + plotHeight / 2;
285
651
  }
286
652
  return (PLOT_TOP +
287
- (1 -
288
- (value - priceBounds.min) / (priceBounds.max - priceBounds.min)) *
653
+ (1 - (value - priceBounds.min) / (priceBounds.max - priceBounds.min)) *
289
654
  plotHeight);
290
655
  };
291
656
  const priceTicks = getPriceTicks(priceBounds.min, priceBounds.max, priceBounds.step);
292
657
  const hasOpenPositions = renderablePositions.some((position) => position.status === "OPEN");
658
+ const latestSettlement = renderablePositions
659
+ .filter((position) => (position.status === "WIN" || position.status === "LOSS") &&
660
+ position.settlementPrice != null)
661
+ .sort((a, b) => b.marketEndTimestampMs - a.marketEndTimestampMs)[0];
662
+ const groupSizes = new Map();
663
+ const groupOrdinals = new Map();
664
+ renderablePositions.forEach((position) => {
665
+ const timestamp = position.marketEndTimestampMs;
666
+ const ordinal = groupSizes.get(timestamp) ?? 0;
667
+ groupOrdinals.set(position, ordinal);
668
+ groupSizes.set(timestamp, ordinal + 1);
669
+ });
670
+ const dayWidth = timeMin === timeMax ? 0 : (DAY_MS / (timeMax - timeMin)) * plotWidth;
671
+ const getBandX = (position) => {
672
+ const groupSize = groupSizes.get(position.marketEndTimestampMs) ?? 1;
673
+ const ordinal = groupOrdinals.get(position) ?? 0;
674
+ const center = scaleX(position.marketEndTimestampMs);
675
+ const spreadWidth = groupSize <= 1
676
+ ? 0
677
+ : timeMin === timeMax
678
+ ? plotWidth / 4
679
+ : Math.max(dayWidth, BAND_WIDTH * (groupSize + 1));
680
+ const offset = groupSize <= 1
681
+ ? 0
682
+ : (ordinal - (groupSize - 1) / 2) * (spreadWidth / groupSize);
683
+ const rawX = center + offset - BAND_WIDTH / 2;
684
+ return clamp(rawX, PLOT_LEFT, PLOT_LEFT + plotWidth - BAND_WIDTH);
685
+ };
686
+ const getBandCenterX = (position) => getBandX(position) + BAND_WIDTH / 2;
293
687
  const svgChildren = [];
294
688
  if (hasOpenPositions) {
295
689
  svgChildren.push((0, h_1.h)("defs", null, (0, h_1.h)("pattern", {
@@ -303,7 +697,7 @@ function buildChartArea(theme, btcHistory, positions) {
303
697
  strokeWidth: 2,
304
698
  }))));
305
699
  }
306
- priceTicks.forEach((tick) => {
700
+ priceTicks.ticks.forEach((tick) => {
307
701
  const y = scaleY(tick);
308
702
  svgChildren.push((0, h_1.h)("line", {
309
703
  x1: PLOT_LEFT,
@@ -315,10 +709,23 @@ function buildChartArea(theme, btcHistory, positions) {
315
709
  strokeWidth: 1,
316
710
  }));
317
711
  });
712
+ if (latestSettlement) {
713
+ const y = scaleY(latestSettlement.settlementPrice);
714
+ svgChildren.push((0, h_1.h)("line", {
715
+ x1: PLOT_LEFT,
716
+ x2: PLOT_LEFT + plotWidth,
717
+ y1: y,
718
+ y2: y,
719
+ stroke: theme.bitcoin,
720
+ strokeDasharray: "4 5",
721
+ strokeWidth: 1.5,
722
+ strokeOpacity: 0.75,
723
+ }));
724
+ }
318
725
  renderablePositions.forEach((position) => {
319
726
  const topPrice = Math.max(position.lowerPrice, position.upperPrice);
320
727
  const bottomPrice = Math.min(position.lowerPrice, position.upperPrice);
321
- const x = scaleX(position.marketEndTimestampMs) - BAND_WIDTH / 2;
728
+ const x = getBandX(position);
322
729
  const top = scaleY(topPrice);
323
730
  const bottom = scaleY(bottomPrice);
324
731
  const height = Math.max(bottom - top, 6);
@@ -330,13 +737,22 @@ function buildChartArea(theme, btcHistory, positions) {
330
737
  height,
331
738
  rx: 6,
332
739
  fill: "url(#profile-open-band)",
740
+ opacity: 0.4,
333
741
  stroke: theme.bitcoin,
334
742
  strokeWidth: 2,
335
743
  }));
336
744
  return;
337
745
  }
338
- const fill = position.status === "WIN" ? theme.primary : theme.negative;
339
- const fillOpacity = position.status === "WIN" ? 0.55 : 0.28;
746
+ const fill = position.status === "WIN"
747
+ ? theme.primary
748
+ : position.status === "LOSS"
749
+ ? theme.negative
750
+ : theme.inkMuted;
751
+ const fillOpacity = position.status === "WIN"
752
+ ? 0.45
753
+ : position.status === "LOSS"
754
+ ? 0.28
755
+ : 0.3;
340
756
  svgChildren.push((0, h_1.h)("rect", {
341
757
  x,
342
758
  y: top,
@@ -346,7 +762,7 @@ function buildChartArea(theme, btcHistory, positions) {
346
762
  fill,
347
763
  fillOpacity,
348
764
  stroke: fill,
349
- strokeOpacity: 0.9,
765
+ strokeOpacity: position.status === "CLOSED" ? 0.55 : 0.9,
350
766
  }));
351
767
  });
352
768
  if (sortedHistory.length === 1) {
@@ -371,14 +787,14 @@ function buildChartArea(theme, btcHistory, positions) {
371
787
  if ((position.status === "WIN" || position.status === "LOSS") &&
372
788
  position.settlementPrice != null) {
373
789
  svgChildren.push((0, h_1.h)("circle", {
374
- cx: scaleX(position.marketEndTimestampMs),
790
+ cx: getBandCenterX(position),
375
791
  cy: scaleY(position.settlementPrice),
376
792
  r: 4.5,
377
793
  fill: position.status === "WIN" ? theme.primary : theme.negative,
378
794
  }));
379
795
  }
380
796
  });
381
- const labelNodes = priceTicks.map((tick) => (0, h_1.h)("div", {
797
+ const labelNodes = priceTicks.ticks.map((tick) => (0, h_1.h)("div", {
382
798
  style: {
383
799
  position: "absolute",
384
800
  left: 12,
@@ -390,7 +806,26 @@ function buildChartArea(theme, btcHistory, positions) {
390
806
  background: theme.card,
391
807
  padding: "0 4px",
392
808
  },
393
- }, formatAxisLabel(tick)));
809
+ }, formatAxisLabel(tick, priceTicks.step)));
810
+ const settlementLabel = latestSettlement == null
811
+ ? []
812
+ : [
813
+ (0, h_1.h)("div", {
814
+ style: {
815
+ position: "absolute",
816
+ left: PLOT_LEFT + plotWidth - 64,
817
+ top: scaleY(latestSettlement.settlementPrice) - 14,
818
+ display: "flex",
819
+ fontSize: 14,
820
+ fontWeight: 700,
821
+ color: theme.bitcoin,
822
+ background: theme.card,
823
+ border: `1px solid ${theme.bitcoin}`,
824
+ borderRadius: 999,
825
+ padding: "4px 10px",
826
+ },
827
+ }, formatAxisLabel(latestSettlement.settlementPrice, priceTicks.step)),
828
+ ];
394
829
  return (0, h_1.h)("div", {
395
830
  style: {
396
831
  width: CHART_WIDTH,
@@ -398,9 +833,6 @@ function buildChartArea(theme, btcHistory, positions) {
398
833
  display: "flex",
399
834
  position: "relative",
400
835
  overflow: "hidden",
401
- background: theme.neutralBg,
402
- border: `1px solid ${theme.border}`,
403
- borderRadius: 20,
404
836
  },
405
837
  }, (0, h_1.h)("svg", {
406
838
  width: CHART_WIDTH,
@@ -412,43 +844,15 @@ function buildChartArea(theme, btcHistory, positions) {
412
844
  display: "flex",
413
845
  pointerEvents: "none",
414
846
  },
415
- }, ...svgChildren), ...labelNodes);
416
- }
417
- function buildStatCard(label, value, valueColor, theme) {
418
- return (0, h_1.h)("div", {
419
- style: {
420
- flex: 1,
421
- display: "flex",
422
- flexDirection: "column",
423
- padding: "16px 18px",
424
- borderRadius: 18,
425
- border: `1px solid ${theme.border}`,
426
- background: theme.neutralBg,
427
- },
428
- }, (0, h_1.h)("div", {
429
- style: {
430
- display: "flex",
431
- fontSize: 14,
432
- fontWeight: 500,
433
- color: theme.inkMuted,
434
- marginBottom: 10,
435
- },
436
- }, label), (0, h_1.h)("div", {
437
- style: {
438
- display: "flex",
439
- fontSize: 28,
440
- lineHeight: 1.1,
441
- fontWeight: 700,
442
- color: valueColor,
443
- },
444
- }, value));
847
+ }, ...svgChildren), ...labelNodes, ...settlementLabel);
445
848
  }
446
849
  function buildProfileVNode(input) {
447
850
  const theme = OG_THEMES[input.theme];
448
- const accent = TIER_ACCENT_COLORS[input.tier];
449
851
  const mainText = buildMainText(input);
450
852
  const subline = buildSubline(input);
451
- const pnlValue = input.hideAmounts ? "••••" : formatProfilePnl(input.totalPnl);
853
+ const pnlValue = input.hideAmounts
854
+ ? "••••"
855
+ : formatProfilePnl(input.totalPnl);
452
856
  const roiValue = formatProfileRoi(input.roi);
453
857
  const bestMultValue = formatBestMult(input.bestMult);
454
858
  return (0, h_1.h)("div", {
@@ -470,79 +874,18 @@ function buildProfileVNode(input) {
470
874
  border: `1px solid ${theme.border}`,
471
875
  borderRadius: 24,
472
876
  },
473
- }, (0, h_1.h)("div", {
474
- style: {
475
- display: "flex",
476
- alignItems: "center",
477
- justifyContent: "space-between",
478
- },
479
- }, (0, h_1.h)("div", {
480
- style: {
481
- display: "flex",
482
- alignItems: "center",
483
- gap: 10,
484
- },
485
- }, (0, h_1.h)("img", {
486
- src: assets_1.SIGNALS_LOGO_URI,
487
- width: 34,
488
- height: 32,
489
- style: { display: "flex" },
490
- }), (0, h_1.h)("div", {
491
- style: {
492
- display: "flex",
493
- fontSize: 28,
494
- fontWeight: 700,
495
- color: theme.primary,
496
- },
497
- }, "Signals")), (0, h_1.h)("div", {
498
- style: {
499
- display: "flex",
500
- fontSize: 18,
501
- fontWeight: 700,
502
- letterSpacing: "0.04em",
503
- textTransform: "uppercase",
504
- color: accent,
505
- },
506
- }, input.tier)), (0, h_1.h)("div", {
877
+ }, buildHeader(input, theme, mainText, subline), (0, h_1.h)("div", {
507
878
  style: {
508
879
  flex: 1,
509
880
  display: "flex",
510
881
  flexDirection: "column",
511
- marginTop: 20,
512
- },
513
- }, (0, h_1.h)("div", {
514
- style: {
515
- display: "flex",
516
- flexDirection: "column",
882
+ marginTop: 22,
517
883
  },
518
884
  }, (0, h_1.h)("div", {
519
885
  style: {
520
886
  display: "flex",
521
- fontSize: 52,
522
- lineHeight: 1.05,
523
- fontWeight: 700,
524
- color: theme.ink,
525
- marginBottom: 10,
526
- },
527
- }, mainText), (0, h_1.h)("div", {
528
- style: {
529
- display: "flex",
530
- fontSize: 20,
531
- fontWeight: 500,
532
- color: theme.inkMuted,
533
- },
534
- }, subline)), (0, h_1.h)("div", {
535
- style: {
536
- display: "flex",
537
- marginTop: 24,
538
- },
539
- }, buildChartArea(theme, input.btcHistory, input.positions)), (0, h_1.h)("div", {
540
- style: {
541
- display: "flex",
542
- gap: 16,
543
- marginTop: 20,
544
887
  },
545
- }, buildStatCard("P&L", pnlValue, getSignedValueColor(input.totalPnl, theme), theme), buildStatCard("ROI", roiValue, getSignedValueColor(input.roi, theme), theme), buildStatCard("Best call", bestMultValue, theme.primary, theme)), (0, h_1.h)("div", {
888
+ }, buildChartArea(theme, input.btcHistory, input.positions)), buildStatRow(input, theme, pnlValue, roiValue, bestMultValue), (0, h_1.h)("div", {
546
889
  style: {
547
890
  display: "flex",
548
891
  justifyContent: "flex-end",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signals-protocol/v1-sdk",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Signals v1 SDK for CLMSR market calculations and utilities",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",