Skip to content

Commit

Permalink
Use Decimal instead of f64 for better precision
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
bjorn committed Sep 15, 2023
1 parent e2e681b commit 115fa21
Show file tree
Hide file tree
Showing 16 changed files with 408 additions and 141 deletions.
261 changes: 254 additions & 7 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ esplora-client = { version = "0.6", default-features = false, features = ["block
bitcoin = "0.30.1"
slint = "1.2"
open = "5.0.0"
rust_decimal = "1.32"
rust_decimal_macros = "1.32"
cryptotax_ui = { path = "cryptotax_ui" }
61 changes: 34 additions & 27 deletions src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{error::Error, path::Path, fmt};

use chrono::NaiveDateTime;
use serde::{Serialize, Deserialize};
use rust_decimal::prelude::*;

#[derive(Debug)]
pub enum GainError {
Expand All @@ -24,11 +25,32 @@ impl fmt::Display for GainError {

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub(crate) struct Amount {
pub quantity: f64,
pub quantity: Decimal,
pub currency: String,
}

impl Amount {
pub(crate) fn new(quantity: Decimal, currency: String) -> Self {
Self {
quantity,
currency,
}
}

pub(crate) fn from_f64(quantity: f64, currency: String) -> Self {
Self {
quantity: Decimal::from_f64(quantity).unwrap(),
currency: currency,
}
}

pub(crate) fn from_satoshis(quantity: u64) -> Self {
Self {
quantity: Decimal::new(quantity as i64, 8),
currency: "BTC".to_owned(),
}
}

pub(crate) fn is_fiat(&self) -> bool {
self.currency == "EUR"
}
Expand Down Expand Up @@ -122,7 +144,7 @@ pub(crate) struct Transaction {
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_value: Option<Amount>,
#[serde(skip)]
pub gain: Option<Result<f64, GainError>>,
pub gain: Option<Result<Decimal, GainError>>,
#[serde(skip)]
pub source_index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -140,57 +162,42 @@ impl Transaction {
}
}

pub(crate) fn fiat_deposit(timestamp: NaiveDateTime, quantity: f64, currency: &str) -> Self {
pub(crate) fn fiat_deposit(timestamp: NaiveDateTime, amount: Amount) -> Self {
Self {
timestamp,
operation: Operation::FiatDeposit(Amount {
quantity,
currency: currency.to_string(),
}),
operation: Operation::FiatDeposit(amount),
..Default::default()
}
}

pub(crate) fn fiat_withdrawal(timestamp: NaiveDateTime, quantity: f64, currency: &str) -> Self {
pub(crate) fn fiat_withdrawal(timestamp: NaiveDateTime, amount: Amount) -> Self {
Self {
timestamp,
operation: Operation::FiatWithdrawal(Amount {
quantity,
currency: currency.to_string(),
}),
operation: Operation::FiatWithdrawal(amount),
..Default::default()
}
}

pub(crate) fn send(timestamp: NaiveDateTime, quantity: f64, currency: &str) -> Self {
pub(crate) fn send(timestamp: NaiveDateTime, amount: Amount) -> Self {
Self {
timestamp,
operation: Operation::Send(Amount {
quantity,
currency: currency.to_string(),
}),
operation: Operation::Send(amount),
..Default::default()
}
}

pub(crate) fn receive(timestamp: NaiveDateTime, quantity: f64, currency: &str) -> Self {
pub(crate) fn receive(timestamp: NaiveDateTime, amount: Amount) -> Self {
Self {
timestamp,
operation: Operation::Receive(Amount {
quantity,
currency: currency.to_string(),
}),
operation: Operation::Receive(amount),
..Default::default()
}
}

pub(crate) fn fee(timestamp: NaiveDateTime, quantity: f64, currency: &str) -> Self {
pub(crate) fn fee(timestamp: NaiveDateTime, amount: Amount) -> Self {
Self {
timestamp,
operation: Operation::Fee(Amount {
quantity,
currency: currency.to_string(),
}),
operation: Operation::Fee(amount),
..Default::default()
}
}
Expand Down
9 changes: 5 additions & 4 deletions src/bitcoin_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use std::{error::Error, path::Path};

use chrono::{NaiveDateTime, TimeZone};
use chrono_tz::Europe::Berlin;
use rust_decimal::Decimal;
use serde::Deserialize;

use crate::base::Transaction;
use crate::base::{Amount, Transaction};

#[derive(Debug, Clone, Deserialize)]
enum TransferType {
Expand All @@ -27,7 +28,7 @@ struct BitcoinCoreAction {
// #[serde(rename = "Address")]
// address: String,
#[serde(rename = "Amount (BTC)", alias = "Amount (PPC)")]
amount: f64,
amount: Decimal,
#[serde(rename = "ID")]
id: String,
}
Expand All @@ -38,10 +39,10 @@ impl BitcoinCoreAction {
let utc_time = Berlin.from_local_datetime(&self.date).unwrap().naive_utc();
let mut tx = match self.type_ {
TransferType::SentTo => {
Transaction::send(utc_time, -self.amount, currency)
Transaction::send(utc_time, Amount::new(-self.amount, currency.to_owned()))
},
TransferType::ReceivedWith => {
Transaction::receive(utc_time, self.amount, currency)
Transaction::receive(utc_time, Amount::new(self.amount, currency.to_owned()))
},
};
tx.description = if self.label.is_empty() { None } else { Some(self.label) };
Expand Down
21 changes: 11 additions & 10 deletions src/bitcoin_de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::path::Path;

use chrono::{NaiveDateTime, TimeZone};
use chrono_tz::Europe::Berlin;
use rust_decimal::Decimal;
use serde::Deserialize;

use crate::{time::deserialize_date_time, base::{Transaction, Amount}};
Expand Down Expand Up @@ -32,25 +33,25 @@ pub(crate) struct BitcoinDeAction {
// #[serde(rename = "BTC-address")]
// pub btc_address: String,
// #[serde(rename = "Price")]
// pub price: Option<f64>,
// pub price: Option<Decimal>,
// #[serde(rename = "unit (rate)")]
// pub unit_rate: String,
// #[serde(rename = "BTC incl. fee")]
// pub btc_incl_fee: Option<f64>,
// pub btc_incl_fee: Option<Decimal>,
// #[serde(rename = "amount before fee")]
// pub amount_before_fee: Option<f64>,
// pub amount_before_fee: Option<Decimal>,
// #[serde(rename = "unit (amount before fee)")]
// pub unit_amount_before_fee: String,
// #[serde(rename = "BTC excl. Bitcoin.de fee")]
// pub btc_excl_bitcoin_de_fee: Option<f64>,
// pub btc_excl_bitcoin_de_fee: Option<Decimal>,
#[serde(rename = "amount after Bitcoin.de-fee")]
pub amount_after_bitcoin_de_fee: Option<f64>,
pub amount_after_bitcoin_de_fee: Option<Decimal>,
#[serde(rename = "unit (amount after Bitcoin.de-fee)")]
pub unit_amount_after_bitcoin_de_fee: String,
#[serde(rename = "Incoming / Outgoing")]
pub incoming_outgoing: f64,
pub incoming_outgoing: Decimal,
// #[serde(rename = "Account balance")]
// pub account_balance: f64,
// pub account_balance: Decimal,
}

impl From<BitcoinDeAction> for Transaction {
Expand All @@ -73,8 +74,8 @@ impl From<BitcoinDeAction> for Transaction {
},
)
},
BitcoinDeActionType::Disbursement => Transaction::send(utc_time, -item.incoming_outgoing, &item.currency),
BitcoinDeActionType::Deposit => Transaction::receive(utc_time, item.incoming_outgoing, &item.currency),
BitcoinDeActionType::Disbursement => Transaction::send(utc_time, Amount::new(-item.incoming_outgoing, item.currency)),
BitcoinDeActionType::Deposit => Transaction::receive(utc_time, Amount::new(item.incoming_outgoing, item.currency)),
BitcoinDeActionType::Sale => {
Transaction::trade(
utc_time,
Expand All @@ -88,7 +89,7 @@ impl From<BitcoinDeAction> for Transaction {
},
)
},
BitcoinDeActionType::NetworkFee => Transaction::fee(utc_time, -item.incoming_outgoing, &item.currency),
BitcoinDeActionType::NetworkFee => Transaction::fee(utc_time, Amount::new(-item.incoming_outgoing, item.currency)),
};
match item.type_ {
BitcoinDeActionType::Registration => {},
Expand Down
17 changes: 7 additions & 10 deletions src/bitonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{error::Error, path::Path};

use chrono::{NaiveDateTime, TimeZone};
use chrono_tz::Europe::Berlin;
use rust_decimal::Decimal;
use serde::Deserialize;

use crate::{base::{Transaction, Operation, Amount}, time::deserialize_date_time};
Expand All @@ -19,9 +20,9 @@ pub(crate) struct BitonicAction {
#[serde(rename = "Action")]
pub action: BitonicActionType,
#[serde(rename = "Amount")]
pub amount: f64,
pub amount: Decimal,
#[serde(rename = "Price")]
pub price: f64,
pub price: Decimal,
}

impl From<BitonicAction> for Transaction {
Expand Down Expand Up @@ -63,24 +64,20 @@ pub(crate) fn load_bitonic_csv(input_path: &Path) -> Result<Vec<Transaction>, Bo
if incoming.is_fiat() {
transactions.push(Transaction::receive(
transaction.timestamp - chrono::Duration::minutes(1),
outgoing.quantity,
&outgoing.currency));
Amount::new(outgoing.quantity, outgoing.currency.clone())));

transactions.push(Transaction::fiat_withdrawal(
transaction.timestamp + chrono::Duration::minutes(1),
incoming.quantity,
&incoming.currency));
Amount::new(incoming.quantity, incoming.currency.clone())));
}
else if outgoing.is_fiat() {
transactions.push(Transaction::fiat_deposit(
transaction.timestamp - chrono::Duration::minutes(1),
outgoing.quantity,
&outgoing.currency));
Amount::new(outgoing.quantity, outgoing.currency.clone())));

transactions.push(Transaction::send(
transaction.timestamp + chrono::Duration::minutes(1),
incoming.quantity,
&incoming.currency));
Amount::new(incoming.quantity, incoming.currency.clone())));
}
}

Expand Down
21 changes: 11 additions & 10 deletions src/coinmarketcap.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{error::Error, path::Path};

use chrono::{DateTime, FixedOffset, NaiveDateTime};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

// struct to deserialize the following json data:
Expand Down Expand Up @@ -44,19 +45,19 @@ struct CmcQuote {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Quote {
open: f64,
// high: f64,
// low: f64,
// close: f64,
// volume: f64,
// market_cap: f64,
open: Decimal,
// high: Decimal,
// low: Decimal,
// close: Decimal,
// volume: Decimal,
// market_cap: Decimal,
// timestamp: DateTime<FixedOffset>,
}

#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct PricePoint {
timestamp: NaiveDateTime,
price: f64,
price: Decimal,
}

// command to download BTC price history for 2023:
Expand Down Expand Up @@ -108,7 +109,7 @@ pub(crate) fn load_btc_price_history_data() -> Result<Vec<PricePoint>, Box<dyn E
Ok(prices)
}

pub(crate) fn estimate_btc_price(time: NaiveDateTime, prices: &Vec<PricePoint>) -> Option<f64> {
pub(crate) fn estimate_btc_price(time: NaiveDateTime, prices: &Vec<PricePoint>) -> Option<Decimal> {
let index = prices.partition_point(|p| p.timestamp < time);
let next_price_point = prices.get(index).or_else(|| prices.last());
let prev_price_point = if index > 0 { prices.get(index - 1) } else { None };
Expand All @@ -117,8 +118,8 @@ pub(crate) fn estimate_btc_price(time: NaiveDateTime, prices: &Vec<PricePoint>)
// calculate the most probable price, by linear iterpolation based on the previous and next price
let price_difference = next_price.price - prev_price.price;

let total_duration = (next_price.timestamp - prev_price.timestamp).num_seconds() as f64;
let time_since_prev = (time - prev_price.timestamp).num_seconds() as f64;
let total_duration: Decimal = (next_price.timestamp - prev_price.timestamp).num_seconds().into();
let time_since_prev: Decimal = (time - prev_price.timestamp).num_seconds().into();
let time_ratio = time_since_prev / total_duration;

Some(prev_price.price + time_ratio * price_difference)
Expand Down
15 changes: 8 additions & 7 deletions src/coinpanda.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::error::Error;

use chrono::NaiveDateTime;
use rust_decimal::Decimal;
use serde::Serialize;

use crate::{ctc::{CtcTxType, CtcTx}, time::serialize_date_time};
Expand All @@ -27,31 +28,31 @@ struct CoinpandaTx<'a> {

/// The amount sold/withdrawn/sent (outgoing)
#[serde(rename = "Sent Amount")]
sent_amount: Option<f64>,
sent_amount: Option<Decimal>,

/// Currency sold/withdrawn/sent (outgoing)
#[serde(rename = "Sent Currency")]
sent_currency: Option<&'a str>,

/// The amount bought/deposited/received (incoming)
#[serde(rename = "Received Amount")]
received_amount: Option<f64>,
received_amount: Option<Decimal>,

/// Currency bought/deposited/received (incoming)
#[serde(rename = "Received Currency")]
received_currency: Option<&'a str>,

/// Any associated fee amount
#[serde(rename = "Fee Amount")]
fee_amount: Option<f64>,
fee_amount: Option<Decimal>,

/// Fee currency if you paid any fee
#[serde(rename = "Fee Currency")]
fee_currency: Option<&'a str>,

/// The value of the transaction in a fiat currency
#[serde(rename = "Net Worth Amount")]
net_worth_amount: Option<f64>,
net_worth_amount: Option<Decimal>,

/// The fiat currency used to value the transaction
#[serde(rename = "Net Worth Currency")]
Expand All @@ -71,7 +72,7 @@ struct CoinpandaTx<'a> {
}

impl<'a> CoinpandaTx<'a> {
fn trade(timestamp: NaiveDateTime, received_amount: f64, received_currency: &'a str, sent_amount: f64, sent_currency: &'a str) -> Self {
fn trade(timestamp: NaiveDateTime, received_amount: Decimal, received_currency: &'a str, sent_amount: Decimal, sent_currency: &'a str) -> Self {
Self {
timestamp,
type_: CoinpandaTxType::Trade,
Expand All @@ -89,7 +90,7 @@ impl<'a> CoinpandaTx<'a> {
}
}

fn send(timestamp: NaiveDateTime, sent_amount: f64, sent_currency: &'a str) -> Self {
fn send(timestamp: NaiveDateTime, sent_amount: Decimal, sent_currency: &'a str) -> Self {
Self {
timestamp,
type_: CoinpandaTxType::Send,
Expand All @@ -107,7 +108,7 @@ impl<'a> CoinpandaTx<'a> {
}
}

fn receive(timestamp: NaiveDateTime, received_amount: f64, received_currency: &'a str) -> Self {
fn receive(timestamp: NaiveDateTime, received_amount: Decimal, received_currency: &'a str) -> Self {
Self {
timestamp,
type_: CoinpandaTxType::Receive,
Expand Down
Loading

0 comments on commit 115fa21

Please sign in to comment.