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 +2 -1
- package/Cargo.toml +2 -1
- package/package.json +6 -6
- package/src/app.rs +13 -5
- package/src/cli.rs +4 -4
- package/src/output.rs +20 -7
- package/src/time.rs +70 -2
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.
|
|
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.
|
|
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.
|
|
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.
|
|
35
|
-
"agent-finance-cli-darwin-x64": "0.1.
|
|
36
|
-
"agent-finance-cli-linux-arm64": "0.1.
|
|
37
|
-
"agent-finance-cli-linux-x64": "0.1.
|
|
38
|
-
"agent-finance-cli-win32-x64": "0.1.
|
|
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.
|
|
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) =>
|
|
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) =>
|
|
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.
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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,
|
|
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()
|
|
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()
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|