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
@@ -0,0 +1,302 @@
1
+ use anyhow::{Context, Result, anyhow};
2
+ use serde::Deserialize;
3
+ use serde_json::Value;
4
+ use url::Url;
5
+ use wreq::Client;
6
+
7
+ use crate::http::{change_pct, utc_now};
8
+ use crate::model::{Quote, ResearchHighlight, research_value_string};
9
+
10
+ const BASE_URL: &str = "https://quote.cnbc.com/quote-html-webservice/quote.htm";
11
+
12
+ #[derive(Debug, Deserialize)]
13
+ struct CnbcResponse {
14
+ #[serde(rename = "ITVQuoteResult")]
15
+ result: CnbcQuoteResult,
16
+ }
17
+
18
+ #[derive(Debug, Deserialize)]
19
+ struct CnbcQuoteResult {
20
+ #[serde(rename = "ITVQuote")]
21
+ quote: Option<OneOrMany<CnbcQuote>>,
22
+ }
23
+
24
+ #[derive(Debug, Deserialize)]
25
+ #[serde(untagged)]
26
+ enum OneOrMany<T> {
27
+ One(T),
28
+ Many(Vec<T>),
29
+ }
30
+
31
+ impl<T> OneOrMany<T> {
32
+ fn into_vec(self) -> Vec<T> {
33
+ match self {
34
+ OneOrMany::One(value) => vec![value],
35
+ OneOrMany::Many(values) => values,
36
+ }
37
+ }
38
+ }
39
+
40
+ #[derive(Debug, Deserialize)]
41
+ struct CnbcQuote {
42
+ symbol: Option<String>,
43
+ last: Option<String>,
44
+ last_timedate: Option<String>,
45
+ exchange: Option<String>,
46
+ open: Option<String>,
47
+ high: Option<String>,
48
+ low: Option<String>,
49
+ volume: Option<String>,
50
+ #[serde(rename = "currencyCode")]
51
+ currency_code: Option<String>,
52
+ previous_day_closing: Option<String>,
53
+ change_pct: Option<String>,
54
+ #[serde(rename = "feedSymbol")]
55
+ feed_symbol: Option<String>,
56
+ alt_symbol: Option<String>,
57
+ curmktstatus: Option<String>,
58
+ #[serde(rename = "ExtendedMktQuote")]
59
+ extended_market_quote: Option<CnbcExtendedQuote>,
60
+ }
61
+
62
+ #[derive(Debug, Deserialize)]
63
+ struct CnbcExtendedQuote {
64
+ #[serde(rename = "type")]
65
+ quote_type: Option<String>,
66
+ last: Option<String>,
67
+ last_timedate: Option<String>,
68
+ last_time: Option<String>,
69
+ change_pct: Option<String>,
70
+ volume: Option<String>,
71
+ }
72
+
73
+ pub async fn fetch_quote(client: &Client, symbol: &str) -> Result<Quote> {
74
+ let provider_symbol = symbol.to_uppercase();
75
+ let mut url = Url::parse(BASE_URL).context("invalid CNBC base URL")?;
76
+ url.query_pairs_mut()
77
+ .append_pair("symbols", &provider_symbol)
78
+ .append_pair("requestMethod", "itv")
79
+ .append_pair("noform", "1")
80
+ .append_pair("partnerId", "2")
81
+ .append_pair("fund", "1")
82
+ .append_pair("exthrs", "1")
83
+ .append_pair("output", "json");
84
+ let response: CnbcResponse = client
85
+ .get(url.as_str())
86
+ .send()
87
+ .await
88
+ .context("CNBC quote request failed")?
89
+ .error_for_status()
90
+ .context("CNBC quote returned HTTP error")?
91
+ .json()
92
+ .await
93
+ .context("CNBC quote JSON parse failed")?;
94
+ let quote = response
95
+ .result
96
+ .quote
97
+ .map(OneOrMany::into_vec)
98
+ .and_then(|mut quotes| quotes.pop())
99
+ .ok_or_else(|| anyhow!("CNBC response missing quote"))?;
100
+
101
+ let extended = quote.extended_market_quote.as_ref();
102
+ let extended_price = extended.and_then(|quote| parse_market_f64(quote.last.as_deref()));
103
+ let regular_price = parse_market_f64(quote.last.as_deref());
104
+ let price = extended_price
105
+ .or(regular_price)
106
+ .ok_or_else(|| anyhow!("CNBC quote missing usable price"))?;
107
+ let previous_close = parse_market_f64(quote.previous_day_closing.as_deref());
108
+ let using_extended = extended_price.is_some();
109
+ let session = if using_extended {
110
+ session_label(extended.and_then(|quote| quote.quote_type.as_deref()))
111
+ } else {
112
+ session_label(quote.curmktstatus.as_deref())
113
+ };
114
+ let market_time = if using_extended {
115
+ extended
116
+ .and_then(|quote| {
117
+ quote
118
+ .last_timedate
119
+ .clone()
120
+ .or_else(|| quote.last_time.clone())
121
+ })
122
+ .or_else(|| quote.last_timedate.clone())
123
+ } else {
124
+ quote.last_timedate.clone()
125
+ };
126
+ let provider_change_pct = if using_extended {
127
+ extended.and_then(|quote| parse_market_f64(quote.change_pct.as_deref()))
128
+ } else {
129
+ parse_market_f64(quote.change_pct.as_deref())
130
+ };
131
+ let change_pct_value = change_pct(price, previous_close).or(provider_change_pct);
132
+ let volume = if using_extended {
133
+ extended.and_then(|quote| parse_market_u64(quote.volume.as_deref()))
134
+ } else {
135
+ parse_market_u64(quote.volume.as_deref())
136
+ };
137
+ let open = non_zero(parse_market_f64(quote.open.as_deref()));
138
+ let high = non_zero(parse_market_f64(quote.high.as_deref()));
139
+ let low = non_zero(parse_market_f64(quote.low.as_deref()));
140
+ let symbol = quote.symbol.unwrap_or_else(|| provider_symbol.clone());
141
+ let currency = quote.currency_code;
142
+ let exchange = quote.exchange;
143
+ let provider_symbol = quote
144
+ .feed_symbol
145
+ .or(quote.alt_symbol)
146
+ .or(Some(provider_symbol));
147
+
148
+ Ok(Quote {
149
+ symbol,
150
+ price,
151
+ currency,
152
+ provider: "cnbc-extended".to_string(),
153
+ session: Some(session),
154
+ fetched_at_utc: utc_now(),
155
+ market_time,
156
+ previous_close,
157
+ open,
158
+ high,
159
+ low,
160
+ volume,
161
+ exchange,
162
+ provider_symbol,
163
+ change_pct: change_pct_value,
164
+ })
165
+ }
166
+
167
+ pub async fn fetch_quote_payload(client: &Client, symbol: &str) -> Result<Value> {
168
+ let provider_symbol = symbol.to_uppercase();
169
+ let mut url = Url::parse(BASE_URL).context("invalid CNBC base URL")?;
170
+ url.query_pairs_mut()
171
+ .append_pair("symbols", &provider_symbol)
172
+ .append_pair("requestMethod", "itv")
173
+ .append_pair("noform", "1")
174
+ .append_pair("partnerId", "2")
175
+ .append_pair("fund", "1")
176
+ .append_pair("exthrs", "1")
177
+ .append_pair("output", "json");
178
+ client
179
+ .get(url.as_str())
180
+ .send()
181
+ .await
182
+ .context("CNBC quote payload request failed")?
183
+ .error_for_status()
184
+ .context("CNBC quote payload returned HTTP error")?
185
+ .json()
186
+ .await
187
+ .context("CNBC quote payload JSON parse failed")
188
+ }
189
+
190
+ pub fn fundamentals_highlights(payload: &Value) -> Vec<ResearchHighlight> {
191
+ let mut rows = Vec::new();
192
+ let Some(quote) = first_quote(payload) else {
193
+ return rows;
194
+ };
195
+ for (label, path) in [
196
+ ("Company", "/name"),
197
+ ("Exchange", "/exchange"),
198
+ ("Quote source", "/provider"),
199
+ ("Current status", "/curmktstatus"),
200
+ ("Market cap", "/mktcapView"),
201
+ ("PE", "/pe"),
202
+ ("Forward PE", "/fpe"),
203
+ ("EPS", "/eps"),
204
+ ("Forward EPS", "/feps"),
205
+ ("Dividend", "/dividend"),
206
+ ("Dividend yield", "/dividendyield"),
207
+ ("Beta", "/beta"),
208
+ ("10-day avg volume", "/tendayavgvol"),
209
+ ("Revenue TTM", "/revenuettm"),
210
+ ("Price/Sales", "/psales"),
211
+ ("Forward Sales", "/fsales"),
212
+ ("Shares Out", "/sharesout"),
213
+ ("ROE TTM", "/ROETTM"),
214
+ ("Net margin TTM", "/NETPROFTTM"),
215
+ ("Gross margin TTM", "/GROSMGNTTM"),
216
+ ("EBITDA TTM", "/TTMEBITD"),
217
+ ("Debt/Equity", "/DEBTEQTYQ"),
218
+ ("52-week high", "/yrhiprice"),
219
+ ("52-week low", "/yrloprice"),
220
+ ] {
221
+ push_path(&mut rows, quote, label, path);
222
+ }
223
+ if let Some(extended) = quote.get("ExtendedMktQuote") {
224
+ for (label, path) in [
225
+ ("Extended type", "/type"),
226
+ ("Extended price", "/last"),
227
+ ("Extended change", "/change"),
228
+ ("Extended change pct", "/change_pct"),
229
+ ("Extended volume", "/volume"),
230
+ ("Extended time", "/last_time"),
231
+ ] {
232
+ if let Some(value) = string_at(extended, path) {
233
+ rows.push(ResearchHighlight::new(
234
+ label,
235
+ value,
236
+ "cnbc",
237
+ "fundamentals-lite",
238
+ ));
239
+ }
240
+ }
241
+ }
242
+ rows
243
+ }
244
+
245
+ fn first_quote(payload: &Value) -> Option<&Value> {
246
+ payload
247
+ .pointer("/ITVQuoteResult/ITVQuote")
248
+ .and_then(|quote| match quote {
249
+ Value::Array(quotes) => quotes.first(),
250
+ Value::Object(_) => Some(quote),
251
+ _ => None,
252
+ })
253
+ .or_else(|| {
254
+ payload
255
+ .pointer("/FormattedQuoteResult/FormattedQuote")
256
+ .and_then(|quote| match quote {
257
+ Value::Array(quotes) => quotes.first(),
258
+ Value::Object(_) => Some(quote),
259
+ _ => None,
260
+ })
261
+ })
262
+ }
263
+
264
+ fn push_path(rows: &mut Vec<ResearchHighlight>, payload: &Value, label: &str, path: &str) {
265
+ if let Some(row) =
266
+ ResearchHighlight::from_path(Some(payload), label, path, "cnbc", "fundamentals-lite")
267
+ .filter(|row| !row.value.trim().is_empty())
268
+ {
269
+ rows.push(row);
270
+ }
271
+ }
272
+
273
+ fn string_at(payload: &Value, path: &str) -> Option<String> {
274
+ research_value_string(payload.pointer(path)).filter(|value| !value.trim().is_empty())
275
+ }
276
+
277
+ fn parse_market_f64(value: Option<&str>) -> Option<f64> {
278
+ let value = value?.trim();
279
+ if value.is_empty() || value == "N/D" || value == "--" {
280
+ return None;
281
+ }
282
+ let cleaned = value
283
+ .trim_start_matches('$')
284
+ .trim_end_matches('%')
285
+ .replace(',', "");
286
+ cleaned.parse::<f64>().ok()
287
+ }
288
+
289
+ fn parse_market_u64(value: Option<&str>) -> Option<u64> {
290
+ parse_market_f64(value).map(|value| value as u64)
291
+ }
292
+
293
+ fn non_zero(value: Option<f64>) -> Option<f64> {
294
+ value.filter(|value| *value != 0.0)
295
+ }
296
+
297
+ fn session_label(value: Option<&str>) -> String {
298
+ let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
299
+ return "regular".to_string();
300
+ };
301
+ value.to_ascii_lowercase()
302
+ }
@@ -0,0 +1,117 @@
1
+ use anyhow::{Result, anyhow};
2
+ use wreq::Client;
3
+
4
+ use crate::cli::{HistoryAdjustment, Provider, StooqAsset, StooqMarket};
5
+ use crate::model::{HistoryBatch, Quote};
6
+
7
+ pub mod binance_futures;
8
+ pub mod capabilities;
9
+ pub mod cnbc;
10
+ pub mod robinhood;
11
+ pub mod sec_edgar;
12
+ pub mod stooq;
13
+ pub mod yahoo;
14
+
15
+ #[derive(Debug, Clone)]
16
+ pub struct HistoryRequest {
17
+ pub symbol: String,
18
+ pub interval: String,
19
+ pub range: String,
20
+ pub limit: usize,
21
+ pub extended_session: bool,
22
+ pub adjustment: HistoryAdjustment,
23
+ pub actions: bool,
24
+ pub repair: bool,
25
+ pub stooq_market: StooqMarket,
26
+ pub stooq_asset: StooqAsset,
27
+ }
28
+
29
+ pub async fn fetch_quote_without_boats(
30
+ client: &Client,
31
+ symbol: &str,
32
+ label: &str,
33
+ ) -> Result<Quote> {
34
+ let extended_error = match yahoo::fetch_extended_quote(client, symbol).await {
35
+ Ok(quote) => return Ok(quote),
36
+ Err(error) => error.to_string(),
37
+ };
38
+ let yahoo_error = match yahoo::fetch_quote(client, symbol).await {
39
+ Ok(quote) => return Ok(quote),
40
+ Err(error) => error.to_string(),
41
+ };
42
+ match stooq::fetch_quote(client, symbol).await {
43
+ Ok(quote) => Ok(quote),
44
+ Err(stooq_error) => Err(anyhow!(
45
+ "{}: yahoo-extended: {}; yahoo: {}; stooq: {}",
46
+ label,
47
+ extended_error,
48
+ yahoo_error,
49
+ stooq_error
50
+ )),
51
+ }
52
+ }
53
+
54
+ pub async fn fetch_history(
55
+ client: &Client,
56
+ provider: Provider,
57
+ request: &HistoryRequest,
58
+ ) -> Result<HistoryBatch> {
59
+ match provider {
60
+ Provider::Yahoo => yahoo::fetch_history(client, request).await,
61
+ Provider::YahooExtended => yahoo::fetch_extended_history(client, request).await,
62
+ Provider::YahooBoats => Err(anyhow!(
63
+ "yahoo-boats does not support history; use yahoo-extended for pre/post chart bars"
64
+ )),
65
+ Provider::Stooq => {
66
+ stooq::fetch_history(
67
+ client,
68
+ &request.symbol,
69
+ &request.interval,
70
+ request.limit,
71
+ request.stooq_market,
72
+ request.stooq_asset,
73
+ )
74
+ .await
75
+ }
76
+ Provider::CnbcExtended => Err(anyhow!("cnbc-extended does not support history")),
77
+ Provider::Robinhood => {
78
+ robinhood::fetch_history(
79
+ client,
80
+ &request.symbol,
81
+ &request.interval,
82
+ &request.range,
83
+ request.extended_session,
84
+ request.limit,
85
+ )
86
+ .await
87
+ }
88
+ Provider::BinanceFutures => {
89
+ binance_futures::fetch_history(
90
+ client,
91
+ &request.symbol,
92
+ &request.interval,
93
+ request.limit,
94
+ )
95
+ .await
96
+ }
97
+ Provider::Auto => {
98
+ let yahoo_error = match yahoo::fetch_history(client, request).await {
99
+ Ok(history) => return Ok(history),
100
+ Err(error) => error.to_string(),
101
+ };
102
+ match stooq::fetch_history(
103
+ client,
104
+ &request.symbol,
105
+ &request.interval,
106
+ request.limit,
107
+ request.stooq_market,
108
+ request.stooq_asset,
109
+ )
110
+ .await
111
+ {
112
+ Ok(history) => Ok(history),
113
+ Err(stooq_error) => Err(anyhow!("yahoo: {}; stooq: {}", yahoo_error, stooq_error)),
114
+ }
115
+ }
116
+ }
117
+ }