agent-finance-cli 0.1.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 (48) hide show
  1. package/Cargo.lock +2632 -0
  2. package/Cargo.toml +31 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/LICENSE-MIT +21 -0
  5. package/README.md +119 -0
  6. package/bin/agent-finance.js +27 -0
  7. package/npm/check-binary-links.js +50 -0
  8. package/npm/check-package.js +39 -0
  9. package/npm/create-platform-package.js +90 -0
  10. package/npm/platform.js +33 -0
  11. package/npm/postinstall.js +62 -0
  12. package/npm/resolve-binary.js +38 -0
  13. package/package.json +54 -0
  14. package/skills/core-full.md +74 -0
  15. package/skills/core.md +59 -0
  16. package/skills/futures.md +18 -0
  17. package/skills/history-indicators.md +42 -0
  18. package/skills/price.md +40 -0
  19. package/skills/providers.md +25 -0
  20. package/skills/research-data.md +34 -0
  21. package/src/app.rs +642 -0
  22. package/src/cache.rs +67 -0
  23. package/src/cli.rs +651 -0
  24. package/src/history.rs +150 -0
  25. package/src/http.rs +76 -0
  26. package/src/indicators.rs +82 -0
  27. package/src/lib.rs +15 -0
  28. package/src/main.rs +4 -0
  29. package/src/model.rs +347 -0
  30. package/src/output.rs +544 -0
  31. package/src/page_read.rs +443 -0
  32. package/src/price.rs +255 -0
  33. package/src/providers/binance_futures.rs +342 -0
  34. package/src/providers/capabilities.rs +322 -0
  35. package/src/providers/cnbc.rs +302 -0
  36. package/src/providers/mod.rs +117 -0
  37. package/src/providers/robinhood.rs +580 -0
  38. package/src/providers/sec_edgar.rs +399 -0
  39. package/src/providers/stooq/catalog.rs +159 -0
  40. package/src/providers/stooq.rs +904 -0
  41. package/src/providers/yahoo.rs +836 -0
  42. package/src/research/fetchers.rs +111 -0
  43. package/src/research/highlights.rs +345 -0
  44. package/src/research/mod.rs +943 -0
  45. package/src/research/tests.rs +42 -0
  46. package/src/skills.rs +58 -0
  47. package/src/stream.rs +356 -0
  48. package/src/time.rs +21 -0
package/src/output.rs ADDED
@@ -0,0 +1,544 @@
1
+ use std::collections::BTreeMap;
2
+
3
+ use anyhow::Result;
4
+
5
+ use crate::model::{
6
+ DerivedIndicator, FuturesStats, HistoryBatch, PricePoint, PriceSummary, ProviderProfile,
7
+ ResearchReport, SearchReport, StreamQuote,
8
+ };
9
+ use crate::page_read::PageReadReport;
10
+
11
+ pub fn print_price_summary(summary: &PriceSummary, show_all: bool) {
12
+ println!(
13
+ "{} price summary fetched={} tz={}",
14
+ summary.symbol, summary.fetched_at_local, summary.timezone
15
+ );
16
+ if let Some(current) = summary.current.as_ref() {
17
+ println!(
18
+ "Current: {} {} session={} source={} change={} time={}",
19
+ currency(current.currency.as_deref()),
20
+ money_value(current.price),
21
+ current.session.as_deref().unwrap_or("-"),
22
+ current.provider,
23
+ pct_value(current.change_pct),
24
+ current.market_time_local.as_deref().unwrap_or("-")
25
+ );
26
+ } else {
27
+ println!("Current: no quote available");
28
+ }
29
+ println!(
30
+ "Regular basis: prev_close={} open={} high={} low={} volume={}",
31
+ money_value(summary.regular_basis.previous_close),
32
+ money_value(summary.regular_basis.open),
33
+ money_value(summary.regular_basis.high),
34
+ money_value(summary.regular_basis.low),
35
+ number_value(summary.regular_basis.volume.map(|value| value as f64))
36
+ );
37
+ if let Some(proxy) = summary.proxy.as_ref() {
38
+ println!(
39
+ "Proxy: {} {} via {} time={} note={}",
40
+ currency(proxy.currency.as_deref()),
41
+ money_value(proxy.price),
42
+ proxy.provider,
43
+ proxy.market_time_local.as_deref().unwrap_or("-"),
44
+ proxy.note.as_deref().unwrap_or("-")
45
+ );
46
+ }
47
+ if show_all {
48
+ println!();
49
+ println!("Session / provider split");
50
+ let headers = [
51
+ "label", "price", "chg%", "session", "provider", "time", "open", "high", "low",
52
+ "volume",
53
+ ];
54
+ let rows = summary
55
+ .sessions
56
+ .iter()
57
+ .map(price_point_row)
58
+ .collect::<Vec<_>>();
59
+ print_table(&headers, &rows);
60
+ } else if summary.sessions.len() > 1 {
61
+ println!(
62
+ "Note: fetched {} session/provider rows; use sessions to inspect the split.",
63
+ summary.sessions.len()
64
+ );
65
+ }
66
+ if !summary.errors.is_empty() {
67
+ println!();
68
+ println!("Quote errors");
69
+ for (provider, error) in &summary.errors {
70
+ println!("{provider}: {error}");
71
+ }
72
+ }
73
+ }
74
+
75
+ pub fn print_history_table(history: &HistoryBatch) {
76
+ println!(
77
+ "{} history via {} interval={} adjustment={} actions={} repair_requested={} repair_applied={}",
78
+ history.symbol,
79
+ history.provider,
80
+ history.interval,
81
+ history.adjustment,
82
+ history.actions_included,
83
+ history.repair_requested,
84
+ history.repair_applied
85
+ );
86
+ let headers = [
87
+ "time",
88
+ "open",
89
+ "high",
90
+ "low",
91
+ "close",
92
+ "adj_close",
93
+ "volume",
94
+ "dividend",
95
+ "split",
96
+ "gain",
97
+ "repair",
98
+ ];
99
+ let rows = history
100
+ .bars
101
+ .iter()
102
+ .map(|bar| {
103
+ vec![
104
+ bar.open_time.clone(),
105
+ money_value(bar.open),
106
+ money_value(bar.high),
107
+ money_value(bar.low),
108
+ money_value(Some(bar.close)),
109
+ money_value(bar.adj_close),
110
+ number_value(bar.volume),
111
+ money_value(bar.dividend),
112
+ number_value(bar.stock_split),
113
+ money_value(bar.capital_gain),
114
+ if bar.repaired { "yes" } else { "-" }.to_string(),
115
+ ]
116
+ })
117
+ .collect::<Vec<_>>();
118
+ print_table(&headers, &rows);
119
+ }
120
+
121
+ pub fn print_indicator_table(indicators: &[DerivedIndicator], errors: &BTreeMap<String, String>) {
122
+ let headers = [
123
+ "symbol", "close", "1bar", "5bar", "20bar", "sma20", "sma50", "hi20", "lo20", "rv20",
124
+ ];
125
+ let mut rows = indicators
126
+ .iter()
127
+ .map(|indicator| {
128
+ vec![
129
+ indicator.symbol.clone(),
130
+ money_value(indicator.latest_close),
131
+ pct_value(indicator.return_1_bar_pct),
132
+ pct_value(indicator.return_5_bar_pct),
133
+ pct_value(indicator.return_20_bar_pct),
134
+ money_value(indicator.sma_20),
135
+ money_value(indicator.sma_50),
136
+ money_value(indicator.high_20),
137
+ money_value(indicator.low_20),
138
+ pct_value(indicator.realized_vol_20_annualized_pct),
139
+ ]
140
+ })
141
+ .collect::<Vec<_>>();
142
+
143
+ for (symbol, error) in errors {
144
+ rows.push(vec![
145
+ symbol.clone(),
146
+ "ERROR".to_string(),
147
+ "-".to_string(),
148
+ "-".to_string(),
149
+ "-".to_string(),
150
+ "-".to_string(),
151
+ "-".to_string(),
152
+ "-".to_string(),
153
+ "-".to_string(),
154
+ error.clone(),
155
+ ]);
156
+ }
157
+ print_table(&headers, &rows);
158
+ }
159
+
160
+ pub fn print_futures_stats(stats: &FuturesStats) {
161
+ println!(
162
+ "{} futures stats via {} fetched_at={}",
163
+ stats.symbol, stats.provider, stats.fetched_at_utc
164
+ );
165
+ if let Some(ticker) = stats.ticker_24h.as_ref() {
166
+ println!(
167
+ "24h: last={} change={} high={} low={} quote_volume={} trades={}",
168
+ money_value(ticker.last_price),
169
+ pct_value(ticker.price_change_pct),
170
+ money_value(ticker.high_price),
171
+ money_value(ticker.low_price),
172
+ number_value(ticker.quote_volume),
173
+ ticker
174
+ .count
175
+ .map(|value| value.to_string())
176
+ .unwrap_or_else(|| "-".to_string())
177
+ );
178
+ }
179
+ if let Some(mark) = stats.mark_price.as_ref() {
180
+ println!(
181
+ "mark: mark={} index={} funding={} next_funding={}",
182
+ money_value(mark.mark_price),
183
+ money_value(mark.index_price),
184
+ pct_value(mark.last_funding_rate.map(|value| value * 100.0)),
185
+ mark.next_funding_time.as_deref().unwrap_or("-")
186
+ );
187
+ }
188
+ if let Some(open_interest) = stats.open_interest.as_ref() {
189
+ println!(
190
+ "open_interest: {} time={}",
191
+ number_value(open_interest.open_interest),
192
+ open_interest.time.as_deref().unwrap_or("-")
193
+ );
194
+ }
195
+ if !stats.funding_rates.is_empty() {
196
+ println!("recent funding:");
197
+ let headers = ["time", "rate", "mark"];
198
+ let rows = stats
199
+ .funding_rates
200
+ .iter()
201
+ .map(|row| {
202
+ vec![
203
+ row.funding_time.clone().unwrap_or_else(|| "-".to_string()),
204
+ pct_value(row.funding_rate.map(|value| value * 100.0)),
205
+ money_value(row.mark_price),
206
+ ]
207
+ })
208
+ .collect::<Vec<_>>();
209
+ print_table(&headers, &rows);
210
+ }
211
+ if !stats.errors.is_empty() {
212
+ println!("errors:");
213
+ for (name, error) in &stats.errors {
214
+ println!("{name}: {error}");
215
+ }
216
+ }
217
+ }
218
+
219
+ pub fn print_page_read_report(report: &PageReadReport) {
220
+ println!(
221
+ "URL reader via {} fetched={} words={} chars={} truncated={}",
222
+ report.provider,
223
+ report.fetched_at_utc,
224
+ report.word_count,
225
+ report.char_count,
226
+ if report.truncated { "yes" } else { "no" }
227
+ );
228
+ println!("url: {}", report.url);
229
+ println!("source_url: {}", report.source_url);
230
+ if let Some(title) = report.title.as_deref() {
231
+ println!("title: {title}");
232
+ }
233
+ if !report.errors.is_empty() {
234
+ println!();
235
+ println!("Fallback errors:");
236
+ for error in &report.errors {
237
+ println!("{}: {}", error.provider, error.error);
238
+ }
239
+ }
240
+ println!();
241
+ println!("{}", report.content);
242
+ }
243
+
244
+ pub fn print_research_report(report: &ResearchReport, raw: bool) -> Result<()> {
245
+ println!(
246
+ "{} {} fetched={}",
247
+ report.symbol, report.category, report.fetched_at_local
248
+ );
249
+ if !report.sources.is_empty() {
250
+ println!(
251
+ "sources: {}",
252
+ report
253
+ .sources
254
+ .iter()
255
+ .map(|source| format!("{}:{}", source.provider, source.cache_status))
256
+ .collect::<Vec<_>>()
257
+ .join(", ")
258
+ );
259
+ }
260
+ if !report.modules.is_empty() {
261
+ println!(
262
+ "modules: {}",
263
+ report
264
+ .modules
265
+ .iter()
266
+ .map(|module| format!("{}:{}:{}", module.provider, module.name, module.status))
267
+ .collect::<Vec<_>>()
268
+ .join(", ")
269
+ );
270
+ }
271
+ if report.highlights.is_empty() {
272
+ println!("No highlights extracted; use --json to inspect the raw payload.");
273
+ } else {
274
+ let headers = ["source", "module", "field", "value"];
275
+ let rows = report
276
+ .highlights
277
+ .iter()
278
+ .map(|row| {
279
+ vec![
280
+ row.provider.clone(),
281
+ row.module.clone(),
282
+ row.label.clone(),
283
+ row.value.clone(),
284
+ ]
285
+ })
286
+ .collect::<Vec<_>>();
287
+ print_table(&headers, &rows);
288
+ }
289
+ if !report.coverage_gaps.is_empty() {
290
+ println!();
291
+ println!("Coverage Gaps");
292
+ println!("-------------");
293
+ for gap in &report.coverage_gaps {
294
+ println!("{}: {}", gap.module, gap.reason);
295
+ }
296
+ }
297
+ if raw {
298
+ println!();
299
+ println!("{}", serde_json::to_string_pretty(&report.payload)?);
300
+ }
301
+ Ok(())
302
+ }
303
+
304
+ pub fn print_search_report(report: &SearchReport, raw: bool) -> Result<()> {
305
+ println!(
306
+ "{} {} via {} fetched={} cache={}",
307
+ report.category,
308
+ report.query,
309
+ report.provider,
310
+ report.fetched_at_local,
311
+ report.cache_status
312
+ );
313
+ if report.highlights.is_empty() {
314
+ println!("No highlights extracted; use --json to inspect the raw payload.");
315
+ } else {
316
+ let headers = ["source", "item", "value"];
317
+ let rows = report
318
+ .highlights
319
+ .iter()
320
+ .map(|row| vec![row.provider.clone(), row.label.clone(), row.value.clone()])
321
+ .collect::<Vec<_>>();
322
+ print_table(&headers, &rows);
323
+ }
324
+ if raw {
325
+ println!();
326
+ println!("{}", serde_json::to_string_pretty(&report.payload)?);
327
+ }
328
+ Ok(())
329
+ }
330
+
331
+ pub fn print_provider_profiles(profiles: &[ProviderProfile]) {
332
+ let headers = [
333
+ "provider",
334
+ "key",
335
+ "official",
336
+ "stability",
337
+ "large",
338
+ "best_for",
339
+ ];
340
+ let rows = profiles
341
+ .iter()
342
+ .map(|profile| {
343
+ vec![
344
+ profile.provider.clone(),
345
+ if profile.requires_api_key {
346
+ "required".to_string()
347
+ } else {
348
+ "no".to_string()
349
+ },
350
+ profile.official_status.clone(),
351
+ profile.stability.clone(),
352
+ profile.large_download.to_string(),
353
+ profile.best_for.clone(),
354
+ ]
355
+ })
356
+ .collect::<Vec<_>>();
357
+ print_table(&headers, &rows);
358
+
359
+ println!();
360
+ println!("Capabilities");
361
+ println!("------------");
362
+ let headers = ["provider", "module", "status", "implemented", "note"];
363
+ let rows = profiles
364
+ .iter()
365
+ .flat_map(|profile| {
366
+ profile.capabilities.iter().map(|capability| {
367
+ vec![
368
+ profile.provider.clone(),
369
+ capability.module.clone(),
370
+ capability.status.clone(),
371
+ capability.implemented.to_string(),
372
+ capability.note.clone(),
373
+ ]
374
+ })
375
+ })
376
+ .collect::<Vec<_>>();
377
+ print_table(&headers, &rows);
378
+ }
379
+
380
+ pub fn print_stooq_catalog(catalog: &crate::model::StooqCatalog) {
381
+ println!(
382
+ "Stooq bulk catalog fetched={} source={}",
383
+ catalog.fetched_at_utc, catalog.source_url
384
+ );
385
+ let headers = [
386
+ "frequency",
387
+ "market",
388
+ "asset",
389
+ "size_mb",
390
+ "cached",
391
+ "cache_key",
392
+ "label",
393
+ ];
394
+ let rows = catalog
395
+ .entries
396
+ .iter()
397
+ .map(|entry| {
398
+ vec![
399
+ entry.frequency.clone(),
400
+ entry.market.clone(),
401
+ entry.asset.clone(),
402
+ number_value(entry.approx_size_mb),
403
+ entry
404
+ .cached_zip_path
405
+ .clone()
406
+ .unwrap_or_else(|| "no".to_string()),
407
+ entry.cache_key.clone(),
408
+ entry.label.clone(),
409
+ ]
410
+ })
411
+ .collect::<Vec<_>>();
412
+ print_table(&headers, &rows);
413
+ println!();
414
+ println!(
415
+ "Download note: Stooq bulk download links are captcha-authorized. Use `stooq sync --zip-path <file>` or `stooq sync --url <authorized-url>`."
416
+ );
417
+ }
418
+
419
+ pub fn print_stooq_sync_report(report: &crate::model::StooqSyncReport) {
420
+ println!(
421
+ "Stooq synced {} {} {} bytes={} path={}",
422
+ report.frequency, report.market, report.asset, report.bytes, report.zip_path
423
+ );
424
+ println!("source: {}", report.source);
425
+ println!("imported_at_utc: {}", report.imported_at_utc);
426
+ }
427
+
428
+ pub fn print_stream_quotes(updates: &[StreamQuote]) {
429
+ let headers = [
430
+ "symbol",
431
+ "price",
432
+ "chg%",
433
+ "market_hours",
434
+ "time",
435
+ "exchange",
436
+ "volume",
437
+ "name",
438
+ ];
439
+ let rows = updates
440
+ .iter()
441
+ .map(|quote| {
442
+ vec![
443
+ quote.symbol.clone(),
444
+ money_value(Some(quote.price)),
445
+ pct_value(quote.change_pct),
446
+ quote
447
+ .market_hours
448
+ .map(|value| value.to_string())
449
+ .unwrap_or_else(|| "-".to_string()),
450
+ quote.time_local.clone().unwrap_or_else(|| "-".to_string()),
451
+ quote.exchange.clone().unwrap_or_else(|| "-".to_string()),
452
+ quote
453
+ .day_volume
454
+ .map(|value| value.to_string())
455
+ .unwrap_or_else(|| "-".to_string()),
456
+ quote.short_name.clone().unwrap_or_else(|| "-".to_string()),
457
+ ]
458
+ })
459
+ .collect::<Vec<_>>();
460
+ print_table(&headers, &rows);
461
+ }
462
+
463
+ fn price_point_row(point: &PricePoint) -> Vec<String> {
464
+ vec![
465
+ point.label.clone(),
466
+ money_value(point.price),
467
+ pct_value(point.change_pct),
468
+ point.session.clone().unwrap_or_else(|| "-".to_string()),
469
+ point.provider.clone(),
470
+ point
471
+ .market_time_local
472
+ .clone()
473
+ .unwrap_or_else(|| "-".to_string()),
474
+ money_value(point.open),
475
+ money_value(point.high),
476
+ money_value(point.low),
477
+ number_value(point.volume.map(|value| value as f64)),
478
+ ]
479
+ }
480
+
481
+ fn print_table(headers: &[&str], rows: &[Vec<String>]) {
482
+ let mut widths = headers
483
+ .iter()
484
+ .map(|header| header.len())
485
+ .collect::<Vec<_>>();
486
+ for row in rows {
487
+ for (index, value) in row.iter().enumerate() {
488
+ widths[index] = widths[index].max(value.len());
489
+ }
490
+ }
491
+
492
+ println!("{}", table_row(headers.iter().copied(), &widths));
493
+ println!(
494
+ "{}",
495
+ table_row(widths.iter().map(|width| "-".repeat(*width)), &widths)
496
+ );
497
+ for row in rows {
498
+ println!("{}", table_row(row.iter().map(String::as_str), &widths));
499
+ }
500
+ }
501
+
502
+ fn table_row<I, S>(values: I, widths: &[usize]) -> String
503
+ where
504
+ I: IntoIterator<Item = S>,
505
+ S: AsRef<str>,
506
+ {
507
+ values
508
+ .into_iter()
509
+ .zip(widths.iter())
510
+ .map(|(value, width)| format!("{:<width$}", value.as_ref()))
511
+ .collect::<Vec<_>>()
512
+ .join(" ")
513
+ }
514
+
515
+ fn money_value(value: Option<f64>) -> String {
516
+ match value {
517
+ Some(value) => format!("${value:.2}"),
518
+ None => "-".to_string(),
519
+ }
520
+ }
521
+
522
+ fn currency(value: Option<&str>) -> &str {
523
+ value.unwrap_or("USD")
524
+ }
525
+
526
+ fn number_value(value: Option<f64>) -> String {
527
+ match value {
528
+ Some(value) => {
529
+ let formatted = format!("{value:.4}");
530
+ formatted
531
+ .trim_end_matches('0')
532
+ .trim_end_matches('.')
533
+ .to_string()
534
+ }
535
+ None => "-".to_string(),
536
+ }
537
+ }
538
+
539
+ fn pct_value(value: Option<f64>) -> String {
540
+ match value {
541
+ Some(value) => format!("{value:+.2}%"),
542
+ None => "-".to_string(),
543
+ }
544
+ }