Skip to content

Commit

Permalink
Real CoinPriceApiT implementation (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
mertwole committed Aug 12, 2024
1 parent eeeae25 commit 86d7d91
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 43 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 89 additions & 17 deletions app/src/api/coin_price.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Decimal>;

#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
Expand All @@ -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<Utc>,
open_price: Decimal,
high_price: Decimal,
low_price: Decimal,
close_price: Decimal,
volume: Decimal,
close_time: DateTime<Utc>,
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<Utc>,
Decimal,
Decimal,
Decimal,
Decimal,
Decimal,
#[serde(with = "chrono::serde::ts_milliseconds")] DateTime<Utc>,
Decimal,
u32,
Decimal,
Decimal,
String,
);

impl From<BinanceApiKlineSerde> 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);
Expand All @@ -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<PriceHistory> {
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<BinanceApiKline> = 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;

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ fn create_screen(screen: ScreenName) -> Box<dyn Screen> {
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)));

Expand Down
19 changes: 2 additions & 17 deletions app/src/screen/asset/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::time::Instant;

use futures::executor::block_on;
use ratatui::{crossterm::event::Event, Frame};
use rust_decimal::Decimal;
Expand Down Expand Up @@ -40,10 +38,7 @@ enum TimePeriod {
All,
}

struct PriceHistoryPoint {
timestamp: Instant,
price: Decimal,
}
type PriceHistoryPoint = Decimal;

impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Model<C, M> {
pub fn new(coin_price_api: C, blockchain_monitoring_api: M) -> Self {
Expand Down Expand Up @@ -87,17 +82,7 @@ impl<C: CoinPriceApiT, M: BlockchainMonitoringApiT> Model<C, M> {
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(
Expand Down
8 changes: 2 additions & 6 deletions app/src/screen/asset/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,12 @@ fn render_price_chart<C: CoinPriceApiT, M: BlockchainMonitoringApiT>(
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| {
Expand Down

0 comments on commit 86d7d91

Please sign in to comment.