agent-finance-cli 0.1.0 → 0.1.2

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/Cargo.lock CHANGED
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10
10
 
11
11
  [[package]]
12
12
  name = "agent-finance-cli"
13
- version = "0.1.0"
13
+ version = "0.1.2"
14
14
  dependencies = [
15
15
  "anyhow",
16
16
  "base64",
@@ -19,6 +19,7 @@ dependencies = [
19
19
  "clap",
20
20
  "csv",
21
21
  "futures-util",
22
+ "iana-time-zone",
22
23
  "prost",
23
24
  "rustls",
24
25
  "scraper",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "agent-finance-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  edition = "2024"
5
5
  license = "MIT OR Apache-2.0"
6
6
  repository = "https://github.com/M4n5ter/agent-finance-cli"
@@ -29,3 +29,4 @@ zip = { version = "8.6.0", default-features = false, features = ["deflate"] }
29
29
  scraper = "0.27.0"
30
30
  wreq = { version = "6.0.0-rc.29", default-features = false, features = ["cookies", "deflate", "gzip", "json", "socks", "system-proxy", "tokio-rt", "webpki-roots"] }
31
31
  wreq-util = "3.0.0-rc.12"
32
+ iana-time-zone = "0.1"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-finance-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI Agent-first CLI for no-key financial market data and research context.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -31,11 +31,11 @@
31
31
  "check:npm": "node npm/check-package.js"
32
32
  },
33
33
  "optionalDependencies": {
34
- "agent-finance-cli-darwin-arm64": "0.1.0",
35
- "agent-finance-cli-darwin-x64": "0.1.0",
36
- "agent-finance-cli-linux-arm64": "0.1.0",
37
- "agent-finance-cli-linux-x64": "0.1.0",
38
- "agent-finance-cli-win32-x64": "0.1.0"
34
+ "agent-finance-cli-darwin-arm64": "0.1.2",
35
+ "agent-finance-cli-darwin-x64": "0.1.2",
36
+ "agent-finance-cli-linux-arm64": "0.1.2",
37
+ "agent-finance-cli-linux-x64": "0.1.2",
38
+ "agent-finance-cli-win32-x64": "0.1.2"
39
39
  },
40
40
  "keywords": [
41
41
  "finance",
package/src/app.rs CHANGED
@@ -21,21 +21,27 @@ use crate::providers::{self, binance_futures, stooq};
21
21
  use crate::research;
22
22
  use crate::skills;
23
23
  use crate::stream;
24
+ use crate::time::resolve_timezone;
24
25
 
25
26
  pub async fn run() -> Result<()> {
26
27
  let cli = Cli::parse();
27
28
  let proxy = cli.proxy.as_deref();
28
29
  let no_proxy = cli.no_proxy;
29
- let timezone = cli.timezone.as_str();
30
+ let timezone = resolve_timezone(cli.timezone.as_deref())?;
31
+ let timezone = timezone.as_str();
30
32
  let timeout_seconds = cli.timeout_seconds;
31
33
  match cli.command {
32
34
  Command::Price(args) => run_price(args, proxy, no_proxy, timeout_seconds, timezone).await,
33
35
  Command::Sessions(args) => {
34
36
  run_sessions(args, proxy, no_proxy, timeout_seconds, timezone).await
35
37
  }
36
- Command::History(args) => run_history(args, proxy, no_proxy, timeout_seconds).await,
38
+ Command::History(args) => {
39
+ run_history(args, proxy, no_proxy, timeout_seconds, timezone).await
40
+ }
37
41
  Command::Indicators(args) => run_indicators(args, proxy, no_proxy, timeout_seconds).await,
38
- Command::Futures(args) => run_futures(args, proxy, no_proxy, timeout_seconds).await,
42
+ Command::Futures(args) => {
43
+ run_futures(args, proxy, no_proxy, timeout_seconds, timezone).await
44
+ }
39
45
  Command::Fundamentals(args) => {
40
46
  run_provider_quote_summary(
41
47
  args,
@@ -193,6 +199,7 @@ async fn run_history(
193
199
  proxy: Option<&str>,
194
200
  no_proxy: bool,
195
201
  timeout_seconds: u64,
202
+ timezone: &str,
196
203
  ) -> Result<()> {
197
204
  let client = http_client(timeout_seconds, proxy, no_proxy)?;
198
205
  let provider = effective_history_provider(args.provider, args.session);
@@ -213,7 +220,7 @@ async fn run_history(
213
220
  if args.json {
214
221
  println!("{}", serde_json::to_string_pretty(&history)?);
215
222
  } else {
216
- output::print_history_table(&history);
223
+ output::print_history_table(&history, timezone);
217
224
  }
218
225
  Ok(())
219
226
  }
@@ -291,6 +298,7 @@ async fn run_futures(
291
298
  proxy: Option<&str>,
292
299
  no_proxy: bool,
293
300
  timeout_seconds: u64,
301
+ timezone: &str,
294
302
  ) -> Result<()> {
295
303
  let client = http_client(timeout_seconds, proxy, no_proxy)?;
296
304
  let stats =
@@ -299,7 +307,7 @@ async fn run_futures(
299
307
  if args.json {
300
308
  println!("{}", serde_json::to_string_pretty(&stats)?);
301
309
  } else {
302
- output::print_futures_stats(&stats);
310
+ output::print_futures_stats(&stats, timezone);
303
311
  }
304
312
 
305
313
  if stats.errors.is_empty() {
package/src/cli.rs CHANGED
@@ -2,7 +2,6 @@ use std::path::PathBuf;
2
2
 
3
3
  use clap::{Parser, Subcommand, ValueEnum};
4
4
 
5
- use crate::time::DEFAULT_TIMEZONE;
6
5
  pub const HISTORY_INTERVAL_HELP: &str = "Bar interval. Provider-specific values: Yahoo 1m/2m/5m/15m/30m/60m/90m/1h/1d/5d/1wk/1mo/3mo; Robinhood 5m/10m/1h/1d/1w; Stooq live 1d/1w/1mo; Stooq bulk 5m/1h after sync; Binance klines 1m/3m/5m/15m/30m/1h/2h/4h/6h/8h/12h/1d/3d/1w/1M.";
7
6
 
8
7
  #[derive(Parser, Debug)]
@@ -21,9 +20,10 @@ pub struct Cli {
21
20
  #[arg(long, global = true)]
22
21
  pub no_proxy: bool,
23
22
 
24
- /// Human-output timezone. UTC is still preserved in JSON fields.
25
- #[arg(long, global = true, default_value = DEFAULT_TIMEZONE)]
26
- pub timezone: String,
23
+ /// Human-output timezone. Defaults to the machine's local IANA timezone.
24
+ /// UTC is still preserved in JSON fields.
25
+ #[arg(long, global = true)]
26
+ pub timezone: Option<String>,
27
27
 
28
28
  /// HTTP timeout in seconds.
29
29
  #[arg(long, global = true, default_value_t = 10)]
package/src/output.rs CHANGED
@@ -7,6 +7,7 @@ use crate::model::{
7
7
  ResearchReport, SearchReport, StreamQuote,
8
8
  };
9
9
  use crate::page_read::PageReadReport;
10
+ use crate::time::utc_to_local;
10
11
 
11
12
  pub fn print_price_summary(summary: &PriceSummary, show_all: bool) {
12
13
  println!(
@@ -72,7 +73,7 @@ pub fn print_price_summary(summary: &PriceSummary, show_all: bool) {
72
73
  }
73
74
  }
74
75
 
75
- pub fn print_history_table(history: &HistoryBatch) {
76
+ pub fn print_history_table(history: &HistoryBatch, timezone: &str) {
76
77
  println!(
77
78
  "{} history via {} interval={} adjustment={} actions={} repair_requested={} repair_applied={}",
78
79
  history.symbol,
@@ -101,7 +102,7 @@ pub fn print_history_table(history: &HistoryBatch) {
101
102
  .iter()
102
103
  .map(|bar| {
103
104
  vec![
104
- bar.open_time.clone(),
105
+ local_or_original(&bar.open_time, timezone),
105
106
  money_value(bar.open),
106
107
  money_value(bar.high),
107
108
  money_value(bar.low),
@@ -157,10 +158,12 @@ pub fn print_indicator_table(indicators: &[DerivedIndicator], errors: &BTreeMap<
157
158
  print_table(&headers, &rows);
158
159
  }
159
160
 
160
- pub fn print_futures_stats(stats: &FuturesStats) {
161
+ pub fn print_futures_stats(stats: &FuturesStats, timezone: &str) {
161
162
  println!(
162
163
  "{} futures stats via {} fetched_at={}",
163
- stats.symbol, stats.provider, stats.fetched_at_utc
164
+ stats.symbol,
165
+ stats.provider,
166
+ local_or_original(&stats.fetched_at_utc, timezone)
164
167
  );
165
168
  if let Some(ticker) = stats.ticker_24h.as_ref() {
166
169
  println!(
@@ -182,14 +185,14 @@ pub fn print_futures_stats(stats: &FuturesStats) {
182
185
  money_value(mark.mark_price),
183
186
  money_value(mark.index_price),
184
187
  pct_value(mark.last_funding_rate.map(|value| value * 100.0)),
185
- mark.next_funding_time.as_deref().unwrap_or("-")
188
+ local_or_original_optional(mark.next_funding_time.as_deref(), timezone)
186
189
  );
187
190
  }
188
191
  if let Some(open_interest) = stats.open_interest.as_ref() {
189
192
  println!(
190
193
  "open_interest: {} time={}",
191
194
  number_value(open_interest.open_interest),
192
- open_interest.time.as_deref().unwrap_or("-")
195
+ local_or_original_optional(open_interest.time.as_deref(), timezone)
193
196
  );
194
197
  }
195
198
  if !stats.funding_rates.is_empty() {
@@ -200,7 +203,7 @@ pub fn print_futures_stats(stats: &FuturesStats) {
200
203
  .iter()
201
204
  .map(|row| {
202
205
  vec![
203
- row.funding_time.clone().unwrap_or_else(|| "-".to_string()),
206
+ local_or_original_optional(row.funding_time.as_deref(), timezone),
204
207
  pct_value(row.funding_rate.map(|value| value * 100.0)),
205
208
  money_value(row.mark_price),
206
209
  ]
@@ -512,6 +515,16 @@ where
512
515
  .join(" ")
513
516
  }
514
517
 
518
+ fn local_or_original_optional(value: Option<&str>, timezone: &str) -> String {
519
+ value
520
+ .map(|value| local_or_original(value, timezone))
521
+ .unwrap_or_else(|| "-".to_string())
522
+ }
523
+
524
+ fn local_or_original(value: &str, timezone: &str) -> String {
525
+ utc_to_local(Some(value), timezone).unwrap_or_else(|| value.to_string())
526
+ }
527
+
515
528
  fn money_value(value: Option<f64>) -> String {
516
529
  match value {
517
530
  Some(value) => format!("${value:.2}"),
package/src/time.rs CHANGED
@@ -1,7 +1,15 @@
1
+ use anyhow::{Result, anyhow};
1
2
  use chrono::{DateTime, SecondsFormat, Utc};
2
3
  use chrono_tz::Tz;
3
4
 
4
- pub const DEFAULT_TIMEZONE: &str = "Asia/Singapore";
5
+ const FALLBACK_TIMEZONE: &str = "UTC";
6
+
7
+ pub fn resolve_timezone(value: Option<&str>) -> Result<String> {
8
+ match value {
9
+ Some(value) => parse_timezone_name(value).map(str::to_string),
10
+ None => Ok(system_timezone()),
11
+ }
12
+ }
5
13
 
6
14
  pub fn utc_to_local(value: Option<&str>, timezone: &str) -> Option<String> {
7
15
  let value = value?;
@@ -14,8 +22,68 @@ pub fn now_local(timezone: &str) -> String {
14
22
  }
15
23
 
16
24
  pub fn format_local(datetime: DateTime<Utc>, timezone: &str) -> String {
17
- let timezone = timezone.parse::<Tz>().unwrap_or(chrono_tz::Asia::Singapore);
25
+ let timezone = parse_timezone(timezone).expect("timezone must be resolved before formatting");
18
26
  datetime
19
27
  .with_timezone(&timezone)
20
28
  .to_rfc3339_opts(SecondsFormat::Secs, true)
21
29
  }
30
+
31
+ fn system_timezone() -> String {
32
+ iana_time_zone::get_timezone()
33
+ .ok()
34
+ .and_then(|value| parse_timezone_name(&value).ok().map(str::to_string))
35
+ .unwrap_or_else(|| FALLBACK_TIMEZONE.to_string())
36
+ }
37
+
38
+ fn parse_timezone_name(value: &str) -> Result<&str> {
39
+ let value = value.trim();
40
+ if parse_timezone(value).is_ok() {
41
+ Ok(value)
42
+ } else {
43
+ Err(anyhow!("invalid IANA timezone: {value}"))
44
+ }
45
+ }
46
+
47
+ fn parse_timezone(value: &str) -> Result<Tz> {
48
+ value
49
+ .parse::<Tz>()
50
+ .map_err(|_| anyhow!("invalid IANA timezone: {value}"))
51
+ }
52
+
53
+ #[cfg(test)]
54
+ mod tests {
55
+ use super::*;
56
+
57
+ #[test]
58
+ fn explicit_timezone_overrides_system_timezone() {
59
+ assert_eq!(resolve_timezone(Some("UTC")).unwrap(), "UTC");
60
+ }
61
+
62
+ #[test]
63
+ fn invalid_explicit_timezone_is_rejected() {
64
+ assert!(resolve_timezone(Some("Not/AZone")).is_err());
65
+ }
66
+
67
+ #[test]
68
+ fn default_timezone_uses_system_timezone_when_available() {
69
+ let expected = iana_time_zone::get_timezone()
70
+ .ok()
71
+ .and_then(|value| parse_timezone_name(&value).ok().map(str::to_string))
72
+ .unwrap_or_else(|| FALLBACK_TIMEZONE.to_string());
73
+
74
+ assert_eq!(resolve_timezone(None).unwrap(), expected);
75
+ }
76
+
77
+ #[test]
78
+ fn local_format_uses_explicit_timezone() {
79
+ let datetime = DateTime::parse_from_rfc3339("2026-06-21T00:00:00Z")
80
+ .unwrap()
81
+ .with_timezone(&Utc);
82
+
83
+ assert_eq!(format_local(datetime, "UTC"), "2026-06-21T00:00:00Z");
84
+ assert_eq!(
85
+ format_local(datetime, "America/New_York"),
86
+ "2026-06-20T20:00:00-04:00"
87
+ );
88
+ }
89
+ }