parser: add auto fetch price from main instance

This commit is contained in:
k
2024-07-22 15:58:02 +02:00
parent 232276d106
commit 9871fdffc9
9 changed files with 183 additions and 23 deletions
+1
View File
@@ -8,3 +8,4 @@ TODO.md
.stfolder .stfolder
/charts /charts
/price
+2
View File
@@ -8,6 +8,7 @@
- Improved self-hosting by: - Improved self-hosting by:
- Fixing an incredibly annoying bug that made the program panic because of a wrong utxo/address durable state after a or many new datasets were added/changed after a first successful parse of the chain - Fixing an incredibly annoying bug that made the program panic because of a wrong utxo/address durable state after a or many new datasets were added/changed after a first successful parse of the chain
- Fixing a bug that would crash the program if launched for the first time ever - Fixing a bug that would crash the program if launched for the first time ever
- Auto fetch prices from the main Satonomics instance if missing instead of only trying Kraken's and Binance's API which are limited to the last 16 hours
- Merged the core of `HeightMap` and `DateMap` structs into `GenericMap` - Merged the core of `HeightMap` and `DateMap` structs into `GenericMap`
- Added `Height` struct and many others - Added `Height` struct and many others
- CLI - CLI
@@ -70,6 +71,7 @@
- Run file - Run file
- Only run with a watcher if `cargo watch` is available - Only run with a watcher if `cargo watch` is available
- Added trigger folder to automatically restart when a new dataset has been added in the parser
## v. 0.2.0 | [851286](https://mempool.space/block/0000000000000000000281ca7f1bf8c50702bfca168c7af1bdc67c977c1ac8ed) - 2024/07/08 ## v. 0.2.0 | [851286](https://mempool.space/block/0000000000000000000281ca7f1bf8c50702bfca168c7af1bdc67c977c1ac8ed) - 2024/07/08
+2 -2
View File
@@ -96,7 +96,7 @@ impl BitcoinDaemon {
fn get_blockchain_info(&self) -> BlockchainInfo { fn get_blockchain_info(&self) -> BlockchainInfo {
retry( retry(
|| { |_| {
// bitcoin-cli -datadir=/Users/k/Developer/bitcoin getblockchaininfo // bitcoin-cli -datadir=/Users/k/Developer/bitcoin getblockchaininfo
let output = Command::new("bitcoin-cli") let output = Command::new("bitcoin-cli")
.arg(self.datadir_arg()) .arg(self.datadir_arg())
@@ -122,7 +122,7 @@ impl BitcoinDaemon {
Ok(BlockchainInfo { headers, blocks }) Ok(BlockchainInfo { headers, blocks })
}, },
1, 1,
u64::MAX, usize::MAX,
) )
.unwrap() .unwrap()
} }
+56 -14
View File
@@ -9,8 +9,11 @@ use color_eyre::eyre::Error;
pub use ohlc::*; pub use ohlc::*;
use crate::{ use crate::{
price::{Binance, Kraken}, price::{Binance, Kraken, Satonomics},
structs::{AnyBiMap, AnyDateMap, BiMap, Date, DateMap, Height, MapKey}, structs::{
AnyBiMap, AnyDateMap, BiMap, Date, DateMap, DateMapChunkId, Height, HeightMapChunkId,
MapKey,
},
utils::{ONE_MONTH_IN_DAYS, ONE_WEEK_IN_DAYS, ONE_YEAR_IN_DAYS}, utils::{ONE_MONTH_IN_DAYS, ONE_WEEK_IN_DAYS, ONE_YEAR_IN_DAYS},
}; };
@@ -24,7 +27,8 @@ pub struct PriceDatasets {
kraken_1mn: Option<BTreeMap<u32, OHLC>>, kraken_1mn: Option<BTreeMap<u32, OHLC>>,
binance_1mn: Option<BTreeMap<u32, OHLC>>, binance_1mn: Option<BTreeMap<u32, OHLC>>,
binance_har: Option<BTreeMap<u32, OHLC>>, binance_har: Option<BTreeMap<u32, OHLC>>,
satonomics_by_height: BTreeMap<usize, Option<BTreeMap<usize, OHLC>>>, satonomics_by_height: BTreeMap<HeightMapChunkId, Vec<OHLC>>,
satonomics_by_date: BTreeMap<DateMapChunkId, BTreeMap<Date, OHLC>>,
// Inserted // Inserted
pub ohlcs: BiMap<OHLC>, pub ohlcs: BiMap<OHLC>,
@@ -89,6 +93,7 @@ impl PriceDatasets {
kraken_1mn: None, kraken_1mn: None,
kraken_daily: None, kraken_daily: None,
satonomics_by_height: BTreeMap::default(), satonomics_by_height: BTreeMap::default(),
satonomics_by_date: BTreeMap::default(),
ohlcs: BiMap::new_json(1, price_path), ohlcs: BiMap::new_json(1, price_path),
closes: BiMap::new_bin(1, &f("close")), closes: BiMap::new_bin(1, &f("close")),
@@ -304,7 +309,9 @@ impl PriceDatasets {
if self.ohlcs.date.is_key_safe(date) { if self.ohlcs.date.is_key_safe(date) {
Ok(self.ohlcs.date.get(&date).unwrap().to_owned()) Ok(self.ohlcs.date.get(&date).unwrap().to_owned())
} else { } else {
let ohlc = self.get_from_daily_kraken(&date)?; let ohlc = self
.get_from_date_satonomics(&date)
.or_else(|_| self.get_from_daily_kraken(&date))?;
self.ohlcs.date.insert(date, ohlc); self.ohlcs.date.insert(date, ohlc);
@@ -312,12 +319,27 @@ impl PriceDatasets {
} }
} }
fn get_from_date_satonomics(&mut self, date: &Date) -> color_eyre::Result<OHLC> {
let chunk_id = date.to_chunk_id();
#[allow(clippy::map_entry)]
if !self.satonomics_by_date.contains_key(&chunk_id) {
self.satonomics_by_date
.insert(chunk_id, Satonomics::fetch_date_prices(chunk_id)?);
}
self.satonomics_by_date
.get(&chunk_id)
.unwrap()
.get(date)
.cloned()
.ok_or(Error::msg("Couldn't find date in satonomics"))
}
fn get_from_daily_kraken(&mut self, date: &Date) -> color_eyre::Result<OHLC> { fn get_from_daily_kraken(&mut self, date: &Date) -> color_eyre::Result<OHLC> {
if self.kraken_daily.is_none() { if self.kraken_daily.is_none() {
self.kraken_daily.replace( self.kraken_daily
Kraken::fetch_daily_prices() .replace(Kraken::fetch_daily_prices().or_else(|_| Binance::fetch_daily_prices())?);
.unwrap_or_else(|_| Binance::fetch_daily_prices().unwrap()),
);
} }
self.kraken_daily self.kraken_daily
@@ -325,7 +347,7 @@ impl PriceDatasets {
.unwrap() .unwrap()
.get(date) .get(date)
.cloned() .cloned()
.ok_or(Error::msg("Couldn't find date in daily kraken")) .ok_or(Error::msg("Couldn't find date"))
} }
pub fn get_height_ohlc( pub fn get_height_ohlc(
@@ -358,15 +380,17 @@ impl PriceDatasets {
let previous_timestamp = previous_timestamp.map(clean_timestamp); let previous_timestamp = previous_timestamp.map(clean_timestamp);
let ohlc = self let ohlc = self
.get_from_1mn_kraken(timestamp, previous_timestamp) .get_from_height_satonomics(&height)
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
self.get_from_1mn_binance(timestamp, previous_timestamp) self.get_from_1mn_kraken(timestamp, previous_timestamp)
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
self.get_from_har_binance(timestamp, previous_timestamp) self.get_from_1mn_binance(timestamp, previous_timestamp)
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
let date = Date::from_timestamp(timestamp); self.get_from_har_binance(timestamp, previous_timestamp)
.unwrap_or_else(|_| {
let date = Date::from_timestamp(timestamp);
panic!( panic!(
"Can't find the price for: height: {height} - date: {date} "Can't find the price for: height: {height} - date: {date}
1mn APIs are limited to the last 16 hours for Binance's and the last 10 hours for Kraken's 1mn APIs are limited to the last 16 hours for Binance's and the last 10 hours for Kraken's
How to fix this: How to fix this:
@@ -381,6 +405,7 @@ How to fix this:
9. Move the file to 'parser/imports/binance.har' 9. Move the file to 'parser/imports/binance.har'
" "
) )
})
}) })
}) })
}); });
@@ -390,6 +415,23 @@ How to fix this:
Ok(ohlc) Ok(ohlc)
} }
fn get_from_height_satonomics(&mut self, height: &Height) -> color_eyre::Result<OHLC> {
let chunk_id = height.to_chunk_id();
#[allow(clippy::map_entry)]
if !self.satonomics_by_height.contains_key(&chunk_id) {
self.satonomics_by_height
.insert(chunk_id, Satonomics::fetch_height_prices(chunk_id)?);
}
self.satonomics_by_height
.get(&chunk_id)
.unwrap()
.get(height.to_serialized_key().to_usize())
.cloned()
.ok_or(Error::msg("Couldn't find height in satonomics"))
}
fn get_from_1mn_kraken( fn get_from_1mn_kraken(
&mut self, &mut self,
timestamp: u32, timestamp: u32,
+2 -2
View File
@@ -107,7 +107,7 @@ impl Binance {
log("binance: fetch 1mn"); log("binance: fetch 1mn");
retry( retry(
|| { |_| {
let body: Value = reqwest::blocking::get( let body: Value = reqwest::blocking::get(
"https://api.binance.com/api/v3/uiKlines?symbol=BTCUSDT&interval=1m&limit=1000", "https://api.binance.com/api/v3/uiKlines?symbol=BTCUSDT&interval=1m&limit=1000",
)? )?
@@ -154,7 +154,7 @@ impl Binance {
log("binance: fetch 1d"); log("binance: fetch 1d");
retry( retry(
|| { |_| {
let body: Value = reqwest::blocking::get( let body: Value = reqwest::blocking::get(
"https://api.binance.com/api/v3/uiKlines?symbol=BTCUSDT&interval=1d", "https://api.binance.com/api/v3/uiKlines?symbol=BTCUSDT&interval=1d",
)? )?
+2 -2
View File
@@ -16,7 +16,7 @@ impl Kraken {
log("kraken: fetch 1mn"); log("kraken: fetch 1mn");
retry( retry(
|| { |_| {
let body: Value = reqwest::blocking::get( let body: Value = reqwest::blocking::get(
"https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1", "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1",
)? )?
@@ -70,7 +70,7 @@ impl Kraken {
log("fetch kraken daily"); log("fetch kraken daily");
retry( retry(
|| { |_| {
let body: Value = reqwest::blocking::get( let body: Value = reqwest::blocking::get(
"https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1440", "https://api.kraken.com/0/public/OHLC?pair=XBTUSD&interval=1440",
)? )?
+2
View File
@@ -1,5 +1,7 @@
mod binance; mod binance;
mod kraken; mod kraken;
mod satonomics;
pub use binance::*; pub use binance::*;
pub use kraken::*; pub use kraken::*;
pub use satonomics::*;
+113
View File
@@ -0,0 +1,113 @@
use std::{collections::BTreeMap, str::FromStr};
use chrono::NaiveDate;
use color_eyre::eyre::ContextCompat;
use itertools::Itertools;
use serde_json::Value;
use crate::{
datasets::OHLC,
structs::{Date, DateMapChunkId, HeightMapChunkId},
utils::{log, retry},
MapChunkId,
};
pub struct Satonomics;
const SATONOMICS_OFFICIAL_URL: &str = "https://api.satonomics.xyz";
const SATONOMICS_OFFICIAL_BACKUP_URL: &str = "https://api-bkp.satonomics.xyz";
const RETRIES: usize = 10;
impl Satonomics {
fn get_base_url(try_index: usize) -> &'static str {
if try_index < RETRIES / 2 {
SATONOMICS_OFFICIAL_URL
} else {
SATONOMICS_OFFICIAL_BACKUP_URL
}
}
pub fn fetch_height_prices(chunk_id: HeightMapChunkId) -> color_eyre::Result<Vec<OHLC>> {
log("satonomics: fetch height prices");
retry(
|try_index| {
let base_url = Self::get_base_url(try_index);
let body: Value = reqwest::blocking::get(format!(
"{base_url}/height-to-price?chunk={}",
chunk_id.to_usize()
))?
.json()?;
Ok(body
.as_object()
.context("Expect to be an object")?
.get("dataset")
.context("Expect object to have dataset")?
.as_object()
.context("Expect to be an object")?
.get("map")
.context("Expect to have map")?
.as_array()
.context("Expect to be an array")?
.iter()
.map(Self::value_to_ohlc)
.collect_vec())
},
10,
RETRIES,
)
}
pub fn fetch_date_prices(chunk_id: DateMapChunkId) -> color_eyre::Result<BTreeMap<Date, OHLC>> {
log("satonomics: date height prices");
retry(
|try_index| {
let base_url = Self::get_base_url(try_index);
let body: Value = reqwest::blocking::get(format!(
"{base_url}/date-to-price?chunk={}",
chunk_id.to_usize()
))?
.json()?;
Ok(body
.as_object()
.context("Expect to be an object")?
.get("dataset")
.context("Expect object to have dataset")?
.as_object()
.context("Expect to be an object")?
.get("map")
.context("Expect to have map")?
.as_object()
.context("Expect to be an object")?
.iter()
.map(|(serialized_date, value)| {
let date = Date::wrap(NaiveDate::from_str(serialized_date).unwrap());
(date, Self::value_to_ohlc(value))
})
.collect::<BTreeMap<_, _>>())
},
10,
RETRIES,
)
}
fn value_to_ohlc(value: &Value) -> OHLC {
let ohlc = value.as_object().unwrap();
let get_value = |key: &str| ohlc.get(key).unwrap().as_f64().unwrap() as f32;
OHLC {
open: get_value("open"),
high: get_value("high"),
low: get_value("low"),
close: get_value("close"),
}
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
use std::{thread::sleep, time::Duration}; use std::{thread::sleep, time::Duration};
pub fn retry<T>( pub fn retry<T>(
function: impl Fn() -> color_eyre::Result<T>, function: impl Fn(usize) -> color_eyre::Result<T>,
sleep_in_s: u64, sleep_in_s: u64,
retries: u64, retries: usize,
) -> color_eyre::Result<T> { ) -> color_eyre::Result<T> {
if retries < 1 { if retries < 1 {
unreachable!() unreachable!()
@@ -12,7 +12,7 @@ pub fn retry<T>(
let mut i = 0; let mut i = 0;
loop { loop {
let res = function(); let res = function(i);
if i == retries || res.is_ok() { if i == retries || res.is_ok() {
return res; return res;