finalytics 0.4.0__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.0 → finalytics-0.4.2}/PKG-INFO +1 -1
- {finalytics-0.4.0 → finalytics-0.4.2}/README.md +5 -5
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/Cargo.toml +5 -4
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/fundamentals.rs +42 -41
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/performance.rs +82 -30
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/statistics.rs +3 -3
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/stochastics.rs +1 -1
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/charts/portfolio.rs +31 -16
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/charts/ticker.rs +13 -15
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/data/ticker.rs +99 -63
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/lib.rs +25 -4
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/models/portfolio.rs +3 -3
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/models/ticker.rs +1 -1
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/utils/date_utils.rs +0 -17
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/utils/web_utils.rs +1 -2
- {finalytics-0.4.0 → finalytics-0.4.2}/python/Cargo.toml +3 -3
- {finalytics-0.4.0 → finalytics-0.4.2}/python/conf.py +9 -18
- finalytics-0.4.2/python/index.rst +198 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/python/src/ffi.rs +1 -1
- finalytics-0.4.0/python/index.rst +0 -6
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/optimization.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/sentiment.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/analytics/technicals.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/charts/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/data/config.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/data/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/models/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/utils/chart_utils.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/finalytics/src/utils/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/pyproject.toml +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/python/Cargo.lock +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/python/README.md +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/python/build.sh +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/python/src/lib.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/python/src/portfolio.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.2}/python/src/ticker.rs +0 -0
- {finalytics-0.4.0 → 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.
|
|
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,18 +24,19 @@ 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"
|
|
31
|
-
statrs = "0.
|
|
31
|
+
statrs = "0.17.1"
|
|
32
32
|
rand = "0.8.5"
|
|
33
33
|
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
|
+
futures = "0.3.30"
|
|
39
40
|
|
|
40
41
|
[features]
|
|
41
42
|
kaleido = ["plotly/kaleido"]
|
|
@@ -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,7 @@
|
|
|
1
1
|
use polars::prelude::*;
|
|
2
2
|
use std::error::Error;
|
|
3
|
+
use chrono::{DateTime, NaiveDateTime};
|
|
4
|
+
use futures::future::join_all;
|
|
3
5
|
|
|
4
6
|
use crate::data::config::Interval;
|
|
5
7
|
use crate::models::ticker::{Ticker, TickerBuilder};
|
|
@@ -14,6 +16,7 @@ pub struct TickerPerformanceStats {
|
|
|
14
16
|
pub benchmark_symbol: String,
|
|
15
17
|
pub start_date: String,
|
|
16
18
|
pub end_date: String,
|
|
19
|
+
pub dates_array: Vec<String>,
|
|
17
20
|
pub interval: Interval,
|
|
18
21
|
pub confidence_level: f64,
|
|
19
22
|
pub risk_free_rate: f64,
|
|
@@ -48,17 +51,22 @@ impl TickerPerformance for Ticker {
|
|
|
48
51
|
.risk_free_rate(self.risk_free_rate)
|
|
49
52
|
.build();
|
|
50
53
|
let benchmark_returns = benchmark_ticker.roc(1).await?;
|
|
51
|
-
let benchmark_returns =
|
|
52
|
-
&
|
|
54
|
+
let benchmark_returns = security_returns.join(
|
|
55
|
+
&benchmark_returns,
|
|
53
56
|
&["timestamp"],
|
|
54
57
|
&["timestamp"],
|
|
55
|
-
JoinArgs::new(JoinType::
|
|
58
|
+
JoinArgs::new(JoinType::Left),
|
|
56
59
|
)?;
|
|
57
|
-
let benchmark_returns = benchmark_returns.sort(&["timestamp"], false
|
|
60
|
+
let benchmark_returns = benchmark_returns.sort(&["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
|
|
58
61
|
let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Forward(None))?;
|
|
59
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();
|
|
60
68
|
let benchmark_returns = benchmark_returns.column("roc-1")?.clone();
|
|
61
|
-
|
|
69
|
+
|
|
62
70
|
let performance_stats = PerformanceStats::compute_stats(
|
|
63
71
|
security_returns.clone(), benchmark_returns.clone(),
|
|
64
72
|
self.risk_free_rate, self.confidence_level, self.interval)?;
|
|
@@ -67,6 +75,7 @@ impl TickerPerformance for Ticker {
|
|
|
67
75
|
benchmark_symbol: self.benchmark_symbol.clone(),
|
|
68
76
|
start_date: self.start_date.clone(),
|
|
69
77
|
end_date: self.end_date.clone(),
|
|
78
|
+
dates_array,
|
|
70
79
|
interval: self.interval.clone(),
|
|
71
80
|
confidence_level: self.confidence_level,
|
|
72
81
|
risk_free_rate: self.risk_free_rate,
|
|
@@ -88,6 +97,7 @@ pub struct PortfolioPerformanceStats {
|
|
|
88
97
|
pub start_date: String,
|
|
89
98
|
pub end_date: String,
|
|
90
99
|
pub interval: Interval,
|
|
100
|
+
pub dates_array: Vec<String>,
|
|
91
101
|
pub confidence_level: f64,
|
|
92
102
|
pub risk_free_rate: f64,
|
|
93
103
|
pub portfolio_returns: DataFrame,
|
|
@@ -131,21 +141,55 @@ impl PortfolioPerformanceStats {
|
|
|
131
141
|
max_iterations: u64,
|
|
132
142
|
objective_function: ObjectiveFunction
|
|
133
143
|
) -> Result<PortfolioPerformanceStats, Box<dyn Error>> {
|
|
134
|
-
let mut
|
|
135
|
-
|
|
136
|
-
|
|
144
|
+
let mut futures = Vec::new();
|
|
145
|
+
|
|
146
|
+
for ticker_symbol in ticker_symbols.clone().into_iter() {
|
|
147
|
+
let ticker = TickerBuilder::new().ticker(&ticker_symbol)
|
|
137
148
|
.start_date(start_date)
|
|
138
149
|
.end_date(end_date)
|
|
139
150
|
.interval(interval)
|
|
140
151
|
.confidence_level(confidence_level)
|
|
141
152
|
.risk_free_rate(risk_free_rate)
|
|
142
153
|
.build();
|
|
143
|
-
|
|
144
|
-
let
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
|
|
155
|
+
let fut = tokio::task::spawn(async move {
|
|
156
|
+
match ticker.roc(1).await {
|
|
157
|
+
Ok(security_df) => {
|
|
158
|
+
match DataFrame::new(vec![
|
|
159
|
+
security_df.column("timestamp").map_err(|e| e.to_string())?.clone(),
|
|
160
|
+
security_df.column("roc-1").map_err(|e| e.to_string())?.clone().with_name(&ticker_symbol)
|
|
161
|
+
]) {
|
|
162
|
+
Ok(security_returns_df) => Ok(security_returns_df),
|
|
163
|
+
Err(e) => {
|
|
164
|
+
eprintln!("Error creating DataFrame for {}: {}", ticker_symbol, e);
|
|
165
|
+
Err(format!("Error creating DataFrame for {}: {}", ticker_symbol, e))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
Err(e) => {
|
|
170
|
+
eprintln!("Error Fetching Price Data for {}: {}", ticker_symbol, e);
|
|
171
|
+
Err(format!("Error Fetching Price Data for {}: {}", ticker_symbol, e))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
futures.push(fut);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let results = join_all(futures).await;
|
|
180
|
+
let mut fetched_symbols: Vec<String> = Vec::new();
|
|
181
|
+
let mut dfs: Vec<DataFrame> = Vec::new();
|
|
182
|
+
|
|
183
|
+
for result in results {
|
|
184
|
+
match result {
|
|
185
|
+
Ok(Ok(df)) => {
|
|
186
|
+
let symbol = df.get_column_names()[1].to_string();
|
|
187
|
+
fetched_symbols.push(symbol);
|
|
188
|
+
dfs.push(df);
|
|
189
|
+
}
|
|
190
|
+
Ok(Err(_)) => continue,
|
|
191
|
+
Err(e) => eprintln!("Error in task: {}", e),
|
|
192
|
+
}
|
|
149
193
|
}
|
|
150
194
|
|
|
151
195
|
let mut portfolio_returns = dfs[0].clone();
|
|
@@ -156,10 +200,11 @@ impl PortfolioPerformanceStats {
|
|
|
156
200
|
df,
|
|
157
201
|
&["timestamp"],
|
|
158
202
|
&["timestamp"],
|
|
159
|
-
JoinArgs::new(JoinType::
|
|
203
|
+
JoinArgs::new(JoinType::Full).with_coalesce(JoinCoalesce::CoalesceColumns),
|
|
160
204
|
)?;
|
|
161
205
|
}
|
|
162
|
-
|
|
206
|
+
|
|
207
|
+
portfolio_returns = portfolio_returns.sort(&["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
|
|
163
208
|
portfolio_returns = portfolio_returns.fill_null(FillNullStrategy::Forward(None))?;
|
|
164
209
|
portfolio_returns = portfolio_returns.fill_null(FillNullStrategy::Backward(None))?;
|
|
165
210
|
|
|
@@ -171,33 +216,31 @@ impl PortfolioPerformanceStats {
|
|
|
171
216
|
.risk_free_rate(risk_free_rate)
|
|
172
217
|
.build();
|
|
173
218
|
let benchmark_returns = benchmark_ticker.roc(1).await?;
|
|
174
|
-
let benchmark_returns =
|
|
175
|
-
benchmark_returns.join(
|
|
176
|
-
&portfolio_returns,
|
|
177
|
-
&["timestamp"],
|
|
178
|
-
&["timestamp"],
|
|
179
|
-
JoinArgs::new(JoinType::Outer),
|
|
180
|
-
)?} else {
|
|
181
|
-
portfolio_returns.join(
|
|
219
|
+
let benchmark_returns = portfolio_returns.join(
|
|
182
220
|
&benchmark_returns,
|
|
183
221
|
&["timestamp"],
|
|
184
222
|
&["timestamp"],
|
|
185
|
-
JoinArgs::new(JoinType::
|
|
186
|
-
)
|
|
223
|
+
JoinArgs::new(JoinType::Left),
|
|
224
|
+
)?;
|
|
187
225
|
|
|
188
|
-
let benchmark_returns = benchmark_returns.sort(&["timestamp"], false
|
|
226
|
+
let benchmark_returns = benchmark_returns.sort(&["timestamp"], SortMultipleOptions::new().with_order_descending(false))?;
|
|
189
227
|
let benchmark_returns = benchmark_returns.fill_null(FillNullStrategy::Forward(None))?;
|
|
190
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>>();
|
|
191
233
|
let benchmark_returns = benchmark_returns.column("roc-1")?.clone();
|
|
192
234
|
|
|
193
235
|
let _ = portfolio_returns.drop_in_place("timestamp")?;
|
|
194
236
|
|
|
195
237
|
Ok(PortfolioPerformanceStats {
|
|
196
|
-
ticker_symbols:
|
|
238
|
+
ticker_symbols: fetched_symbols.clone(),
|
|
197
239
|
benchmark_symbol: benchmark_symbol.to_string(),
|
|
198
240
|
start_date: start_date.to_string(),
|
|
199
241
|
end_date: end_date.to_string(),
|
|
200
242
|
interval,
|
|
243
|
+
dates_array,
|
|
201
244
|
confidence_level,
|
|
202
245
|
risk_free_rate,
|
|
203
246
|
portfolio_returns: portfolio_returns.clone(),
|
|
@@ -218,8 +261,16 @@ impl PortfolioPerformanceStats {
|
|
|
218
261
|
///
|
|
219
262
|
/// * `PortfolioPerformanceStats` struct
|
|
220
263
|
pub fn compute_stats(&self) -> Result<PortfolioPerformanceStats, Box<dyn Error>> {
|
|
221
|
-
let mean_returns = self.portfolio_returns
|
|
222
|
-
.
|
|
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>>();
|
|
223
274
|
let cov_matrix = covariance_matrix(&self.portfolio_returns)?;
|
|
224
275
|
|
|
225
276
|
let opt_result = portfolio_optimization(&mean_returns, &cov_matrix, &self.portfolio_returns, self.risk_free_rate,
|
|
@@ -238,6 +289,7 @@ impl PortfolioPerformanceStats {
|
|
|
238
289
|
start_date: self.start_date.clone(),
|
|
239
290
|
end_date: self.end_date.clone(),
|
|
240
291
|
interval: self.interval.clone(),
|
|
292
|
+
dates_array: self.dates_array.clone(),
|
|
241
293
|
confidence_level: self.confidence_level,
|
|
242
294
|
risk_free_rate: self.risk_free_rate,
|
|
243
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
|
|
|
@@ -52,11 +52,20 @@ impl PortfolioCharts for Portfolio {
|
|
|
52
52
|
let ticker_symbols = self.performance_stats.ticker_symbols.clone();
|
|
53
53
|
let weights = self.performance_stats.optimal_weights.clone().iter()
|
|
54
54
|
.map(|x| x * 100.0).collect::<Vec<f64>>();
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
let filtered: Vec<_> = ticker_symbols.iter()
|
|
57
|
+
.zip(weights.iter())
|
|
58
|
+
.filter(|&(_, &weight)| weight > 0.0)
|
|
59
|
+
.collect();
|
|
60
|
+
|
|
61
|
+
let filtered_ticker_symbols: Vec<String> = filtered.iter().map(|&(ticker, _)| ticker.clone()).collect();
|
|
62
|
+
let filtered_weights: Vec<f64> = filtered.iter().map(|&(_, &weight)| weight).collect();
|
|
63
|
+
|
|
64
|
+
let allocation_trace = Bar::new(filtered_ticker_symbols.clone(), filtered_weights.clone())
|
|
56
65
|
.name("Asset Allocation")
|
|
57
66
|
.x_axis("x2")
|
|
58
67
|
.y_axis("y2")
|
|
59
|
-
.text_array(
|
|
68
|
+
.text_array(filtered_weights.clone().iter().map(|w| format!("{:.2}%", w).to_string()).collect::<Vec<_>>());
|
|
60
69
|
|
|
61
70
|
|
|
62
71
|
let mut plot = Plot::new();
|
|
@@ -111,8 +120,7 @@ impl PortfolioCharts for Portfolio {
|
|
|
111
120
|
///
|
|
112
121
|
/// * `Plot` Plotly Chart struct
|
|
113
122
|
fn performance_chart(&self, height: usize, width: usize) -> Result<Plot, Box<dyn Error>> {
|
|
114
|
-
let dates =
|
|
115
|
-
&*self.performance_stats.end_date, 1);
|
|
123
|
+
let dates = self.performance_stats.dates_array.clone();
|
|
116
124
|
|
|
117
125
|
let returns = self.performance_stats.optimal_portfolio_returns.f64().unwrap().to_vec()
|
|
118
126
|
.iter().map(|x| x.unwrap()).collect::<Vec<f64>>();
|
|
@@ -198,17 +206,24 @@ impl PortfolioCharts for Portfolio {
|
|
|
198
206
|
fn asset_returns_chart(&self, height: usize, width: usize) -> Result<Plot, Box<dyn Error>> {
|
|
199
207
|
let symbols = self.performance_stats.ticker_symbols.clone();
|
|
200
208
|
let asset_returns = self.performance_stats.portfolio_returns.clone();
|
|
201
|
-
let dates =
|
|
209
|
+
let dates = self.performance_stats.dates_array.clone();
|
|
202
210
|
let mut plot = Plot::new();
|
|
203
211
|
|
|
204
212
|
for symbol in symbols {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
match asset_returns.column(&symbol) {
|
|
214
|
+
Ok(returns_series) => {
|
|
215
|
+
let returns = returns_series.f64().unwrap().to_vec()
|
|
216
|
+
.iter().map(|x| x.unwrap()).collect::<Vec<f64>>();
|
|
217
|
+
let cum_returns = cumulative_returns_list(returns.clone());
|
|
218
|
+
let cum_returns_trace = Scatter::new(dates.clone(), cum_returns.clone())
|
|
219
|
+
.name(format!("{}", symbol))
|
|
220
|
+
.mode(Mode::Lines);
|
|
221
|
+
plot.add_trace(cum_returns_trace);
|
|
222
|
+
}
|
|
223
|
+
Err(e) => {
|
|
224
|
+
eprintln!("Unable to fetch returns for {}: {}", symbol, e);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
212
227
|
}
|
|
213
228
|
|
|
214
229
|
let layout = Layout::new()
|
|
@@ -273,11 +288,11 @@ impl PortfolioCharts for Portfolio {
|
|
|
273
288
|
|
|
274
289
|
|
|
275
290
|
let trace = Table::new(
|
|
276
|
-
vec![
|
|
291
|
+
Header::new(vec![
|
|
277
292
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Performance Stats"),
|
|
278
293
|
format!("<span style=\"font-weight:bold; color:darkgreen;\">{}</span>", "Values"),
|
|
279
|
-
],
|
|
280
|
-
vec![fields, values],
|
|
294
|
+
]),
|
|
295
|
+
Cells::new(vec![fields, values]),
|
|
281
296
|
);
|
|
282
297
|
let mut plot = Plot::new();
|
|
283
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);
|