create-hedgeboard 1.0.7 → 1.0.8
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/index.js +1 -1
- package/package.json +1 -1
- package/hedgeboard/CLAUDE.md +0 -284
- package/hedgeboard/README.md +0 -64
- package/hedgeboard/brand/base.css +0 -419
- package/hedgeboard/brand/theme.json +0 -25
- package/hedgeboard/data/__init__.py +0 -1
- package/hedgeboard/data/sec.py +0 -218
- package/hedgeboard/modules/__init__.py +0 -1
- package/hedgeboard/modules/company_overview.py +0 -157
- package/hedgeboard/requirements.txt +0 -2
- package/hedgeboard/viz/__init__.py +0 -1
- package/hedgeboard/viz/charts.py +0 -198
- package/hedgeboard/viz/components.py +0 -391
- package/hedgeboard/viz/dashboard.py +0 -330
- package/hedgeboard/viz/tables.py +0 -140
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
/* === HedgeBoard Dashboard Styles === */
|
|
2
|
-
/* Used by viz/dashboard.py — loaded into every generated dashboard */
|
|
3
|
-
|
|
4
|
-
/* Fonts */
|
|
5
|
-
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
|
|
6
|
-
|
|
7
|
-
/* ---------------------------------------------------------------------------
|
|
8
|
-
Grid Layout
|
|
9
|
-
--------------------------------------------------------------------------- */
|
|
10
|
-
.hb-grid {
|
|
11
|
-
display: grid;
|
|
12
|
-
gap: 16px;
|
|
13
|
-
margin-bottom: 24px;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.hb-grid-2 {
|
|
17
|
-
grid-template-columns: 1fr 1fr;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.hb-grid-3 {
|
|
21
|
-
grid-template-columns: 1fr 1fr 1fr;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.hb-grid-cell {
|
|
25
|
-
min-width: 0;
|
|
26
|
-
/* prevent overflow */
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/* ---------------------------------------------------------------------------
|
|
30
|
-
KPI Row
|
|
31
|
-
--------------------------------------------------------------------------- */
|
|
32
|
-
.hb-kpi-row {
|
|
33
|
-
display: grid;
|
|
34
|
-
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
35
|
-
gap: 1px;
|
|
36
|
-
background: var(--color-surface, #2A2A33);
|
|
37
|
-
border: 1px solid var(--color-surface, #2A2A33);
|
|
38
|
-
border-radius: 2px;
|
|
39
|
-
margin-bottom: 24px;
|
|
40
|
-
overflow: hidden;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.hb-kpi {
|
|
44
|
-
background: var(--color-bg, #0C0C10);
|
|
45
|
-
padding: 20px 24px;
|
|
46
|
-
display: flex;
|
|
47
|
-
flex-direction: column;
|
|
48
|
-
gap: 4px;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.hb-kpi-label {
|
|
52
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
53
|
-
font-size: 11px;
|
|
54
|
-
font-weight: 500;
|
|
55
|
-
text-transform: uppercase;
|
|
56
|
-
letter-spacing: 1.5px;
|
|
57
|
-
color: var(--color-muted, #6B6B78);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.hb-kpi-value {
|
|
61
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
62
|
-
font-size: 28px;
|
|
63
|
-
font-weight: 700;
|
|
64
|
-
color: var(--color-text, #EAEAEA);
|
|
65
|
-
letter-spacing: -0.5px;
|
|
66
|
-
line-height: 1.2;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.hb-kpi-delta {
|
|
70
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
71
|
-
font-size: 12px;
|
|
72
|
-
font-weight: 500;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.hb-delta-up,
|
|
76
|
-
.hb-delta-badge.hb-delta-up {
|
|
77
|
-
color: #4ADE80;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.hb-delta-down,
|
|
81
|
-
.hb-delta-badge.hb-delta-down {
|
|
82
|
-
color: #F87171;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.hb-delta-flat,
|
|
86
|
-
.hb-delta-badge.hb-delta-flat {
|
|
87
|
-
color: #FBBF24;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/* ---------------------------------------------------------------------------
|
|
91
|
-
Delta Badge (inline)
|
|
92
|
-
--------------------------------------------------------------------------- */
|
|
93
|
-
.hb-delta-badge {
|
|
94
|
-
display: inline-flex;
|
|
95
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
96
|
-
font-size: 12px;
|
|
97
|
-
font-weight: 600;
|
|
98
|
-
padding: 2px 8px;
|
|
99
|
-
border-radius: 2px;
|
|
100
|
-
border: 1px solid currentColor;
|
|
101
|
-
line-height: 1.4;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/* ---------------------------------------------------------------------------
|
|
105
|
-
Callout Boxes
|
|
106
|
-
--------------------------------------------------------------------------- */
|
|
107
|
-
.hb-callout {
|
|
108
|
-
display: flex;
|
|
109
|
-
gap: 12px;
|
|
110
|
-
align-items: flex-start;
|
|
111
|
-
padding: 16px 20px;
|
|
112
|
-
border-radius: 2px;
|
|
113
|
-
border-left: 3px solid;
|
|
114
|
-
margin-bottom: 16px;
|
|
115
|
-
font-size: 14px;
|
|
116
|
-
line-height: 1.6;
|
|
117
|
-
color: var(--color-text, #EAEAEA);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.hb-callout-icon {
|
|
121
|
-
font-size: 16px;
|
|
122
|
-
flex-shrink: 0;
|
|
123
|
-
margin-top: 1px;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.hb-callout-info {
|
|
127
|
-
background: rgba(96, 165, 250, 0.08);
|
|
128
|
-
border-color: #60A5FA;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.hb-callout-success {
|
|
132
|
-
background: rgba(74, 222, 128, 0.08);
|
|
133
|
-
border-color: #4ADE80;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.hb-callout-warning {
|
|
137
|
-
background: rgba(251, 191, 36, 0.08);
|
|
138
|
-
border-color: #FBBF24;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.hb-callout-danger {
|
|
142
|
-
background: rgba(248, 113, 113, 0.08);
|
|
143
|
-
border-color: #F87171;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/* ---------------------------------------------------------------------------
|
|
147
|
-
Scorecard
|
|
148
|
-
--------------------------------------------------------------------------- */
|
|
149
|
-
.hb-scorecard {
|
|
150
|
-
background: var(--color-surface, #151519);
|
|
151
|
-
border: 1px solid var(--color-surface, #2A2A33);
|
|
152
|
-
border-radius: 2px;
|
|
153
|
-
padding: 24px;
|
|
154
|
-
margin-bottom: 24px;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.hb-scorecard-verdict {
|
|
158
|
-
font-family: 'Space Grotesk', sans-serif;
|
|
159
|
-
font-size: 28px;
|
|
160
|
-
font-weight: 700;
|
|
161
|
-
margin: 8px 0 12px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.hb-scorecard-reason {
|
|
165
|
-
font-size: 14px;
|
|
166
|
-
color: var(--color-muted, #6B6B78);
|
|
167
|
-
line-height: 1.6;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/* ---------------------------------------------------------------------------
|
|
171
|
-
Comparison & Heatmap Tables
|
|
172
|
-
--------------------------------------------------------------------------- */
|
|
173
|
-
.hb-table-title {
|
|
174
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
175
|
-
font-size: 11px;
|
|
176
|
-
font-weight: 500;
|
|
177
|
-
text-transform: uppercase;
|
|
178
|
-
letter-spacing: 1.5px;
|
|
179
|
-
color: var(--color-muted, #6B6B78);
|
|
180
|
-
margin-bottom: 12px;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.hb-table-wrap {
|
|
184
|
-
overflow-x: auto;
|
|
185
|
-
margin-bottom: 24px;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.hb-comp-table {
|
|
189
|
-
width: 100%;
|
|
190
|
-
border-collapse: collapse;
|
|
191
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
192
|
-
font-size: 13px;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.hb-comp-table th {
|
|
196
|
-
text-align: left;
|
|
197
|
-
padding: 10px 16px;
|
|
198
|
-
font-size: 11px;
|
|
199
|
-
font-weight: 500;
|
|
200
|
-
text-transform: uppercase;
|
|
201
|
-
letter-spacing: 1px;
|
|
202
|
-
color: var(--color-muted, #6B6B78);
|
|
203
|
-
border-bottom: 1px solid var(--color-surface, #2A2A33);
|
|
204
|
-
background: var(--color-surface, #151519);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.hb-comp-table th:not(:first-child) {
|
|
208
|
-
text-align: right;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.hb-comp-table td {
|
|
212
|
-
padding: 10px 16px;
|
|
213
|
-
color: var(--color-text, #B0B0B8);
|
|
214
|
-
border-bottom: 1px solid var(--color-surface, #2A2A33);
|
|
215
|
-
font-variant-numeric: tabular-nums;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
.hb-comp-table td:not(:first-child) {
|
|
219
|
-
text-align: right;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.hb-cell-label {
|
|
223
|
-
color: var(--color-text, #EAEAEA) !important;
|
|
224
|
-
font-weight: 600;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.hb-cell-best {
|
|
228
|
-
color: #4ADE80 !important;
|
|
229
|
-
font-weight: 600;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.hb-cell-worst {
|
|
233
|
-
color: #F87171 !important;
|
|
234
|
-
opacity: 0.8;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.hb-comp-table tbody tr:hover {
|
|
238
|
-
background: rgba(234, 234, 234, 0.03);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/* ---------------------------------------------------------------------------
|
|
242
|
-
Section Headers
|
|
243
|
-
--------------------------------------------------------------------------- */
|
|
244
|
-
.hb-section-header {
|
|
245
|
-
margin: 32px 0 20px;
|
|
246
|
-
padding-top: 24px;
|
|
247
|
-
border-top: 1px solid var(--color-surface, #2A2A33);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
.hb-section-lbl {
|
|
251
|
-
display: block;
|
|
252
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
253
|
-
font-size: 11px;
|
|
254
|
-
font-weight: 500;
|
|
255
|
-
text-transform: uppercase;
|
|
256
|
-
letter-spacing: 2px;
|
|
257
|
-
color: var(--color-muted, #6B6B78);
|
|
258
|
-
margin-bottom: 8px;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.hb-section-title {
|
|
262
|
-
font-family: 'Space Grotesk', sans-serif;
|
|
263
|
-
font-size: 22px;
|
|
264
|
-
font-weight: 700;
|
|
265
|
-
color: var(--color-text, #EAEAEA);
|
|
266
|
-
margin-bottom: 8px;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.hb-section-desc {
|
|
270
|
-
font-size: 14px;
|
|
271
|
-
color: var(--color-muted, #B0B0B8);
|
|
272
|
-
line-height: 1.6;
|
|
273
|
-
max-width: 600px;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/* ---------------------------------------------------------------------------
|
|
277
|
-
Source Citation Panel
|
|
278
|
-
--------------------------------------------------------------------------- */
|
|
279
|
-
.hb-sources {
|
|
280
|
-
margin-top: 32px;
|
|
281
|
-
border-top: 1px solid var(--color-surface, #2A2A33);
|
|
282
|
-
padding-top: 16px;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.hb-sources-toggle {
|
|
286
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
287
|
-
font-size: 11px;
|
|
288
|
-
font-weight: 500;
|
|
289
|
-
text-transform: uppercase;
|
|
290
|
-
letter-spacing: 1.5px;
|
|
291
|
-
color: var(--color-muted, #6B6B78);
|
|
292
|
-
cursor: pointer;
|
|
293
|
-
padding: 4px 0;
|
|
294
|
-
list-style: none;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
.hb-sources-toggle::marker,
|
|
298
|
-
.hb-sources-toggle::-webkit-details-marker {
|
|
299
|
-
display: none;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.hb-sources-toggle::before {
|
|
303
|
-
content: '▸ ';
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
details[open] .hb-sources-toggle::before {
|
|
307
|
-
content: '▾ ';
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.hb-sources-list {
|
|
311
|
-
list-style: none;
|
|
312
|
-
margin-top: 12px;
|
|
313
|
-
display: flex;
|
|
314
|
-
flex-direction: column;
|
|
315
|
-
gap: 6px;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.hb-sources-list li {
|
|
319
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
320
|
-
font-size: 11px;
|
|
321
|
-
color: var(--color-muted, #6B6B78);
|
|
322
|
-
padding-left: 16px;
|
|
323
|
-
position: relative;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
.hb-sources-list li::before {
|
|
327
|
-
content: '—';
|
|
328
|
-
position: absolute;
|
|
329
|
-
left: 0;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/* ---------------------------------------------------------------------------
|
|
333
|
-
Timeline
|
|
334
|
-
--------------------------------------------------------------------------- */
|
|
335
|
-
.hb-timeline {
|
|
336
|
-
display: flex;
|
|
337
|
-
gap: 0;
|
|
338
|
-
overflow-x: auto;
|
|
339
|
-
padding: 16px 0;
|
|
340
|
-
margin-bottom: 24px;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
.hb-timeline-item {
|
|
344
|
-
display: flex;
|
|
345
|
-
flex-direction: column;
|
|
346
|
-
align-items: center;
|
|
347
|
-
gap: 8px;
|
|
348
|
-
min-width: 120px;
|
|
349
|
-
flex: 1;
|
|
350
|
-
position: relative;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
.hb-timeline-item:not(:last-child)::after {
|
|
354
|
-
content: '';
|
|
355
|
-
position: absolute;
|
|
356
|
-
top: 32px;
|
|
357
|
-
left: 50%;
|
|
358
|
-
width: 100%;
|
|
359
|
-
height: 1px;
|
|
360
|
-
background: var(--color-surface, #2A2A33);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
.hb-timeline-date {
|
|
364
|
-
font-family: 'IBM Plex Mono', monospace;
|
|
365
|
-
font-size: 11px;
|
|
366
|
-
color: var(--color-muted, #6B6B78);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
.hb-timeline-dot {
|
|
370
|
-
width: 8px;
|
|
371
|
-
height: 8px;
|
|
372
|
-
border-radius: 50%;
|
|
373
|
-
background: var(--color-text, #EAEAEA);
|
|
374
|
-
border: 2px solid var(--color-bg, #0C0C10);
|
|
375
|
-
z-index: 1;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
.hb-timeline-label {
|
|
379
|
-
font-family: 'Space Grotesk', sans-serif;
|
|
380
|
-
font-size: 12px;
|
|
381
|
-
font-weight: 600;
|
|
382
|
-
color: var(--color-text, #EAEAEA);
|
|
383
|
-
text-align: center;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
.hb-timeline-detail {
|
|
387
|
-
font-size: 11px;
|
|
388
|
-
color: var(--color-muted, #6B6B78);
|
|
389
|
-
text-align: center;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/* ---------------------------------------------------------------------------
|
|
393
|
-
Sparklines
|
|
394
|
-
--------------------------------------------------------------------------- */
|
|
395
|
-
.hb-sparkline {
|
|
396
|
-
display: block;
|
|
397
|
-
margin-top: 6px;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/* ---------------------------------------------------------------------------
|
|
401
|
-
Responsive
|
|
402
|
-
--------------------------------------------------------------------------- */
|
|
403
|
-
@media (max-width: 768px) {
|
|
404
|
-
.hb-grid-2 {
|
|
405
|
-
grid-template-columns: 1fr;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.hb-grid-3 {
|
|
409
|
-
grid-template-columns: 1fr;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
.hb-kpi-row {
|
|
413
|
-
grid-template-columns: repeat(2, 1fr);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.hb-kpi-value {
|
|
417
|
-
font-size: 22px;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "HedgeBoard",
|
|
3
|
-
"logo": "",
|
|
4
|
-
"colors": {
|
|
5
|
-
"primary": "#EAEAEA",
|
|
6
|
-
"accent": "#CACACA",
|
|
7
|
-
"accent_dim": "#888888",
|
|
8
|
-
"positive": "#4ADE80",
|
|
9
|
-
"negative": "#F87171",
|
|
10
|
-
"warning": "#FBBF24",
|
|
11
|
-
"background": "#0C0C10",
|
|
12
|
-
"surface": "#151519",
|
|
13
|
-
"hover": "#1E1E24",
|
|
14
|
-
"text": "#EAEAEA",
|
|
15
|
-
"text_secondary": "#B0B0B8",
|
|
16
|
-
"text_muted": "#6B6B78",
|
|
17
|
-
"border": "#2A2A33"
|
|
18
|
-
},
|
|
19
|
-
"font": {
|
|
20
|
-
"family": "Space Grotesk",
|
|
21
|
-
"headings": "Space Grotesk",
|
|
22
|
-
"mono": "IBM Plex Mono"
|
|
23
|
-
},
|
|
24
|
-
"mode": "dark"
|
|
25
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""HedgeBoard data access layer."""
|
package/hedgeboard/data/sec.py
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"""HedgeBoard SEC data client.
|
|
2
|
-
|
|
3
|
-
Provides access to SEC filings, XBRL financial facts, and company
|
|
4
|
-
metadata through the HedgeBoard API.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
from data.sec import HedgeBoardClient
|
|
8
|
-
|
|
9
|
-
hb = HedgeBoardClient()
|
|
10
|
-
company = hb.get_company("AAPL")
|
|
11
|
-
filings = hb.get_filings("AAPL", form_type="10-K")
|
|
12
|
-
facts = hb.get_xbrl("AAPL", concept="Revenues")
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import os
|
|
19
|
-
import sys
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from typing import Optional
|
|
22
|
-
|
|
23
|
-
import requests
|
|
24
|
-
from dotenv import load_dotenv
|
|
25
|
-
|
|
26
|
-
# Load .env from the hedgeboard root
|
|
27
|
-
_root = Path(__file__).resolve().parent.parent
|
|
28
|
-
load_dotenv(_root / ".env")
|
|
29
|
-
|
|
30
|
-
API_BASE = os.environ.get("HEDGEBOARD_API_URL", "https://3oy6d0glul.execute-api.us-east-1.amazonaws.com/v1")
|
|
31
|
-
API_KEY = os.environ.get("HEDGEBOARD_API_KEY", "")
|
|
32
|
-
|
|
33
|
-
# Local cache to avoid re-fetching in the same session
|
|
34
|
-
_cache: dict[str, any] = {}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class HedgeBoardClient:
|
|
38
|
-
"""Client for the HedgeBoard data API."""
|
|
39
|
-
|
|
40
|
-
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
|
41
|
-
self.api_key = api_key or API_KEY
|
|
42
|
-
self.base_url = (base_url or API_BASE).rstrip("/")
|
|
43
|
-
|
|
44
|
-
if not self.api_key:
|
|
45
|
-
print(
|
|
46
|
-
"⚠️ No API key found. Set HEDGEBOARD_API_KEY in hedgeboard/.env",
|
|
47
|
-
file=sys.stderr,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# ------------------------------------------------------------------
|
|
51
|
-
# Internal helpers
|
|
52
|
-
# ------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
def _get(self, path: str, params: dict | None = None) -> dict | list | str:
|
|
55
|
-
"""Make an authenticated GET request to the HedgeBoard API."""
|
|
56
|
-
cache_key = f"{path}|{json.dumps(params or {}, sort_keys=True)}"
|
|
57
|
-
if cache_key in _cache:
|
|
58
|
-
return _cache[cache_key]
|
|
59
|
-
|
|
60
|
-
url = f"{self.base_url}{path}"
|
|
61
|
-
headers = {"x-api-key": self.api_key}
|
|
62
|
-
resp = requests.get(url, headers=headers, params=params, timeout=30)
|
|
63
|
-
|
|
64
|
-
if resp.status_code == 403:
|
|
65
|
-
raise PermissionError("Invalid API key. Check HEDGEBOARD_API_KEY in .env")
|
|
66
|
-
if resp.status_code == 404:
|
|
67
|
-
return None
|
|
68
|
-
if resp.status_code == 429:
|
|
69
|
-
raise RuntimeError("Rate limit exceeded. Wait a moment and try again.")
|
|
70
|
-
resp.raise_for_status()
|
|
71
|
-
|
|
72
|
-
# Filing HTML comes as text, everything else as JSON
|
|
73
|
-
content_type = resp.headers.get("content-type", "")
|
|
74
|
-
if "text/html" in content_type:
|
|
75
|
-
data = resp.text
|
|
76
|
-
else:
|
|
77
|
-
data = resp.json()
|
|
78
|
-
|
|
79
|
-
_cache[cache_key] = data
|
|
80
|
-
return data
|
|
81
|
-
|
|
82
|
-
# ------------------------------------------------------------------
|
|
83
|
-
# Companies
|
|
84
|
-
# ------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
def get_company(self, ticker: str) -> dict | None:
|
|
87
|
-
"""Get company metadata by ticker.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
Dict with keys: cik, name, ticker, exchange, sic, sic_description,
|
|
91
|
-
category, entity_type, state, etc. None if not found.
|
|
92
|
-
"""
|
|
93
|
-
return self._get("/v1/companies", params={"ticker": ticker.upper()})
|
|
94
|
-
|
|
95
|
-
def search_companies(self, query: str, limit: int = 20) -> list[dict]:
|
|
96
|
-
"""Search companies by name, ticker, or sector.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
List of matching company dicts.
|
|
100
|
-
"""
|
|
101
|
-
result = self._get(
|
|
102
|
-
"/v1/companies/search",
|
|
103
|
-
params={"q": query, "limit": limit},
|
|
104
|
-
)
|
|
105
|
-
return result if result else []
|
|
106
|
-
|
|
107
|
-
# ------------------------------------------------------------------
|
|
108
|
-
# Filings
|
|
109
|
-
# ------------------------------------------------------------------
|
|
110
|
-
|
|
111
|
-
def get_filings(
|
|
112
|
-
self,
|
|
113
|
-
ticker: str,
|
|
114
|
-
form_type: str | None = None,
|
|
115
|
-
limit: int = 10,
|
|
116
|
-
) -> list[dict]:
|
|
117
|
-
"""Get SEC filing metadata for a company.
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
ticker: Company ticker symbol.
|
|
121
|
-
form_type: Filter by form type (e.g., "10-K", "10-Q").
|
|
122
|
-
limit: Maximum number of filings to return.
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
List of filing dicts with keys: filing_date, form_type,
|
|
126
|
-
accession_number, s3_key, primary_doc_url, etc.
|
|
127
|
-
Sorted by filing_date descending (newest first).
|
|
128
|
-
"""
|
|
129
|
-
params = {"ticker": ticker.upper(), "limit": limit}
|
|
130
|
-
if form_type:
|
|
131
|
-
params["type"] = form_type
|
|
132
|
-
result = self._get("/v1/filings", params=params)
|
|
133
|
-
return result if result else []
|
|
134
|
-
|
|
135
|
-
def get_filing_html(self, s3_key: str) -> str | None:
|
|
136
|
-
"""Get the raw cleaned HTML of a filing document.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
s3_key: The S3 key from a filing record (e.g., "raw/...").
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
HTML string, or None if not found.
|
|
143
|
-
"""
|
|
144
|
-
return self._get("/v1/filings/html", params={"key": s3_key})
|
|
145
|
-
|
|
146
|
-
# ------------------------------------------------------------------
|
|
147
|
-
# XBRL Financial Facts
|
|
148
|
-
# ------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
def get_xbrl(
|
|
151
|
-
self,
|
|
152
|
-
ticker: str,
|
|
153
|
-
concept: str | None = None,
|
|
154
|
-
form_type: str | None = None,
|
|
155
|
-
limit: int = 40,
|
|
156
|
-
) -> list[dict]:
|
|
157
|
-
"""Get structured XBRL financial facts for a company.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
ticker: Company ticker symbol.
|
|
161
|
-
concept: XBRL concept name to filter (e.g., "Revenues",
|
|
162
|
-
"NetIncomeLoss", "EarningsPerShareBasic").
|
|
163
|
-
If None, returns all concepts.
|
|
164
|
-
form_type: Filter by form type (e.g., "10-K" for annual only).
|
|
165
|
-
limit: Maximum number of facts to return.
|
|
166
|
-
|
|
167
|
-
Returns:
|
|
168
|
-
List of fact dicts with keys: concept, label, value, unit,
|
|
169
|
-
period_start, period_end, form, filed, taxonomy.
|
|
170
|
-
Sorted by period_end descending (newest first).
|
|
171
|
-
"""
|
|
172
|
-
params = {"ticker": ticker.upper(), "limit": limit}
|
|
173
|
-
if concept:
|
|
174
|
-
params["concept"] = concept
|
|
175
|
-
if form_type:
|
|
176
|
-
params["form"] = form_type
|
|
177
|
-
result = self._get("/v1/xbrl", params=params)
|
|
178
|
-
return result if result else []
|
|
179
|
-
|
|
180
|
-
def get_key_financials(
|
|
181
|
-
self,
|
|
182
|
-
ticker: str,
|
|
183
|
-
form_type: str = "10-K",
|
|
184
|
-
periods: int = 5,
|
|
185
|
-
) -> dict[str, list[dict]]:
|
|
186
|
-
"""Convenience: get key financial metrics for a company.
|
|
187
|
-
|
|
188
|
-
Returns a dict mapping concept name to list of fact records:
|
|
189
|
-
{
|
|
190
|
-
"Revenues": [{period_end, value, ...}, ...],
|
|
191
|
-
"NetIncomeLoss": [...],
|
|
192
|
-
"EarningsPerShareBasic": [...],
|
|
193
|
-
"Assets": [...],
|
|
194
|
-
"StockholdersEquity": [...],
|
|
195
|
-
}
|
|
196
|
-
"""
|
|
197
|
-
key_concepts = [
|
|
198
|
-
"Revenues",
|
|
199
|
-
"RevenueFromContractWithCustomerExcludingAssessedTax",
|
|
200
|
-
"NetIncomeLoss",
|
|
201
|
-
"EarningsPerShareBasic",
|
|
202
|
-
"EarningsPerShareDiluted",
|
|
203
|
-
"Assets",
|
|
204
|
-
"Liabilities",
|
|
205
|
-
"StockholdersEquity",
|
|
206
|
-
"OperatingIncomeLoss",
|
|
207
|
-
"CashAndCashEquivalentsAtCarryingValue",
|
|
208
|
-
]
|
|
209
|
-
|
|
210
|
-
results: dict[str, list[dict]] = {}
|
|
211
|
-
for concept in key_concepts:
|
|
212
|
-
facts = self.get_xbrl(
|
|
213
|
-
ticker, concept=concept, form_type=form_type, limit=periods
|
|
214
|
-
)
|
|
215
|
-
if facts:
|
|
216
|
-
results[concept] = facts
|
|
217
|
-
|
|
218
|
-
return results
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""HedgeBoard analysis modules."""
|