Skip to content

Commit ae3e0bb

Browse files
committed
v0.1.3
1 parent f73e058 commit ae3e0bb

13 files changed

Lines changed: 168 additions & 143 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
1414
### Removed -->
1515

16+
## [0.1.3] - 2025-12-26
17+
18+
### Changed
19+
20+
- Better error reporting, and general refactor
21+
1622
## [0.1.2] - 2025-12-24
1723

18-
### Added
24+
### Changed
1925

2026
- Enable Quiver scrapper by default
2127

@@ -24,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2430
### Added
2531

2632
- Better error reporting
33+
34+
### Changed
35+
2736
- Reddit time window set to 10 min
2837

2938
## [0.1.0] - 2025-12-23
@@ -32,7 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3241

3342
- First working version
3443

35-
[unreleased]: https://github.com/alejandromav/quantarded/compare/v0.1.2...HEAD
44+
[unreleased]: https://github.com/alejandromav/quantarded/compare/v0.1.3...HEAD
45+
[0.1.3]: https://github.com/alejandromav/quantarded/compare/v0.2.1...v0.1.3
3646
[0.1.2]: https://github.com/alejandromav/quantarded/compare/v0.1.1...v0.1.2
3747
[0.1.1]: https://github.com/alejandromav/quantarded/compare/v0.1.0...v0.1.1
3848
[0.1.0]: https://github.com/alejandromav/quantarded/releases/tag/v0.1.0

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,17 @@ src/
119119
│ ├── normalize.ts # Reddit event normalization
120120
│ ├── normalize-quiver.ts # Quiver trade normalization
121121
│ ├── reddit.ts # Reddit API client with pagination
122-
│ ├── quiver.ts # Quiver API client with pagination
122+
│ ├── quiver.ts # Quiver API client with pagination
123123
│ ├── job-runner.ts # Common job utilities (summary, job IDs)
124124
│ └── tinybird.ts # Tinybird API client
125125
├── scripts/ # CLI entry points
126126
│ ├── reddit-scraper.ts # Reddit scraper script
127-
│ ├── quiver-scraper.ts # Quiver scraper script
128-
│ └── print-config.ts # Configuration printer
127+
│ ├── quiver-scraper.ts # Quiver scraper script
128+
│ └── print-config.ts # Configuration printer
129+
├── utils/ # Utility functions
130+
│ └── utils.ts # HTTP client, hashing, date utilities
129131
├── config.ts # Configuration constants
130-
├── types.ts # TypeScript type definitions
131-
└── utils.ts # Utility functions
132+
└── types.ts # TypeScript type definitions
132133
tinybird/ # Tinybird datasource schemas
133134
├── events_landing__v0.datasource # Events datasource
134135
├── job_runs__v0.datasource # Job runs datasource

infra/scripts/deploy-to-server.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ services:
3838
TIME_WINDOW_MINUTES: "10"
3939
SCRAPER_INTERVAL_MINUTES: "5"
4040
CLASSIFY_CONCURRENCY: "3"
41-
QUIVER_SCRAPER_ENABLED: "false"
4241
QUIVER_SCRAPER_CRON: "0 */6 * * *"
4342
NODE_ENV: "production"
4443
logging:

src/config.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,13 @@ export const CONFIG = {
1919
tinybirdDatasource: process.env.TINYBIRD_DATASOURCE ?? "events_landing__v0",
2020
tinybirdJobRunsDatasource: process.env.TINYBIRD_JOB_RUNS_DATASOURCE ?? "job_runs__v0",
2121
scraperIntervalMinutes: parseInt(process.env.SCRAPER_INTERVAL_MINUTES ?? "5", 10),
22-
// Quiver Quant Configuration
2322
quiverApiKey: process.env.QUIVER_API_KEY ?? "",
2423
quiverApiUrl: process.env.QUIVER_API_URL ?? "https://api.quiverquant.com/beta",
2524
quiverCongressionalEndpoint: process.env.QUIVER_CONGRESSIONAL_ENDPOINT ?? "bulk/congresstrading",
2625
quiverPageSize: parseInt(process.env.QUIVER_PAGE_SIZE ?? "100", 10),
2726
quiverBatchSize: parseInt(process.env.QUIVER_BATCH_SIZE ?? "100", 10),
28-
// Job scheduling
2927
redditScraperEnabled: process.env.REDDIT_SCRAPER_ENABLED === "true",
3028
quiverScraperEnabled: process.env.QUIVER_SCRAPER_ENABLED === "true",
31-
quiverScraperCron: process.env.QUIVER_SCRAPER_CRON ?? "0 */6 * * *", // Default: every 6 hours
32-
// Reddit proxy (optional, only used for Reddit requests to minimize traffic)
29+
quiverScraperCron: process.env.QUIVER_SCRAPER_CRON ?? "0 */6 * * *",
3330
redditProxy: process.env.REDDIT_PROXY ?? "",
3431
} as const;

src/lib/classify.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,20 +135,16 @@ export async function classifyBatch(items: ClassifyItem[]): Promise<ClassifyResu
135135
);
136136
}
137137

138-
// Handle case where LLM returns a single object instead of { results: [...] }
139-
// This can happen when there's only one item or the LLM doesn't follow the format exactly
140138
if (!parsed || typeof parsed !== "object") {
141139
console.error(
142140
`[CLASSIFY] Invalid response: not an object. Content: ${content.substring(0, 500)}`
143141
);
144-
// Return empty results for all items instead of crashing
145142
return items.map((item) => ({
146143
item_id: item.item_id,
147144
tickers: [],
148145
}));
149146
}
150147

151-
// Type guard for single result object
152148
type SingleResult = {
153149
item_id: string;
154150
tickers: Array<{
@@ -179,7 +175,6 @@ export async function classifyBatch(items: ClassifyItem[]): Promise<ClassifyResu
179175
);
180176
}
181177

182-
// Check if it's a single result object (missing results wrapper)
183178
let classifyResponse: ClassifyResponse;
184179
if (isSingleResult(parsed)) {
185180
console.error(
@@ -192,17 +187,14 @@ export async function classifyBatch(items: ClassifyItem[]): Promise<ClassifyResu
192187
console.error(
193188
`[CLASSIFY] Invalid response structure: ${JSON.stringify(parsed).substring(0, 500)}`
194189
);
195-
// Return empty results for all items instead of crashing
196190
return items.map((item) => ({
197191
item_id: item.item_id,
198192
tickers: [],
199193
}));
200194
}
201195

202-
// Create a map of results by item_id for quick lookup
203196
const resultsMap = new Map<string, ClassifyResult>();
204197
for (const resultItem of classifyResponse.results) {
205-
// Type guard to ensure result has the expected structure
206198
if (
207199
!resultItem ||
208200
typeof resultItem !== "object" ||
@@ -260,7 +252,6 @@ export async function classifyBatch(items: ClassifyItem[]): Promise<ClassifyResu
260252
});
261253
}
262254

263-
// Ensure all items in the batch have a result, even if empty
264255
return items.map((item) => {
265256
const result = resultsMap.get(item.item_id);
266257
return (

src/lib/job-runner.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ export type SummaryItem = {
55
value: string;
66
};
77

8-
/**
9-
* Formats and prints a job execution summary table.
10-
*/
118
export function printJobSummary(summary: SummaryItem[]): void {
129
if (summary.length === 0) {
1310
return;
@@ -29,25 +26,15 @@ export function printJobSummary(summary: SummaryItem[]): void {
2926
console.error("");
3027
}
3128

32-
/**
33-
* Creates a deterministic job ID from job type and timestamp.
34-
*/
3529
export function createJobId(jobType: string, timestamp: Date): string {
3630
return sha256(`job|${jobType}|${timestamp.toISOString()}`);
3731
}
3832

39-
/**
40-
* Formats elapsed time in seconds with 2 decimal places.
41-
*/
4233
export function formatElapsedSeconds(startedAt: Date, endedAt: Date): string {
4334
const elapsedMs = endedAt.getTime() - startedAt.getTime();
4435
return (elapsedMs / 1000).toFixed(2);
4536
}
4637

47-
/**
48-
* Formats an error into a detailed error message string for job tracking.
49-
* Includes stack trace if available and truncates to maxLength to avoid payload size issues.
50-
*/
5138
export function formatErrorMessage(err: unknown, maxLength = 5000): string {
5239
let errorMessage: string;
5340
if (err instanceof Error) {

src/lib/normalize-quiver.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,33 @@
11
import { sha256 } from "../utils/utils.js";
22
import type { NormalizedEvent, QuiverTrade } from "../types.js";
33

4-
/**
5-
* Normalizes a Quiver congressional trade into a NormalizedEvent.
6-
* Creates a stable event_id from trade data and preserves the full Quiver payload.
7-
* Maintains the same schema structure for Tinybird compatibility.
8-
*
9-
* @param trade The Quiver trade data
10-
* @param ingestedAt When the trade was ingested
11-
* @param disclosedAt The date used to query the API (YYYYMMDD format)
12-
*/
134
export function normalizeQuiverTrade(
145
trade: QuiverTrade,
156
ingestedAt: Date,
167
disclosedAt?: string
178
): NormalizedEvent {
18-
// Create stable event ID from trade fields
19-
// Use ticker, transaction_date, representative, and amount for uniqueness
209
const ticker = trade.Ticker || trade.ticker || trade.Symbol || trade.symbol || "UNKNOWN";
2110
const transactionDate = trade.TransactionDate || trade.transaction_date || trade.Date || "";
2211
const representative = trade.Representative || trade.representative || trade.Name || "";
2312
const amount = trade.Amount || trade.amount || "";
2413
const transactionType = trade.Transaction || trade.transaction || trade.TransactionType || "";
2514

26-
// Create deterministic ID from stable fields
2715
const eventIdInput = `quiver-daily|${ticker}|${transactionDate}|${representative}|${amount}|${transactionType}`;
2816
const eventId = sha256(eventIdInput);
2917

30-
// Use transaction_date if available, otherwise use ingested_at
3118
let timestamp: string;
3219
const dateField = transactionDate || trade.Date || trade.date;
3320
if (dateField) {
3421
try {
35-
// Quiver dates might be in various formats
3622
const date = new Date(dateField);
37-
if (!isNaN(date.getTime())) {
38-
timestamp = date.toISOString();
39-
} else {
40-
timestamp = ingestedAt.toISOString();
41-
}
23+
timestamp = !isNaN(date.getTime()) ? date.toISOString() : ingestedAt.toISOString();
4224
} catch {
4325
timestamp = ingestedAt.toISOString();
4426
}
4527
} else {
4628
timestamp = ingestedAt.toISOString();
4729
}
4830

49-
// Convert disclosedAt from YYYYMMDD to ISO format if provided
5031
let disclosedAtIso: string | undefined;
5132
if (disclosedAt && disclosedAt.length === 8) {
5233
try {
@@ -58,7 +39,7 @@ export function normalizeQuiverTrade(
5839
disclosedAtIso = date.toISOString();
5940
}
6041
} catch {
61-
// If conversion fails, leave undefined
42+
// Conversion failed, leave undefined
6243
}
6344
}
6445

@@ -69,9 +50,7 @@ export function normalizeQuiverTrade(
6950
timestamp,
7051
version: "1",
7152
payload: {
72-
// Preserve full Quiver trade data
7353
...trade,
74-
// Add metadata
7554
ingested_at: ingestedAt.toISOString(),
7655
...(disclosedAtIso && { disclosed_at: disclosedAtIso }),
7756
},

src/lib/normalize.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export function normalize(child: RedditListingChild): NormalizedEvent {
1313
const body = child.kind === "t3" ? child.data.selftext || "" : child.data.body || "";
1414
let content = [title, body].filter(Boolean).join(" ").trim();
1515

16-
// Truncate content to max length to avoid excessive token usage
1716
if (content.length > CONFIG.maxContentLength) {
1817
content = content.substring(0, CONFIG.maxContentLength - 3).trim() + "...";
1918
}

src/lib/quiver.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,13 @@ export type OnPageCallback = (
2020
disclosedAt: string
2121
) => Promise<void>;
2222

23-
/**
24-
* Formats a date as YYYYMMDD string.
25-
*/
2623
function formatDateYYYYMMDD(date: Date): string {
2724
const year = date.getFullYear();
2825
const month = String(date.getMonth() + 1).padStart(2, "0");
2926
const day = String(date.getDate()).padStart(2, "0");
3027
return `${year}${month}${day}`;
3128
}
3229

33-
/**
34-
* Builds request headers for Quiver API.
35-
*/
3630
function buildHeaders(): Record<string, string> {
3731
return {
3832
Accept: "application/json",
@@ -46,21 +40,16 @@ function buildHeaders(): Record<string, string> {
4640
};
4741
}
4842

49-
/**
50-
* Handles HTTP error responses from Quiver API.
51-
* @returns true if the error was handled and we should retry, false otherwise
52-
*/
5343
async function handleErrorResponse(
5444
response: Response,
5545
page: number,
5646
errorText: string
5747
): Promise<boolean> {
5848
if (response.status === 429) {
59-
let waitSeconds = 30; // Default fallback
49+
let waitSeconds = 30;
6050
try {
6151
const errorJson = JSON.parse(errorText) as { detail?: string };
6252
const detail = errorJson.detail || "";
63-
// Extract "Expected available in X seconds" from the detail message
6453
const match = detail.match(/Expected available in (\d+)\s*seconds?/i);
6554
if (match && match[1]) {
6655
const parsedSeconds = parseInt(match[1], 10);
@@ -69,14 +58,14 @@ async function handleErrorResponse(
6958
}
7059
}
7160
} catch {
72-
// If parsing fails, use default wait time
61+
// Use default wait time if parsing fails
7362
}
7463

7564
console.error(
7665
`[QUIVER] Page ${page}: Rate limited (429). Waiting ${waitSeconds} seconds before retry...`
7766
);
7867
await sleep(waitSeconds * 1000);
79-
return true; // Indicates we should retry
68+
return true;
8069
}
8170

8271
if (response.status === 500) {
@@ -94,14 +83,6 @@ async function handleErrorResponse(
9483
throw new Error(`HTTP ${response.status} ${response.statusText}: ${errorText}`);
9584
}
9685

97-
/**
98-
* Fetches all congressional trades from Quiver Quant API.
99-
* Uses the bulk congressional trading endpoint.
100-
* See: https://api.quiverquant.com/docs/#/operations/beta_bulk_congresstrading_retrieve
101-
*
102-
* @param onPage Optional callback to process each page as it's fetched (for memory efficiency)
103-
* @returns Promise with total trades fetched and pages fetched
104-
*/
10586
export async function fetchCongressionalTradesPaginated(
10687
onPage?: OnPageCallback
10788
): Promise<FetchCongressionalTradesResult> {
@@ -116,7 +97,6 @@ export async function fetchCongressionalTradesPaginated(
11697
const pageSize = CONFIG.quiverPageSize || 100;
11798
const headers = buildHeaders();
11899

119-
// Use yesterday's date (ensures complete data for the day)
120100
const targetDate = new Date();
121101
targetDate.setDate(targetDate.getDate() - 1);
122102
const dateStr = formatDateYYYYMMDD(targetDate);
@@ -157,7 +137,6 @@ export async function fetchCongressionalTradesPaginated(
157137

158138
totalTradesFetched += data.length;
159139

160-
// Process page immediately if callback provided (memory efficient)
161140
if (onPage) {
162141
await onPage(data, page, dateStr);
163142
} else {
@@ -176,16 +155,13 @@ export async function fetchCongressionalTradesPaginated(
176155
`[QUIVER] Page ${page}: Fetched ${data.length} trades (${totalTradesFetched} total)`
177156
);
178157

179-
// Add delay between requests to respect rate limits (1-3 seconds)
180158
await sleep(Math.floor(Math.random() * 2000) + 1000);
181159
} catch (error) {
182160
const errorMessage = error instanceof Error ? error.message : String(error);
183-
// If it's a 404 or empty response, we've reached the end
184161
if (errorMessage.includes("404") || errorMessage.includes("No data")) {
185162
console.error(`[QUIVER] Page ${page}: No more data available, stopping pagination`);
186163
break;
187164
}
188-
// For other errors, log and stop (could be rate limit or API issue)
189165
console.error(`[QUIVER] Page ${page}: Error fetching data: ${errorMessage}`);
190166
throw error;
191167
}

src/lib/reddit.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ async function fetchPaginated(
2121

2222
console.error(`[REDDIT] ${label}: Fetching page ${pageCount}...`);
2323

24-
// Add small random delay to mimic human behavior (100-500ms)
2524
if (pageCount > 1) {
2625
const delay = Math.floor(Math.random() * 400) + 100;
2726
await new Promise((resolve) => setTimeout(resolve, delay));
@@ -49,7 +48,7 @@ async function fetchPaginated(
4948

5049
if (itemsInWindow === 0) {
5150
console.error(
52-
`[REDDIT] ${label}: Page ${pageCount}: No items in window, stopping pagination (Reddit returns items sorted by time, so all subsequent pages are older)`
51+
`[REDDIT] ${label}: Page ${pageCount}: No items in window, stopping pagination`
5352
);
5453
break;
5554
}

0 commit comments

Comments
 (0)