finalytics 0.4.1__tar.gz → 0.4.2__tar.gz
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.
- {finalytics-0.4.1 → finalytics-0.4.2}/PKG-INFO +1 -1
- {finalytics-0.4.1 → finalytics-0.4.2}/README.md +5 -5
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/Cargo.toml +3 -3
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/fundamentals.rs +42 -41
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/performance.rs +37 -20
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/statistics.rs +3 -3
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/stochastics.rs +1 -1
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/charts/portfolio.rs +6 -7
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/charts/ticker.rs +13 -15
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/utils/date_utils.rs +0 -17
- {finalytics-0.4.1 → finalytics-0.4.2}/python/Cargo.toml +3 -3
- {finalytics-0.4.1 → finalytics-0.4.2}/python/src/ffi.rs +1 -1
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/mod.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/optimization.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/sentiment.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/analytics/technicals.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/charts/mod.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/data/config.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/data/mod.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/data/ticker.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/lib.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/models/mod.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/models/portfolio.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/models/ticker.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/utils/chart_utils.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/utils/mod.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/finalytics/src/utils/web_utils.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/pyproject.toml +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/Cargo.lock +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/README.md +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/build.sh +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/conf.py +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/index.rst +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/src/lib.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/src/portfolio.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/src/ticker.rs +0 -0
- {finalytics-0.4.1 → finalytics-0.4.2}/python/test.py +0 -0
|
@@ -14,7 +14,7 @@ Add the following to your `Cargo.toml` file:
|
|
|
14
14
|
|
|
15
15
|
```toml
|
|
16
16
|
[dependencies]
|
|
17
|
-
finalytics = {version = "0.5.
|
|
17
|
+
finalytics = {version = "0.5.2", features = ["kaleido"]}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Or run the following command:
|
|
@@ -41,7 +41,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|
|
41
41
|
let ticker = TickerBuilder::new()
|
|
42
42
|
.ticker("AAPL")
|
|
43
43
|
.start_date("2023-01-01")
|
|
44
|
-
.end_date("2023-
|
|
44
|
+
.end_date("2023-12-31")
|
|
45
45
|
.interval(Interval::OneDay)
|
|
46
46
|
.benchmark_symbol("^GSPC")
|
|
47
47
|
.confidence_level(0.95)
|
|
@@ -73,7 +73,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|
|
73
73
|
let ticker_symbols = Vec::from(["NVDA", "BRK-A", "AAPL", "ZN=F"]);
|
|
74
74
|
let portfolio = PortfolioBuilder::new().ticker_symbols(ticker_symbols)
|
|
75
75
|
.benchmark_symbol("^GSPC")
|
|
76
|
-
.start_date("
|
|
76
|
+
.start_date("2020-01-01")
|
|
77
77
|
.end_date("2023-01-01")
|
|
78
78
|
.interval(Interval::OneDay)
|
|
79
79
|
.confidence_level(0.95)
|
|
@@ -133,7 +133,7 @@ from finalytics import Ticker
|
|
|
133
133
|
|
|
134
134
|
ticker = Ticker(symbol="AAPL",
|
|
135
135
|
start="2023-01-01",
|
|
136
|
-
end="2023-
|
|
136
|
+
end="2023-12-31",
|
|
137
137
|
interval="1d",
|
|
138
138
|
confidence_level=0.95,
|
|
139
139
|
risk_free_rate=0.02)
|
|
@@ -148,7 +148,7 @@ ticker.performance_chart().show()
|
|
|
148
148
|
from finalytics import Portfolio
|
|
149
149
|
|
|
150
150
|
portfolio = Portfolio(ticker_symbols=["AAPL", "GOOG", "MSFT", "BTC-USD"],
|
|
151
|
-
benchmark_symbol="^GSPC", start_date="2020-01-01", end_date="
|
|
151
|
+
benchmark_symbol="^GSPC", start_date="2020-01-01", end_date="2023-01-01", interval="1d",
|
|
152
152
|
confidence_level=0.95, risk_free_rate=0.02, max_iterations=1000,
|
|
153
153
|
objective_function="max_sharpe")
|
|
154
154
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "finalytics"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.2"
|
|
4
4
|
authors = ["Nnamdi Olisaeloka <nnamdiolisaeloka@gmail.com>"]
|
|
5
5
|
edition = "2021"
|
|
6
6
|
homepage = "https://finalytics.rs/"
|
|
@@ -24,7 +24,7 @@ chrono = "0.4.37"
|
|
|
24
24
|
select = "0.6.0"
|
|
25
25
|
scraper = "0.19.0"
|
|
26
26
|
sentiment = "0.1.1"
|
|
27
|
-
polars = { version = "0.
|
|
27
|
+
polars = { version = "0.41.3", default-features = false, features = ["lazy", "dtype-datetime", "rows", "fmt_no_tty"] }
|
|
28
28
|
ta = "0.5.0"
|
|
29
29
|
num-format = "0.4.4"
|
|
30
30
|
smartcore = "0.3.2"
|
|
@@ -34,7 +34,7 @@ ndarray = "0.15.6"
|
|
|
34
34
|
rand_distr = "0.4.3"
|
|
35
35
|
optimization = "0.2.0"
|
|
36
36
|
html-escape = "0.2.13"
|
|
37
|
-
plotly = { version = "0.
|
|
37
|
+
plotly = { version = "0.9.0", features = ["kaleido"] }
|
|
38
38
|
once_cell = "1.19.0"
|
|
39
39
|
futures = "0.3.30"
|
|
40
40
|
|
|
@@ -21,40 +21,41 @@ impl Financials for Ticker {
|
|
|
21
21
|
let income_statement = self.get_fundamentals("income-statement", "quarterly").await?;
|
|
22
22
|
let balance_sheet = self.get_fundamentals("balance-sheet", "quarterly").await?;
|
|
23
23
|
let cash_flow = self.get_fundamentals("cash-flow", "quarterly").await?;
|
|
24
|
+
let ratios = vec![
|
|
25
|
+
Series::new("date", income_statement.column("asOfDate")?),
|
|
26
|
+
Series::new("Gross Profit Margin", (income_statement.column("GrossProfit")? / income_statement.column("TotalRevenue")?)?),
|
|
27
|
+
Series::new("Operating Profit Margin", (income_statement.column("EBIT")? / income_statement.column("TotalRevenue")?)?),
|
|
28
|
+
Series::new("Net Profit Margin", (income_statement.column("NetIncome")? / income_statement.column("TotalRevenue")?)?),
|
|
29
|
+
Series::new("Return on Assets", (income_statement.column("NetIncome")? / balance_sheet.column("TotalAssets")?)?),
|
|
30
|
+
Series::new("Return on Equity", (income_statement.column("NetIncome")? / balance_sheet.column("TotalEquityGrossMinorityInterest")?)?),
|
|
31
|
+
Series::new("Quick Ratio", (balance_sheet.column("CurrentAssets")? / balance_sheet.column("CurrentLiabilities")?)?),
|
|
32
|
+
Series::new("Current Ratio", (balance_sheet.column("CurrentAssets")? / balance_sheet.column("CurrentLiabilities")?)?),
|
|
33
|
+
Series::new("Debt to Equity", (balance_sheet.column("TotalLiabilitiesNetMinorityInterest")? / balance_sheet.column("TotalEquityGrossMinorityInterest")?)?),
|
|
34
|
+
Series::new("Debt to Assets", (balance_sheet.column("TotalLiabilitiesNetMinorityInterest")? / balance_sheet.column("TotalAssets")?)?),
|
|
35
|
+
Series::new("Interest Coverage", (income_statement.column("EBIT")? / income_statement.column("InterestExpense")?)?),
|
|
36
|
+
Series::new("Asset Turnover", (income_statement.column("TotalRevenue")? / balance_sheet.column("TotalAssets")?)?),
|
|
37
|
+
Series::new("Inventory Turnover", (income_statement.column("CostOfRevenue")? / balance_sheet.column("Inventory")?)?),
|
|
38
|
+
Series::new("Days Receivable", (balance_sheet.column("AccountsReceivable")? / income_statement.column("TotalRevenue")?)? * 365.0),
|
|
39
|
+
Series::new("Days Inventory", (balance_sheet.column("Inventory")? / income_statement.column("CostOfRevenue")?)? * 365.0),
|
|
40
|
+
Series::new("Days Payable", (balance_sheet.column("AccountsPayable")? / income_statement.column("CostOfRevenue")?)? * 365.0),
|
|
41
|
+
Series::new("Earnings per Share", income_statement.column("DilutedEPS")?),
|
|
42
|
+
Series::new("Price to Earnings", (balance_sheet.column("TotalCapitalization")? / income_statement.column("NetIncome")?)?),
|
|
43
|
+
Series::new("Price to Book", (balance_sheet.column("TotalCapitalization")? / balance_sheet.column("TotalEquityGrossMinorityInterest")?)?),
|
|
44
|
+
Series::new("Price to Sales", (balance_sheet.column("TotalCapitalization")? / income_statement.column("TotalRevenue")?)?),
|
|
45
|
+
Series::new("Price to Cashflow", (balance_sheet.column("TotalCapitalization")? / cash_flow.column("OperatingCashFlow")?)?),
|
|
46
|
+
Series::new("Price to Free Cashflow", (balance_sheet.column("TotalCapitalization")? / cash_flow.column("FreeCashFlow")?)?),
|
|
47
|
+
];
|
|
24
48
|
|
|
25
|
-
let df =
|
|
26
|
-
"date" => *&income_statement.column("asOfDate")?,
|
|
27
|
-
"Gross Profit Margin" => *&income_statement.column("GrossProfit")? / *&income_statement.column("TotalRevenue")?,
|
|
28
|
-
"Operating Profit Margin" => *&income_statement.column("EBIT")? / *&income_statement.column("TotalRevenue")?,
|
|
29
|
-
"Net Profit Margin" => *&income_statement.column("NetIncome")? / *&income_statement.column("TotalRevenue")?,
|
|
30
|
-
"Return on Assets" => *&income_statement.column("NetIncome")? / *&balance_sheet.column("TotalAssets")?,
|
|
31
|
-
"Return on Equity" => *&income_statement.column("NetIncome")? / *&balance_sheet.column("TotalEquityGrossMinorityInterest")?,
|
|
32
|
-
"Quick Ratio" => *&balance_sheet.column("CurrentAssets")? / *&balance_sheet.column("CurrentLiabilities")?,
|
|
33
|
-
"Current Ratio" => *&balance_sheet.column("CurrentAssets")? / *&balance_sheet.column("CurrentLiabilities")?,
|
|
34
|
-
"Debt to Equity" => *&balance_sheet.column("TotalLiabilitiesNetMinorityInterest")? / *&balance_sheet.column("TotalEquityGrossMinorityInterest")?,
|
|
35
|
-
"Debt to Assets" => *&balance_sheet.column("TotalLiabilitiesNetMinorityInterest")? / *&balance_sheet.column("TotalAssets")?,
|
|
36
|
-
"Interest Coverage" => *&income_statement.column("EBIT")? / *&income_statement.column("InterestExpense")?,
|
|
37
|
-
"Asset Turnover" => *&income_statement.column("TotalRevenue")? / *&balance_sheet.column("TotalAssets")?,
|
|
38
|
-
"Inventory Turnover" => *&income_statement.column("CostOfRevenue")? / *&balance_sheet.column("Inventory")?,
|
|
39
|
-
"Days Receivable" => *&balance_sheet.column("AccountsReceivable")? / *&income_statement.column("TotalRevenue")? * 365.0,
|
|
40
|
-
"Days Inventory" => *&balance_sheet.column("Inventory")? / *&income_statement.column("CostOfRevenue")? * 365.0,
|
|
41
|
-
"Days Payable" => *&balance_sheet.column("AccountsPayable")? / *&income_statement.column("CostOfRevenue")? * 365.0,
|
|
42
|
-
"Earnings per Share" => *&income_statement.column("DilutedEPS")?,
|
|
43
|
-
"Price to Earnings" => *&balance_sheet.column("TotalCapitalization")? / *&income_statement.column("NetIncome")?,
|
|
44
|
-
"Price to Book" => *&balance_sheet.column("TotalCapitalization")? / *&balance_sheet.column("TotalEquityGrossMinorityInterest")?,
|
|
45
|
-
"Price to Sales" => *&balance_sheet.column("TotalCapitalization")? / *&income_statement.column("TotalRevenue")?,
|
|
46
|
-
"Price to Cashflow" => *&balance_sheet.column("TotalCapitalization")? / *&cash_flow.column("OperatingCashFlow")?,
|
|
47
|
-
"Price to Free Cashflow" => *&balance_sheet.column("TotalCapitalization")? / *&cash_flow.column("FreeCashFlow")?,
|
|
48
|
-
)?;
|
|
49
|
+
let df = DataFrame::new(ratios)?;
|
|
49
50
|
|
|
50
51
|
// Transpose the DataFrame
|
|
51
|
-
let dates = df.column("date")?.
|
|
52
|
-
.
|
|
53
|
-
let df = df.drop("date").unwrap();
|
|
52
|
+
let dates = df.column("date")?.str()?.into_no_null_iter()
|
|
53
|
+
.collect::<Vec<&str>>();
|
|
54
|
+
let mut df = df.drop("date").unwrap();
|
|
54
55
|
let items = Series::new("Items", df.get_column_names());
|
|
55
56
|
let mut transposed_df = df.transpose(None, None)?;
|
|
56
57
|
let _ = transposed_df.set_column_names(&dates)?;
|
|
57
|
-
let _ = transposed_df.
|
|
58
|
+
let _ = transposed_df.insert_column(0, items)?;
|
|
58
59
|
Ok(transposed_df)
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -102,15 +103,15 @@ impl Financials for Ticker {
|
|
|
102
103
|
.collect();
|
|
103
104
|
|
|
104
105
|
// Select and alias columns using the expressions
|
|
105
|
-
let renamed_df = df.lazy().select(expressions).collect()?;
|
|
106
|
+
let mut renamed_df = df.lazy().select(expressions).collect()?;
|
|
106
107
|
|
|
107
108
|
// Transpose the DataFrame
|
|
108
|
-
let dates = income_statement.column("asOfDate")?.
|
|
109
|
-
.
|
|
109
|
+
let dates = income_statement.column("asOfDate")?.str()?.into_no_null_iter()
|
|
110
|
+
.collect::<Vec<&str>>();
|
|
110
111
|
let items = Series::new("Items", renamed_df.get_column_names());
|
|
111
112
|
let mut transposed_df = renamed_df.transpose(None, None)?;
|
|
112
113
|
let _ = transposed_df.set_column_names(&dates)?;
|
|
113
|
-
let _ = transposed_df.
|
|
114
|
+
let _ = transposed_df.insert_column(0, items)?;
|
|
114
115
|
Ok(transposed_df)
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -161,7 +162,7 @@ impl Financials for Ticker {
|
|
|
161
162
|
"CommonStock", "RetainedEarnings", "CommonStockEquity", "TotalEquityGrossMinorityInterest",
|
|
162
163
|
];
|
|
163
164
|
|
|
164
|
-
// remove item from cols if it
|
|
165
|
+
// remove item from cols if it doesn't exist in the balance sheet dataframe
|
|
165
166
|
cols.retain(|x| balance_sheet.column(*x).is_ok());
|
|
166
167
|
|
|
167
168
|
|
|
@@ -177,15 +178,15 @@ impl Financials for Ticker {
|
|
|
177
178
|
.collect();
|
|
178
179
|
|
|
179
180
|
// Select and alias columns using the expressions
|
|
180
|
-
let renamed_df = df.lazy().select(expressions).collect()?;
|
|
181
|
+
let mut renamed_df = df.lazy().select(expressions).collect()?;
|
|
181
182
|
|
|
182
183
|
// Transpose the DataFrame
|
|
183
|
-
let dates = balance_sheet.column("asOfDate")?.
|
|
184
|
-
.
|
|
184
|
+
let dates = balance_sheet.column("asOfDate")?.str()?.into_no_null_iter()
|
|
185
|
+
.collect::<Vec<&str>>();
|
|
185
186
|
let items = Series::new("Items", renamed_df.get_column_names());
|
|
186
187
|
let mut transposed_df = renamed_df.transpose(None, None)?;
|
|
187
188
|
let _ = transposed_df.set_column_names(&dates)?;
|
|
188
|
-
let _ = transposed_df.
|
|
189
|
+
let _ = transposed_df.insert_column(0, items)?;
|
|
189
190
|
|
|
190
191
|
Ok(transposed_df)
|
|
191
192
|
|
|
@@ -267,15 +268,15 @@ impl Financials for Ticker {
|
|
|
267
268
|
.collect();
|
|
268
269
|
|
|
269
270
|
// Select and alias columns using the expressions
|
|
270
|
-
let renamed_df = df.lazy().select(expressions).collect()?;
|
|
271
|
+
let mut renamed_df = df.lazy().select(expressions).collect()?;
|
|
271
272
|
|
|
272
273
|
// Transpose the DataFrame
|
|
273
|
-
let dates = cash_flow.column("asOfDate")?.
|
|
274
|
-
.
|
|
274
|
+
let dates = cash_flow.column("asOfDate")?.str()?.into_no_null_iter()
|
|
275
|
+
.collect::<Vec<&str>>();
|
|
275
276
|
let items = Series::new("Items", renamed_df.get_column_names());
|
|
276
277
|
let mut transposed_df = renamed_df.transpose(None, None)?;
|
|
277
278
|
let _ = transposed_df.set_column_names(&dates)?;
|
|
278
|
-
let _ = transposed_df.
|
|
279
|
+
let _ = transposed_df.insert_column(0, items)?;
|
|
279
280
|
|
|
280
281
|
Ok(transposed_df)
|
|
281
282
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use polars::prelude::*;
|
|
2
2
|
use std::error::Error;
|
|
3
|
+
use chrono::{DateTime, NaiveDateTime};
|
|
3
4
|
use futures::future::join_all;
|
|
4
5
|
|
|
5
6
|
use crate::data::config::Interval;
|
|
@@ -15,6 +16,7 @@ pub struct TickerPerformanceStats {
|
|
|
15
16
|
pub benchmark_symbol: String,
|
|
16
17
|
pub start_date: String,
|
|
17
18
|
pub end_date: String,
|
|
19
|
+
pub dates_array: Vec<String>,
|
|
18
20
|
pub interval: Interval,
|
|
19
21
|
pub confidence_level: f64,
|
|
20
22
|
pub risk_free_rate: f64,
|
|
@@ -49,17 +51,22 @@ impl TickerPerformance for Ticker {
|
|
|
49
51
|
.risk_free_rate(self.risk_free_rate)
|
|
50
52
|
.build();
|
|
51
53
|
let benchmark_returns = benchmark_ticker.roc(1).await?;
|
|
52
|
-
let benchmark_returns =
|
|
53
|
-
&
|
|
54
|
+
let benchmark_returns = security_returns.join(
|
|
55
|
+
&benchmark_returns,
|
|
54
56
|
&["timestamp"],
|
|
55
57
|
&["timestamp"],
|
|
56
|
-
JoinArgs::new(JoinType::
|
|
58
|
+
JoinArgs::new(JoinType::Left),
|
|
57
59
|
)?;
|
|
58
|
-
let benchmark_returns = benchmark_returns.sort(&["timestamp"], false
|
|
60
|
+
let benchmark_returns = benchmark_returns.sort(&["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
|
|
59
61
|
let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Forward(None))?;
|
|
60
62
|
let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Backward(None))?;
|
|
63
|
+
let dates_array = benchmark_returns.column("timestamp")?.datetime()?
|
|
64
|
+
.into_no_null_iter().map(|x| DateTime::from_timestamp_millis(x).unwrap()
|
|
65
|
+
.naive_local()).collect::<Vec<NaiveDateTime>>();
|
|
66
|
+
let dates_array = dates_array.iter().map(|x| x.to_string()).collect::<Vec<String>>();
|
|
67
|
+
let security_returns = benchmark_returns.column(&*self.ticker)?.clone();
|
|
61
68
|
let benchmark_returns = benchmark_returns.column("roc-1")?.clone();
|
|
62
|
-
|
|
69
|
+
|
|
63
70
|
let performance_stats = PerformanceStats::compute_stats(
|
|
64
71
|
security_returns.clone(), benchmark_returns.clone(),
|
|
65
72
|
self.risk_free_rate, self.confidence_level, self.interval)?;
|
|
@@ -68,6 +75,7 @@ impl TickerPerformance for Ticker {
|
|
|
68
75
|
benchmark_symbol: self.benchmark_symbol.clone(),
|
|
69
76
|
start_date: self.start_date.clone(),
|
|
70
77
|
end_date: self.end_date.clone(),
|
|
78
|
+
dates_array,
|
|
71
79
|
interval: self.interval.clone(),
|
|
72
80
|
confidence_level: self.confidence_level,
|
|
73
81
|
risk_free_rate: self.risk_free_rate,
|
|
@@ -89,6 +97,7 @@ pub struct PortfolioPerformanceStats {
|
|
|
89
97
|
pub start_date: String,
|
|
90
98
|
pub end_date: String,
|
|
91
99
|
pub interval: Interval,
|
|
100
|
+
pub dates_array: Vec<String>,
|
|
92
101
|
pub confidence_level: f64,
|
|
93
102
|
pub risk_free_rate: f64,
|
|
94
103
|
pub portfolio_returns: DataFrame,
|
|
@@ -191,10 +200,11 @@ impl PortfolioPerformanceStats {
|
|
|
191
200
|
df,
|
|
192
201
|
&["timestamp"],
|
|
193
202
|
&["timestamp"],
|
|
194
|
-
JoinArgs::new(JoinType::
|
|
203
|
+
JoinArgs::new(JoinType::Full).with_coalesce(JoinCoalesce::CoalesceColumns),
|
|
195
204
|
)?;
|
|
196
205
|
}
|
|
197
|
-
|
|
206
|
+
|
|
207
|
+
portfolio_returns = portfolio_returns.sort(&["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
|
|
198
208
|
portfolio_returns = portfolio_returns.fill_null(FillNullStrategy::Forward(None))?;
|
|
199
209
|
portfolio_returns = portfolio_returns.fill_null(FillNullStrategy::Backward(None))?;
|
|
200
210
|
|
|
@@ -206,23 +216,20 @@ impl PortfolioPerformanceStats {
|
|
|
206
216
|
.risk_free_rate(risk_free_rate)
|
|
207
217
|
.build();
|
|
208
218
|
let benchmark_returns = benchmark_ticker.roc(1).await?;
|
|
209
|
-
let benchmark_returns =
|
|
210
|
-
benchmark_returns.join(
|
|
211
|
-
&portfolio_returns,
|
|
212
|
-
&["timestamp"],
|
|
213
|
-
&["timestamp"],
|
|
214
|
-
JoinArgs::new(JoinType::Outer),
|
|
215
|
-
)?} else {
|
|
216
|
-
portfolio_returns.join(
|
|
219
|
+
let benchmark_returns = portfolio_returns.join(
|
|
217
220
|
&benchmark_returns,
|
|
218
221
|
&["timestamp"],
|
|
219
222
|
&["timestamp"],
|
|
220
|
-
JoinArgs::new(JoinType::
|
|
221
|
-
)
|
|
223
|
+
JoinArgs::new(JoinType::Left),
|
|
224
|
+
)?;
|
|
222
225
|
|
|
223
|
-
let benchmark_returns = benchmark_returns.sort(&["timestamp"], false
|
|
226
|
+
let benchmark_returns = benchmark_returns.sort(&["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
|
|
224
227
|
let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Forward(None))?;
|
|
225
228
|
let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Backward(None))?;
|
|
229
|
+
let dates_array = benchmark_returns.column("timestamp")?.datetime()?
|
|
230
|
+
.into_no_null_iter().map(|x| DateTime::from_timestamp_millis(x).unwrap()
|
|
231
|
+
.naive_local()).collect::<Vec<NaiveDateTime>>();
|
|
232
|
+
let dates_array = dates_array.iter().map(|x| x.to_string()).collect::<Vec<String>>();
|
|
226
233
|
let benchmark_returns = benchmark_returns.column("roc-1")?.clone();
|
|
227
234
|
|
|
228
235
|
let _ = portfolio_returns.drop_in_place("timestamp")?;
|
|
@@ -233,6 +240,7 @@ impl PortfolioPerformanceStats {
|
|
|
233
240
|
start_date: start_date.to_string(),
|
|
234
241
|
end_date: end_date.to_string(),
|
|
235
242
|
interval,
|
|
243
|
+
dates_array,
|
|
236
244
|
confidence_level,
|
|
237
245
|
risk_free_rate,
|
|
238
246
|
portfolio_returns: portfolio_returns.clone(),
|
|
@@ -253,8 +261,16 @@ impl PortfolioPerformanceStats {
|
|
|
253
261
|
///
|
|
254
262
|
/// * `PortfolioPerformanceStats` struct
|
|
255
263
|
pub fn compute_stats(&self) -> Result<PortfolioPerformanceStats, Box<dyn Error>> {
|
|
256
|
-
let mean_returns = self.portfolio_returns
|
|
257
|
-
.
|
|
264
|
+
let mean_returns = self.portfolio_returns
|
|
265
|
+
.get_columns()
|
|
266
|
+
.iter()
|
|
267
|
+
.map(|col| {
|
|
268
|
+
col.f64()
|
|
269
|
+
.unwrap()
|
|
270
|
+
.mean()
|
|
271
|
+
.unwrap()
|
|
272
|
+
})
|
|
273
|
+
.collect::<Vec<f64>>();
|
|
258
274
|
let cov_matrix = covariance_matrix(&self.portfolio_returns)?;
|
|
259
275
|
|
|
260
276
|
let opt_result = portfolio_optimization(&mean_returns, &cov_matrix, &self.portfolio_returns, self.risk_free_rate,
|
|
@@ -273,6 +289,7 @@ impl PortfolioPerformanceStats {
|
|
|
273
289
|
start_date: self.start_date.clone(),
|
|
274
290
|
end_date: self.end_date.clone(),
|
|
275
291
|
interval: self.interval.clone(),
|
|
292
|
+
dates_array: self.dates_array.clone(),
|
|
276
293
|
confidence_level: self.confidence_level,
|
|
277
294
|
risk_free_rate: self.risk_free_rate,
|
|
278
295
|
portfolio_returns: self.portfolio_returns.clone(),
|
|
@@ -85,7 +85,7 @@ impl PerformanceStats {
|
|
|
85
85
|
let downside_mask = &returns.lt_eq(0.0).unwrap();
|
|
86
86
|
let downside_returns = returns.filter(downside_mask).unwrap();
|
|
87
87
|
let sortino_ratio = (annualized_return - risk_free_rate) / (std_dev( &downside_returns) * 252.0_f64.sqrt());
|
|
88
|
-
let excess_returns = returns.clone() - benchmark_returns.clone()
|
|
88
|
+
let excess_returns = (returns.clone() - benchmark_returns.clone())?;
|
|
89
89
|
let active_return = excess_returns.mean().ok_or("Error calculating active return")?;
|
|
90
90
|
let active_return = ((1.0 + active_return/100.0).powf(252.0) - 1.0) * 100.0;
|
|
91
91
|
let active_risk = std_dev(&excess_returns) * 252.0_f64.sqrt();
|
|
@@ -309,7 +309,7 @@ pub fn rand_weights(num_assets: usize) -> Vec<f64> {
|
|
|
309
309
|
pub fn mean_portfolio_return(weights: &Vec<f64>, mean_returns: &Vec<f64>) -> f64 {
|
|
310
310
|
let weights = Series::new("Weights", weights);
|
|
311
311
|
let mean_returns = Series::new("Mean Returns", mean_returns);
|
|
312
|
-
let weighted_returns = mean_returns.mul(weights);
|
|
312
|
+
let weighted_returns = mean_returns.mul(weights).unwrap();
|
|
313
313
|
let mean_portfolio_return = weighted_returns.sum().unwrap();
|
|
314
314
|
mean_portfolio_return
|
|
315
315
|
}
|
|
@@ -348,7 +348,7 @@ pub fn daily_portfolio_returns(weights: &Vec<f64>, returns: &DataFrame) -> Serie
|
|
|
348
348
|
let col_str = returns.get_column_names()[i];
|
|
349
349
|
let security_returns = returns.column(col_str).unwrap().f64().unwrap().to_vec();
|
|
350
350
|
let weighted_returns = security_returns.iter().map(|x| x.unwrap() * weight).collect::<Vec<f64>>();
|
|
351
|
-
portfolio_returns = portfolio_returns + Series::new("Portfolio Returns", weighted_returns);
|
|
351
|
+
portfolio_returns = (portfolio_returns + Series::new("Portfolio Returns", weighted_returns)).unwrap();
|
|
352
352
|
}
|
|
353
353
|
portfolio_returns
|
|
354
354
|
}
|
|
@@ -253,7 +253,7 @@ impl VolatilitySurface for Ticker {
|
|
|
253
253
|
let vol_df = df.filter(&mask)?;
|
|
254
254
|
if vol_df.height() > 0 {
|
|
255
255
|
let option_price = vol_df.column("lastPrice")?.f64()?.get(0).unwrap();
|
|
256
|
-
let option_type = vol_df.column("type")?.
|
|
256
|
+
let option_type = vol_df.column("type")?.str()?.get(0).unwrap();
|
|
257
257
|
let option_type = match option_type {
|
|
258
258
|
"call" => OptionType::Call,
|
|
259
259
|
"put" => OptionType::Put,
|
|
@@ -3,8 +3,8 @@ use plotly::{Bar, Histogram, Layout, Plot, Scatter, Table};
|
|
|
3
3
|
use plotly::color::NamedColor;
|
|
4
4
|
use plotly::common::{Fill, Marker, MarkerSymbol, Mode, Title};
|
|
5
5
|
use plotly::layout::{Axis, GridPattern, LayoutGrid, RowOrder};
|
|
6
|
+
use plotly::traces::table::{Cells, Header};
|
|
6
7
|
use crate::analytics::statistics::cumulative_returns_list;
|
|
7
|
-
use crate::utils::date_utils::generate_dates;
|
|
8
8
|
use crate::models::portfolio::Portfolio;
|
|
9
9
|
|
|
10
10
|
|
|
@@ -120,8 +120,7 @@ impl PortfolioCharts for Portfolio {
|
|
|
120
120
|
///
|
|
121
121
|
/// * `Plot` Plotly Chart struct
|
|
122
122
|
fn performance_chart(&self, height: usize, width: usize) -> Result<Plot, Box<dyn Error>> {
|
|
123
|
-
let dates =
|
|
124
|
-
&*self.performance_stats.end_date, 1);
|
|
123
|
+
let dates = self.performance_stats.dates_array.clone();
|
|
125
124
|
|
|
126
125
|
let returns = self.performance_stats.optimal_portfolio_returns.f64().unwrap().to_vec()
|
|
127
126
|
.iter().map(|x| x.unwrap()).collect::<Vec<f64>>();
|
|
@@ -207,7 +206,7 @@ impl PortfolioCharts for Portfolio {
|
|
|
207
206
|
fn asset_returns_chart(&self, height: usize, width: usize) -> Result<Plot, Box<dyn Error>> {
|
|
208
207
|
let symbols = self.performance_stats.ticker_symbols.clone();
|
|
209
208
|
let asset_returns = self.performance_stats.portfolio_returns.clone();
|
|
210
|
-
let dates =
|
|
209
|
+
let dates = self.performance_stats.dates_array.clone();
|
|
211
210
|
let mut plot = Plot::new();
|
|
212
211
|
|
|
213
212
|
for symbol in symbols {
|
|
@@ -289,11 +288,11 @@ impl PortfolioCharts for Portfolio {
|
|
|
289
288
|
|
|
290
289
|
|
|
291
290
|
let trace = Table::new(
|
|
292
|
-
vec![
|
|
291
|
+
Header::new(vec![
|
|
293
292
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Performance Stats"),
|
|
294
293
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Values"),
|
|
295
|
-
],
|
|
296
|
-
vec![fields, values],
|
|
294
|
+
]),
|
|
295
|
+
Cells::new(vec![fields, values]),
|
|
297
296
|
);
|
|
298
297
|
let mut plot = Plot::new();
|
|
299
298
|
plot.add_trace(trace);
|
|
@@ -6,6 +6,7 @@ use num_format::{Locale, ToFormattedString};
|
|
|
6
6
|
use plotly::common::{Fill, Line, LineShape, Mode, Title};
|
|
7
7
|
use plotly::{Bar, Candlestick, Histogram, Layout, Plot, Scatter, Surface, Table};
|
|
8
8
|
use plotly::layout::{Axis, GridPattern, LayoutGrid, LayoutScene, RangeSelector, RangeSlider, RowOrder, SelectorButton, SelectorStep, StepMode};
|
|
9
|
+
use plotly::traces::table::{Cells, Header};
|
|
9
10
|
|
|
10
11
|
use crate::models::ticker::Ticker;
|
|
11
12
|
use crate::data::ticker::TickerData;
|
|
@@ -13,7 +14,7 @@ use crate::prelude::TechnicalIndicators;
|
|
|
13
14
|
use crate::analytics::fundamentals::Financials;
|
|
14
15
|
use crate::analytics::performance::TickerPerformance;
|
|
15
16
|
use crate::analytics::stochastics::VolatilitySurface;
|
|
16
|
-
use crate::utils::date_utils::
|
|
17
|
+
use crate::utils::date_utils::to_date;
|
|
17
18
|
use crate::analytics::statistics::cumulative_returns_list;
|
|
18
19
|
|
|
19
20
|
pub trait TickerCharts {
|
|
@@ -86,8 +87,7 @@ impl TickerCharts for Ticker {
|
|
|
86
87
|
let layout = Layout::new()
|
|
87
88
|
.height(height)
|
|
88
89
|
.width(width)
|
|
89
|
-
.title(
|
|
90
|
-
self.ticker)))
|
|
90
|
+
.title(&*format!("<span style=\"font-weight:bold; color:darkgreen;\">{} Candlestick Chart</span>", self.ticker))
|
|
91
91
|
.grid(
|
|
92
92
|
LayoutGrid::new()
|
|
93
93
|
.rows(3)
|
|
@@ -171,9 +171,7 @@ impl TickerCharts for Ticker {
|
|
|
171
171
|
/// * `Plot` Plotly Chart struct
|
|
172
172
|
async fn performance_chart(&self, height: usize, width: usize) -> Result<Plot, Box<dyn Error>> {
|
|
173
173
|
let performance_stats = self.performance_stats().await?;
|
|
174
|
-
let dates =
|
|
175
|
-
&*performance_stats.end_date, 1);
|
|
176
|
-
|
|
174
|
+
let dates = performance_stats.dates_array;
|
|
177
175
|
let returns = performance_stats.security_returns.f64().unwrap().to_vec()
|
|
178
176
|
.iter().map(|x| x.unwrap()).collect::<Vec<f64>>();
|
|
179
177
|
|
|
@@ -331,11 +329,11 @@ impl TickerCharts for Ticker {
|
|
|
331
329
|
];
|
|
332
330
|
|
|
333
331
|
let trace = Table::new(
|
|
334
|
-
vec![
|
|
332
|
+
Header::new(vec![
|
|
335
333
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Summary Stats"),
|
|
336
334
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Values"),
|
|
337
|
-
],
|
|
338
|
-
vec![fields, values],
|
|
335
|
+
]),
|
|
336
|
+
Cells::new(vec![fields, values]),
|
|
339
337
|
);
|
|
340
338
|
let mut plot = Plot::new();
|
|
341
339
|
plot.add_trace(trace);
|
|
@@ -404,11 +402,11 @@ impl TickerCharts for Ticker {
|
|
|
404
402
|
|
|
405
403
|
|
|
406
404
|
let trace = Table::new(
|
|
407
|
-
vec![
|
|
405
|
+
Header::new(vec![
|
|
408
406
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Performance Stats"),
|
|
409
407
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Values"),
|
|
410
|
-
],
|
|
411
|
-
vec![fields, values],
|
|
408
|
+
]),
|
|
409
|
+
Cells::new(vec![fields, values]),
|
|
412
410
|
);
|
|
413
411
|
let mut plot = Plot::new();
|
|
414
412
|
plot.add_trace(trace);
|
|
@@ -453,7 +451,7 @@ impl TickerCharts for Ticker {
|
|
|
453
451
|
x.iter()
|
|
454
452
|
.map(|y| match y {
|
|
455
453
|
AnyValue::Null => "".to_string(),
|
|
456
|
-
AnyValue::
|
|
454
|
+
AnyValue::String(str_val) => str_val.to_string(),
|
|
457
455
|
AnyValue::Float64(val) => if name == &"Financial Ratios" {format!("{:.2}", val)}
|
|
458
456
|
else if val > -999.0 && val < 999.0 {format!("${:.2}", val)}
|
|
459
457
|
else{format!("${}", (val as i64).to_formatted_string(&Locale::en))
|
|
@@ -464,8 +462,8 @@ impl TickerCharts for Ticker {
|
|
|
464
462
|
})
|
|
465
463
|
.collect::<Vec<Vec<String>>>();
|
|
466
464
|
let trace = Table::new(
|
|
467
|
-
fields,
|
|
468
|
-
values,
|
|
465
|
+
Header::new(fields),
|
|
466
|
+
Cells::new(values),
|
|
469
467
|
);
|
|
470
468
|
let mut plot = Plot::new();
|
|
471
469
|
plot.add_trace(trace);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
use chrono::{DateTime, NaiveDate, NaiveDateTime, Timelike, Utc};
|
|
2
2
|
use std::error::Error;
|
|
3
|
-
use std::str::FromStr;
|
|
4
3
|
|
|
5
4
|
/// Converts a date string in YYYY-MM-DD format to a Unix Timestamp
|
|
6
5
|
pub fn to_timestamp(date_str: &str) -> Result<i64, Box<dyn Error>> {
|
|
@@ -23,22 +22,6 @@ pub fn to_datetime(date_str: &str) -> Result<NaiveDateTime, Box<dyn Error>> {
|
|
|
23
22
|
Ok(datetime)
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
/// Generates a vector of dates between a start date and an end date
|
|
27
|
-
pub fn generate_dates(start_date: &str, end_date: &str, interval_days: i64) -> Vec<String> {
|
|
28
|
-
let start_date = NaiveDate::from_str(start_date).expect("Invalid start date");
|
|
29
|
-
let end_date = NaiveDate::from_str(end_date).expect("Invalid end date");
|
|
30
|
-
|
|
31
|
-
let mut current_date = start_date;
|
|
32
|
-
let mut date_vector = Vec::new();
|
|
33
|
-
|
|
34
|
-
while current_date <= end_date {
|
|
35
|
-
date_vector.push(current_date.to_string());
|
|
36
|
-
current_date += chrono::Duration::days(interval_days);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
date_vector
|
|
40
|
-
}
|
|
41
|
-
|
|
42
25
|
/// Computes the the time to maturity of an option in months
|
|
43
26
|
pub fn time_to_maturity(timestamp: i64) -> f64{
|
|
44
27
|
let current_date = chrono::Local::now().naive_local().date();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "finalytics-py"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.2"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
|
|
|
10
10
|
|
|
11
11
|
[dependencies]
|
|
12
12
|
finalytics = { path = "../finalytics", features = ["kaleido"] }
|
|
13
|
-
polars = "0.
|
|
13
|
+
polars = "0.41.3"
|
|
14
14
|
tokio = { version = "1.32.0", features = ["full"] }
|
|
15
15
|
pyo3 = { version = "0.20.0", features = ["extension-module", "generate-import-lib"] }
|
|
16
|
-
plotly =
|
|
16
|
+
plotly = "0.9.0"
|
|
@@ -53,7 +53,7 @@ pub fn to_py_array(py: Python, pyarrow: &PyModule, array: ArrayRef) -> PyResult<
|
|
|
53
53
|
pub fn rust_series_to_py_series(series: &Series) -> PyResult<PyObject> {
|
|
54
54
|
// ensure we have a single chunk
|
|
55
55
|
let series = series.rechunk();
|
|
56
|
-
let array = series.to_arrow(0);
|
|
56
|
+
let array = series.to_arrow(0, false);
|
|
57
57
|
|
|
58
58
|
Python::with_gil(|py| {
|
|
59
59
|
// import pyarrow
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|