diff --git a/Cargo.lock b/Cargo.lock index 035847f..9763d4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,6 +462,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] diff --git a/app/Cargo.toml b/app/Cargo.toml index 32a5235..9af4232 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -9,7 +9,7 @@ api-proc-macro.workspace = true binance_spot_connector_rust.workspace = true bs58.workspace = true -chrono.workspace = true +chrono = { workspace = true, features = ["serde"] } copypasta.workspace = true futures.workspace = true ledger_bitcoin_client.workspace = true diff --git a/app/src/api/coin_price.rs b/app/src/api/coin_price.rs index 61bbb70..d600bcd 100644 --- a/app/src/api/coin_price.rs +++ b/app/src/api/coin_price.rs @@ -1,7 +1,9 @@ -use std::time::Instant; - use api_proc_macro::implement_cache; -use binance_spot_connector_rust::{market, ureq::BinanceHttpClient}; +use binance_spot_connector_rust::{ + market::{self, klines::KlineInterval}, + ureq::BinanceHttpClient, +}; +use chrono::{DateTime, Utc}; use rust_decimal::Decimal; use rust_decimal_macros::dec; use serde::Deserialize; @@ -23,7 +25,8 @@ pub enum TimePeriod { All, } -pub type PriceHistory = Vec<(Instant, Decimal)>; +/// Uniformly distributed prices for given period of time, arranged from historical to most recent. +pub type PriceHistory = Vec; #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -47,15 +50,68 @@ pub struct CoinPriceApi { client: BinanceHttpClient, } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -#[allow(dead_code)] +#[allow(unused)] struct BinanceApiMarketAvgPriceResponse { mins: u32, price: Decimal, close_time: u64, } +#[derive(Deserialize, Debug)] +#[serde(from = "BinanceApiKlineSerde")] +#[allow(unused)] +struct BinanceApiKline { + open_time: DateTime, + open_price: Decimal, + high_price: Decimal, + low_price: Decimal, + close_price: Decimal, + volume: Decimal, + close_time: DateTime, + quote_asset_volume: Decimal, + number_of_trades: u32, + taker_buy_base_asset_volume: Decimal, + taker_buy_quote_asset_volume: Decimal, + unused_field: String, +} + +#[derive(Deserialize)] +struct BinanceApiKlineSerde( + #[serde(with = "chrono::serde::ts_milliseconds")] DateTime, + Decimal, + Decimal, + Decimal, + Decimal, + Decimal, + #[serde(with = "chrono::serde::ts_milliseconds")] DateTime, + Decimal, + u32, + Decimal, + Decimal, + String, +); + +impl From for BinanceApiKline { + fn from(value: BinanceApiKlineSerde) -> Self { + BinanceApiKline { + open_time: value.0, + open_price: value.1, + high_price: value.2, + low_price: value.3, + close_price: value.4, + volume: value.5, + close_time: value.6, + quote_asset_volume: value.7, + number_of_trades: value.8, + taker_buy_base_asset_volume: value.9, + taker_buy_quote_asset_volume: value.10, + unused_field: value.11, + } + } +} + impl CoinPriceApi { pub fn new(url: &str) -> Self { let client = BinanceHttpClient::with_url(url); @@ -80,16 +136,37 @@ impl CoinPriceApiT for CoinPriceApi { async fn get_price_history( &self, - _from: Coin, - _to: Coin, - _interval: TimePeriod, + from: Coin, + to: Coin, + interval: TimePeriod, ) -> Option { - todo!() + let pair = [from.to_api_string(), to.to_api_string()].concat(); + + let (kline_interval, limit) = match interval { + TimePeriod::Day => (KlineInterval::Minutes3, 24 * (60 / 3)), // 480 + TimePeriod::Week => (KlineInterval::Minutes15, 7 * 24 * (60 / 15)), // 672 + TimePeriod::Month => (KlineInterval::Hours1, 30 * 24), // 720 + TimePeriod::Year => (KlineInterval::Hours12, 365 * 2), // 730 + // TODO: Adjust KLineInterval to get 500-1000 klines in response. + TimePeriod::All => (KlineInterval::Months1, 500), + }; + + let request = market::klines(&pair, kline_interval).limit(limit); + + let history = self.client.send(request).unwrap().into_body_str().unwrap(); + let history: Vec = serde_json::from_str(&history).unwrap(); + + Some( + history + .into_iter() + .map(|kline| (kline.open_price + kline.close_price) / Decimal::TWO) + .collect(), + ) } } pub mod mock { - use std::{collections::HashMap, time::Duration}; + use std::collections::HashMap; use rust_decimal::prelude::FromPrimitive; @@ -137,16 +214,11 @@ pub mod mock { .checked_div(Decimal::from_usize(line_angle * RESULTS).unwrap()) .unwrap(); - let mut time = Instant::now(); - let time_interval = Duration::new(10, 0); - let mut prices = vec![]; for _ in 0..RESULTS { - prices.push((time, price)); - + prices.push(price); price = price.saturating_sub(price_interval); - time -= time_interval; } Some(prices) diff --git a/app/src/app.rs b/app/src/app.rs index 8cc55f8..486c6e0 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -126,8 +126,8 @@ fn create_screen(screen: ScreenName) -> Box { let mut ledger_api = block_on(LedgerApiCache::new(ledger_api)); ledger_api.set_all_modes(ModePlan::Transparent); - let coin_price_api = CoinPriceApiMock::new(); - let _coin_price_api = CoinPriceApi::new("https://data-api.binance.vision"); + let _coin_price_api = CoinPriceApiMock::new(); + let coin_price_api = CoinPriceApi::new("https://data-api.binance.vision"); let mut coin_price_api = block_on(CoinPriceApiCache::new(coin_price_api)); coin_price_api.set_all_modes(ModePlan::TimedOut(Duration::from_secs(5))); diff --git a/app/src/screen/asset/mod.rs b/app/src/screen/asset/mod.rs index 748d1a6..0a5323b 100644 --- a/app/src/screen/asset/mod.rs +++ b/app/src/screen/asset/mod.rs @@ -1,5 +1,3 @@ -use std::time::Instant; - use futures::executor::block_on; use ratatui::{crossterm::event::Event, Frame}; use rust_decimal::Decimal; @@ -40,10 +38,7 @@ enum TimePeriod { All, } -struct PriceHistoryPoint { - timestamp: Instant, - price: Decimal, -} +type PriceHistoryPoint = Decimal; impl Model { pub fn new(coin_price_api: C, blockchain_monitoring_api: M) -> Self { @@ -87,17 +82,7 @@ impl Model { coin, Coin::USDT, time_period, - )) - .map(|history| { - let mut history: Vec<_> = history - .into_iter() - .map(|(timestamp, price)| PriceHistoryPoint { timestamp, price }) - .collect(); - - history.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); - - history - }); + )); // TODO: Don't make requests to API each tick. let tx_list = block_on( diff --git a/app/src/screen/asset/view.rs b/app/src/screen/asset/view.rs index 57a16a0..43d6c67 100644 --- a/app/src/screen/asset/view.rs +++ b/app/src/screen/asset/view.rs @@ -55,16 +55,12 @@ fn render_price_chart( return; }; - let max_price = prices - .iter() - .map(|price| price.price) - .max() - .expect("Empty `prices` vector provided"); + let max_price = *prices.iter().max().expect("Empty `prices` vector provided"); let price_data: Vec<_> = prices .iter() .enumerate() - .map(|(idx, price)| (idx as f64, price.price.try_into().unwrap())) + .map(|(idx, &price)| (idx as f64, price.try_into().unwrap())) .collect(); let legend = TimePeriod::iter().map(|period| {