finalytics 0.4.0__tar.gz → 0.4.1__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.1}/PKG-INFO +1 -1
- {finalytics-0.4.0 → finalytics-0.4.1}/README.md +1 -1
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/Cargo.toml +4 -3
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/performance.rs +45 -10
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/charts/portfolio.rs +25 -9
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/data/ticker.rs +99 -63
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/lib.rs +25 -4
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/models/portfolio.rs +3 -3
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/models/ticker.rs +1 -1
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/utils/web_utils.rs +1 -2
- {finalytics-0.4.0 → finalytics-0.4.1}/python/Cargo.toml +1 -1
- {finalytics-0.4.0 → finalytics-0.4.1}/python/conf.py +9 -18
- finalytics-0.4.1/python/index.rst +198 -0
- finalytics-0.4.0/python/index.rst +0 -6
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/fundamentals.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/optimization.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/sentiment.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/statistics.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/stochastics.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/analytics/technicals.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/charts/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/charts/ticker.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/data/config.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/data/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/models/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/utils/chart_utils.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/utils/date_utils.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/finalytics/src/utils/mod.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/pyproject.toml +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/Cargo.lock +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/README.md +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/build.sh +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/src/ffi.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/src/lib.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/src/portfolio.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/src/ticker.rs +0 -0
- {finalytics-0.4.0 → finalytics-0.4.1}/python/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "finalytics"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
authors = ["Nnamdi Olisaeloka <nnamdiolisaeloka@gmail.com>"]
|
|
5
5
|
edition = "2021"
|
|
6
6
|
homepage = "https://finalytics.rs/"
|
|
@@ -24,11 +24,11 @@ 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.33.2", default-features = false, features = ["lazy", "dtype-datetime", "rows"] }
|
|
27
|
+
polars = { version = "0.33.2", 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"
|
|
@@ -36,6 +36,7 @@ optimization = "0.2.0"
|
|
|
36
36
|
html-escape = "0.2.13"
|
|
37
37
|
plotly = { version = "0.8.4", package = "plotly-fork"}
|
|
38
38
|
once_cell = "1.19.0"
|
|
39
|
+
futures = "0.3.30"
|
|
39
40
|
|
|
40
41
|
[features]
|
|
41
42
|
kaleido = ["plotly/kaleido"]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use polars::prelude::*;
|
|
2
2
|
use std::error::Error;
|
|
3
|
+
use futures::future::join_all;
|
|
3
4
|
|
|
4
5
|
use crate::data::config::Interval;
|
|
5
6
|
use crate::models::ticker::{Ticker, TickerBuilder};
|
|
@@ -131,21 +132,55 @@ impl PortfolioPerformanceStats {
|
|
|
131
132
|
max_iterations: u64,
|
|
132
133
|
objective_function: ObjectiveFunction
|
|
133
134
|
) -> Result<PortfolioPerformanceStats, Box<dyn Error>> {
|
|
134
|
-
let mut
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
let mut futures = Vec::new();
|
|
136
|
+
|
|
137
|
+
for ticker_symbol in ticker_symbols.clone().into_iter() {
|
|
138
|
+
let ticker = TickerBuilder::new().ticker(&ticker_symbol)
|
|
137
139
|
.start_date(start_date)
|
|
138
140
|
.end_date(end_date)
|
|
139
141
|
.interval(interval)
|
|
140
142
|
.confidence_level(confidence_level)
|
|
141
143
|
.risk_free_rate(risk_free_rate)
|
|
142
144
|
.build();
|
|
143
|
-
|
|
144
|
-
let
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
|
|
146
|
+
let fut = tokio::task::spawn(async move {
|
|
147
|
+
match ticker.roc(1).await {
|
|
148
|
+
Ok(security_df) => {
|
|
149
|
+
match DataFrame::new(vec![
|
|
150
|
+
security_df.column("timestamp").map_err(|e| e.to_string())?.clone(),
|
|
151
|
+
security_df.column("roc-1").map_err(|e| e.to_string())?.clone().with_name(&ticker_symbol)
|
|
152
|
+
]) {
|
|
153
|
+
Ok(security_returns_df) => Ok(security_returns_df),
|
|
154
|
+
Err(e) => {
|
|
155
|
+
eprintln!("Error creating DataFrame for {}: {}", ticker_symbol, e);
|
|
156
|
+
Err(format!("Error creating DataFrame for {}: {}", ticker_symbol, e))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
Err(e) => {
|
|
161
|
+
eprintln!("Error Fetching Price Data for {}: {}", ticker_symbol, e);
|
|
162
|
+
Err(format!("Error Fetching Price Data for {}: {}", ticker_symbol, e))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
futures.push(fut);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let results = join_all(futures).await;
|
|
171
|
+
let mut fetched_symbols: Vec<String> = Vec::new();
|
|
172
|
+
let mut dfs: Vec<DataFrame> = Vec::new();
|
|
173
|
+
|
|
174
|
+
for result in results {
|
|
175
|
+
match result {
|
|
176
|
+
Ok(Ok(df)) => {
|
|
177
|
+
let symbol = df.get_column_names()[1].to_string();
|
|
178
|
+
fetched_symbols.push(symbol);
|
|
179
|
+
dfs.push(df);
|
|
180
|
+
}
|
|
181
|
+
Ok(Err(_)) => continue,
|
|
182
|
+
Err(e) => eprintln!("Error in task: {}", e),
|
|
183
|
+
}
|
|
149
184
|
}
|
|
150
185
|
|
|
151
186
|
let mut portfolio_returns = dfs[0].clone();
|
|
@@ -193,7 +228,7 @@ impl PortfolioPerformanceStats {
|
|
|
193
228
|
let _ = portfolio_returns.drop_in_place("timestamp")?;
|
|
194
229
|
|
|
195
230
|
Ok(PortfolioPerformanceStats {
|
|
196
|
-
ticker_symbols:
|
|
231
|
+
ticker_symbols: fetched_symbols.clone(),
|
|
197
232
|
benchmark_symbol: benchmark_symbol.to_string(),
|
|
198
233
|
start_date: start_date.to_string(),
|
|
199
234
|
end_date: end_date.to_string(),
|
|
@@ -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();
|
|
@@ -202,13 +211,20 @@ impl PortfolioCharts for Portfolio {
|
|
|
202
211
|
let mut plot = Plot::new();
|
|
203
212
|
|
|
204
213
|
for symbol in symbols {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
match asset_returns.column(&symbol) {
|
|
215
|
+
Ok(returns_series) => {
|
|
216
|
+
let returns = returns_series.f64().unwrap().to_vec()
|
|
217
|
+
.iter().map(|x| x.unwrap()).collect::<Vec<f64>>();
|
|
218
|
+
let cum_returns = cumulative_returns_list(returns.clone());
|
|
219
|
+
let cum_returns_trace = Scatter::new(dates.clone(), cum_returns.clone())
|
|
220
|
+
.name(format!("{}", symbol))
|
|
221
|
+
.mode(Mode::Lines);
|
|
222
|
+
plot.add_trace(cum_returns_trace);
|
|
223
|
+
}
|
|
224
|
+
Err(e) => {
|
|
225
|
+
eprintln!("Unable to fetch returns for {}: {}", symbol, e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
212
228
|
}
|
|
213
229
|
|
|
214
230
|
let layout = Layout::new()
|
|
@@ -24,7 +24,8 @@ impl TickerData for Ticker {
|
|
|
24
24
|
let response = REQUEST_CLIENT.get(&url).send().await?;
|
|
25
25
|
let result= response.json::<Value>().await?;
|
|
26
26
|
let value = &result["optionChain"]["result"][0]["quote"].to_string();
|
|
27
|
-
let quote: Quote = serde_json::from_value(value.parse()?)
|
|
27
|
+
let quote: Quote = serde_json::from_value(value.parse()?)
|
|
28
|
+
.map_err(|e| format!("Failed to deserialize into Quote: {}", e))?;
|
|
28
29
|
Ok(quote)
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -34,7 +35,8 @@ impl TickerData for Ticker {
|
|
|
34
35
|
let response = REQUEST_CLIENT.get(&url).send().await?;
|
|
35
36
|
let result = response.json::<Value>().await?;
|
|
36
37
|
let value = &result["optionChain"]["result"][0]["quote"].to_string();
|
|
37
|
-
let stats: TickerSummaryStats = serde_json::from_value(value.parse()?)
|
|
38
|
+
let stats: TickerSummaryStats = serde_json::from_value(value.parse()?)
|
|
39
|
+
.map_err(|e| format!("Failed to deserialize into TickerSummaryStats: {}", e))?;
|
|
38
40
|
Ok(stats)
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -53,7 +55,7 @@ impl TickerData for Ticker {
|
|
|
53
55
|
let value = &result["chart"]["result"][0];
|
|
54
56
|
let timestamp = &value["timestamp"]
|
|
55
57
|
.as_array()
|
|
56
|
-
.ok_or(format!("timestamp array not found: {
|
|
58
|
+
.ok_or(format!("timestamp array not found for {}: {}", self.ticker, result))?
|
|
57
59
|
.iter()
|
|
58
60
|
.map(|ts| {
|
|
59
61
|
let timestamp = ts.as_i64().unwrap();
|
|
@@ -76,35 +78,35 @@ impl TickerData for Ticker {
|
|
|
76
78
|
|
|
77
79
|
let open = indicators["open"]
|
|
78
80
|
.as_array()
|
|
79
|
-
.ok_or("open array not found")?
|
|
81
|
+
.ok_or(format!("open array not found for {}: {}", self.ticker, result))?
|
|
80
82
|
.iter()
|
|
81
83
|
.map(|o| o.as_f64().unwrap_or(0.0))
|
|
82
84
|
.collect::<Vec<f64>>();
|
|
83
85
|
|
|
84
86
|
let high = indicators["high"]
|
|
85
87
|
.as_array()
|
|
86
|
-
.ok_or("high array not found")?
|
|
88
|
+
.ok_or(format!("high array not found for {}: {}", self.ticker, result))?
|
|
87
89
|
.iter()
|
|
88
90
|
.map(|h| h.as_f64().unwrap_or(0.0))
|
|
89
91
|
.collect::<Vec<f64>>();
|
|
90
92
|
|
|
91
93
|
let low = indicators["low"]
|
|
92
94
|
.as_array()
|
|
93
|
-
.ok_or("low array not found")?
|
|
95
|
+
.ok_or(format!("low array not found for {}: {}", self.ticker, result))?
|
|
94
96
|
.iter()
|
|
95
97
|
.map(|l| l.as_f64().unwrap_or(0.0))
|
|
96
98
|
.collect::<Vec<f64>>();
|
|
97
99
|
|
|
98
100
|
let close = indicators["close"]
|
|
99
101
|
.as_array()
|
|
100
|
-
.ok_or("close array not found")?
|
|
102
|
+
.ok_or(format!("close array not found for {}: {}", self.ticker, result))?
|
|
101
103
|
.iter()
|
|
102
104
|
.map(|c| c.as_f64().unwrap_or(0.0))
|
|
103
105
|
.collect::<Vec<f64>>();
|
|
104
106
|
|
|
105
107
|
let volume = indicators["volume"]
|
|
106
108
|
.as_array()
|
|
107
|
-
.ok_or("volume array not found")?
|
|
109
|
+
.ok_or(format!("volume array not found for {}: {}", self.ticker, result))?
|
|
108
110
|
.iter()
|
|
109
111
|
.map(|v| v.as_f64().unwrap_or(0.0))
|
|
110
112
|
.collect::<Vec<f64>>();
|
|
@@ -114,7 +116,7 @@ impl TickerData for Ticker {
|
|
|
114
116
|
.unwrap_or_else(|| {
|
|
115
117
|
indicators["close"]
|
|
116
118
|
.as_array()
|
|
117
|
-
.ok_or("close array not found")
|
|
119
|
+
.ok_or(format!("close array not found for {}: {}", self.ticker, result))
|
|
118
120
|
.unwrap_or_else(|_| {
|
|
119
121
|
indicators["close"]
|
|
120
122
|
.as_array()
|
|
@@ -150,71 +152,103 @@ impl TickerData for Ticker {
|
|
|
150
152
|
Ok(df)
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
|
|
153
156
|
/// Returns Ticker Option Chain Data from Yahoo Finance for all available expirations
|
|
154
157
|
async fn get_options(&self) -> Result<Options, Box<dyn Error>> {
|
|
155
158
|
let url = format!("https://query2.finance.yahoo.com/v6/finance/options/{}", self.ticker);
|
|
156
159
|
let response = REQUEST_CLIENT.get(&url).send().await?;
|
|
157
160
|
let result = response.json::<Value>().await?;
|
|
158
|
-
|
|
159
|
-
let
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
let
|
|
164
|
-
.
|
|
165
|
-
|
|
161
|
+
|
|
162
|
+
let ticker_price = result["optionChain"]["result"][0]["quote"]["regularMarketPrice"]
|
|
163
|
+
.as_f64()
|
|
164
|
+
.ok_or("Failed to parse regularMarketPrice as f64")?;
|
|
165
|
+
|
|
166
|
+
let expiration_dates = result["optionChain"]["result"][0]["expirationDates"]
|
|
167
|
+
.as_array()
|
|
168
|
+
.ok_or("Failed to parse expirationDates as array")?;
|
|
169
|
+
|
|
170
|
+
let strike_values = result["optionChain"]["result"][0]["strikes"]
|
|
171
|
+
.as_array()
|
|
172
|
+
.ok_or("Failed to parse strikes as array")?;
|
|
173
|
+
|
|
174
|
+
let strikes = strike_values
|
|
175
|
+
.iter()
|
|
176
|
+
.map(|x| x.as_f64().ok_or("Failed to parse strike as f64"))
|
|
177
|
+
.collect::<Result<Vec<f64>, _>>()?;
|
|
178
|
+
|
|
179
|
+
let timestamps = expiration_dates
|
|
180
|
+
.iter()
|
|
181
|
+
.map(|x| x.as_i64().ok_or("Failed to parse expiration date as i64"))
|
|
182
|
+
.collect::<Result<Vec<i64>, _>>()?;
|
|
183
|
+
|
|
184
|
+
let ttms = timestamps
|
|
185
|
+
.iter()
|
|
186
|
+
.map(|x| time_to_maturity(*x))
|
|
166
187
|
.collect::<Vec<f64>>();
|
|
167
|
-
|
|
188
|
+
|
|
189
|
+
let expiration_dates = timestamps
|
|
190
|
+
.iter()
|
|
191
|
+
.map(|x| to_date(*x))
|
|
168
192
|
.collect::<Vec<String>>();
|
|
193
|
+
|
|
169
194
|
let mut options_chain = DataFrame::default();
|
|
170
|
-
|
|
171
|
-
|
|
195
|
+
|
|
196
|
+
for t in ×tamps {
|
|
197
|
+
let url = format!(
|
|
198
|
+
"https://query2.finance.yahoo.com/v6/finance/options/{}?date={}",
|
|
199
|
+
self.ticker, t
|
|
200
|
+
);
|
|
172
201
|
let response = REQUEST_CLIENT.get(&url).send().await?;
|
|
173
202
|
let result = response.json::<Value>().await?;
|
|
174
203
|
let expiration = to_date(*t);
|
|
175
204
|
let ttm = time_to_maturity(*t);
|
|
205
|
+
|
|
176
206
|
let calls = &result["optionChain"]["result"][0]["options"][0]["calls"];
|
|
177
|
-
let calls_vec: Vec<OptionContract> = serde_json::from_value(calls.
|
|
178
|
-
.
|
|
207
|
+
let calls_vec: Vec<OptionContract> = serde_json::from_value(calls.clone())
|
|
208
|
+
.map_err(|e| format!("Failed to deserialize calls into OptionContract: {}", e))?;
|
|
209
|
+
|
|
179
210
|
let calls_df = df!(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
211
|
+
"expiration" => calls_vec.iter().map(|_| &*expiration).collect::<Vec<&str>>(),
|
|
212
|
+
"ttm" => calls_vec.iter().map(|_| ttm).collect::<Vec<f64>>(),
|
|
213
|
+
"type" => calls_vec.iter().map(|_| "call").collect::<Vec<&str>>(),
|
|
214
|
+
"contractSymbol" => calls_vec.iter().map(|x| x.contractSymbol.as_str()).collect::<Vec<&str>>(),
|
|
215
|
+
"strike" => calls_vec.iter().map(|x| x.strike).collect::<Vec<f64>>(),
|
|
216
|
+
"currency" => calls_vec.iter().map(|x| x.currency.as_str()).collect::<Vec<&str>>(),
|
|
217
|
+
"lastPrice" => calls_vec.iter().map(|x| x.lastPrice).collect::<Vec<f64>>(),
|
|
218
|
+
"change" => calls_vec.iter().map(|x| x.change).collect::<Vec<f64>>(),
|
|
219
|
+
"percentChange" => calls_vec.iter().map(|x| x.percentChange).collect::<Vec<f64>>(),
|
|
220
|
+
"openInterest" => calls_vec.iter().map(|x| x.openInterest).collect::<Vec<f64>>(),
|
|
221
|
+
"bid" => calls_vec.iter().map(|x| x.bid).collect::<Vec<f64>>(),
|
|
222
|
+
"ask" => calls_vec.iter().map(|x| x.ask).collect::<Vec<f64>>(),
|
|
223
|
+
"contractSize" => calls_vec.iter().map(|x| x.contractSize.as_str()).collect::<Vec<&str>>(),
|
|
224
|
+
"lastTradeDate" => calls_vec.iter().map(|x| DateTime::from_timestamp(x.lastTradeDate, 0).unwrap().naive_local()).collect::<Vec<NaiveDateTime>>(),
|
|
225
|
+
"impliedVolatility" => calls_vec.iter().map(|x| x.impliedVolatility).collect::<Vec<f64>>(),
|
|
226
|
+
"inTheMoney" => calls_vec.iter().map(|x| x.inTheMoney).collect::<Vec<bool>>(),
|
|
227
|
+
)?;
|
|
228
|
+
|
|
197
229
|
let puts = &result["optionChain"]["result"][0]["options"][0]["puts"];
|
|
198
|
-
let puts_vec: Vec<OptionContract> = serde_json::from_value(puts.
|
|
199
|
-
.
|
|
230
|
+
let puts_vec: Vec<OptionContract> = serde_json::from_value(puts.clone())
|
|
231
|
+
.map_err(|e| format!("Failed to deserialize puts into OptionContract: {}", e))?;
|
|
232
|
+
|
|
200
233
|
let puts_df = df!(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
234
|
+
"expiration" => puts_vec.iter().map(|_| &*expiration).collect::<Vec<&str>>(),
|
|
235
|
+
"ttm" => puts_vec.iter().map(|_| ttm).collect::<Vec<f64>>(),
|
|
236
|
+
"type" => puts_vec.iter().map(|_| "put").collect::<Vec<&str>>(),
|
|
237
|
+
"contractSymbol" => puts_vec.iter().map(|x| x.contractSymbol.as_str()).collect::<Vec<&str>>(),
|
|
238
|
+
"strike" => puts_vec.iter().map(|x| x.strike).collect::<Vec<f64>>(),
|
|
239
|
+
"currency" => puts_vec.iter().map(|x| x.currency.as_str()).collect::<Vec<&str>>(),
|
|
240
|
+
"lastPrice" => puts_vec.iter().map(|x| x.lastPrice).collect::<Vec<f64>>(),
|
|
241
|
+
"change" => puts_vec.iter().map(|x| x.change).collect::<Vec<f64>>(),
|
|
242
|
+
"percentChange" => puts_vec.iter().map(|x| x.percentChange).collect::<Vec<f64>>(),
|
|
243
|
+
"openInterest" => puts_vec.iter().map(|x| x.openInterest).collect::<Vec<f64>>(),
|
|
244
|
+
"bid" => puts_vec.iter().map(|x| x.bid).collect::<Vec<f64>>(),
|
|
245
|
+
"ask" => puts_vec.iter().map(|x| x.ask).collect::<Vec<f64>>(),
|
|
246
|
+
"contractSize" => puts_vec.iter().map(|x| x.contractSize.as_str()).collect::<Vec<&str>>(),
|
|
247
|
+
"lastTradeDate" => puts_vec.iter().map(|x| DateTime::from_timestamp(x.lastTradeDate, 0).unwrap().naive_local()).collect::<Vec<NaiveDateTime>>(),
|
|
248
|
+
"impliedVolatility" => puts_vec.iter().map(|x| x.impliedVolatility).collect::<Vec<f64>>(),
|
|
249
|
+
"inTheMoney" => puts_vec.iter().map(|x| x.inTheMoney).collect::<Vec<bool>>(),
|
|
250
|
+
)?;
|
|
251
|
+
|
|
218
252
|
let df = calls_df.vstack(&puts_df)?;
|
|
219
253
|
options_chain.vstack_mut(&df)?;
|
|
220
254
|
}
|
|
@@ -224,10 +258,11 @@ impl TickerData for Ticker {
|
|
|
224
258
|
expiration_dates,
|
|
225
259
|
ttms,
|
|
226
260
|
strikes,
|
|
227
|
-
chain: options_chain
|
|
261
|
+
chain: options_chain,
|
|
228
262
|
})
|
|
229
263
|
}
|
|
230
264
|
|
|
265
|
+
|
|
231
266
|
/// Returns Ticker Fundamental Data from Yahoo Finance for a given statement type and frequency
|
|
232
267
|
///
|
|
233
268
|
/// # Arguments
|
|
@@ -257,7 +292,8 @@ impl TickerData for Ticker {
|
|
|
257
292
|
timeseries/{symbol}?symbol={symbol}&type={_type}&period1={period1}&period2={period2}");
|
|
258
293
|
let response = REQUEST_CLIENT.get(&url).send().await?;
|
|
259
294
|
let result = response.json::<Value>().await?;
|
|
260
|
-
let data: FundamentalsResponse = serde_json::from_value(result)
|
|
295
|
+
let data: FundamentalsResponse = serde_json::from_value(result)
|
|
296
|
+
.map_err(|e| format!("Failed to deserialize into FundamentalsResponse: {}", e))?;
|
|
261
297
|
let mut columns: Vec<Series> = vec![];
|
|
262
298
|
let mut temp_items: HashMap<String, Value> = HashMap::new();
|
|
263
299
|
let mut init = 0;
|
|
@@ -266,7 +302,7 @@ impl TickerData for Ticker {
|
|
|
266
302
|
for (key, value) in item {
|
|
267
303
|
if _type_clone.contains(&key.as_str()){
|
|
268
304
|
let items: Vec<Object> = serde_json::from_value(value.to_string().parse()?)
|
|
269
|
-
.
|
|
305
|
+
.map_err(|e| format!("Failed to deserialize into Object: {}", e))?;
|
|
270
306
|
let date_vec = items.iter().map(|x| x.asOfDate.clone()).collect::<Vec<String>>();
|
|
271
307
|
if date_vec.len() < 4 {
|
|
272
308
|
temp_items.insert(key.clone(), value.clone());
|
|
@@ -309,7 +345,7 @@ impl TickerData for Ticker {
|
|
|
309
345
|
if temp_items.len() > 0 {
|
|
310
346
|
for (key, value) in temp_items {
|
|
311
347
|
let items: Vec<Object> = serde_json::from_value(value.to_string().parse()?)
|
|
312
|
-
.
|
|
348
|
+
.map_err(|e| format!("Failed to deserialize into Object: {}", e))?;
|
|
313
349
|
let mut vars_vec: Vec<f64> = vec![];
|
|
314
350
|
for d in columns[0].iter(){
|
|
315
351
|
let mut found = false;
|
|
@@ -328,7 +364,7 @@ impl TickerData for Ticker {
|
|
|
328
364
|
columns.push(vars_series);
|
|
329
365
|
}
|
|
330
366
|
}
|
|
331
|
-
let df = DataFrame::new(columns)
|
|
367
|
+
let df = DataFrame::new(columns)?;
|
|
332
368
|
Ok(df)
|
|
333
369
|
}
|
|
334
370
|
|
|
@@ -42,17 +42,38 @@ pub mod prelude {
|
|
|
42
42
|
mod tests {
|
|
43
43
|
use crate::prelude::*;
|
|
44
44
|
#[tokio::test]
|
|
45
|
-
async fn
|
|
45
|
+
async fn ticker_performance() {
|
|
46
46
|
let ticker = TickerBuilder::new().ticker("AAPL")
|
|
47
47
|
.start_date("2023-01-01")
|
|
48
|
-
.end_date("2023-
|
|
48
|
+
.end_date("2023-12-31")
|
|
49
49
|
.interval(Interval::OneDay)
|
|
50
50
|
.benchmark_symbol("^GSPC")
|
|
51
51
|
.confidence_level(0.95)
|
|
52
52
|
.risk_free_rate(0.02)
|
|
53
53
|
.build();
|
|
54
54
|
|
|
55
|
-
let
|
|
56
|
-
|
|
55
|
+
let performance = ticker.performance_stats().await.unwrap();
|
|
56
|
+
|
|
57
|
+
dbg!(performance);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[tokio::test]
|
|
61
|
+
async fn portfolio_performance() {
|
|
62
|
+
let ticker_symbols = Vec::from(["AAPL", "BRK-A", "NVDA", "MSFT", "BTC-USD"]);
|
|
63
|
+
let portfolio = PortfolioBuilder::new().ticker_symbols(ticker_symbols)
|
|
64
|
+
.benchmark_symbol("^GSPC")
|
|
65
|
+
.start_date("2023-01-01")
|
|
66
|
+
.end_date("2023-12-31")
|
|
67
|
+
.interval(Interval::OneDay)
|
|
68
|
+
.confidence_level(0.95)
|
|
69
|
+
.risk_free_rate(0.02)
|
|
70
|
+
.max_iterations(1000)
|
|
71
|
+
.objective_function(ObjectiveFunction::MaxSharpe)
|
|
72
|
+
.build().await.unwrap();
|
|
73
|
+
|
|
74
|
+
let performance = portfolio.performance_stats;
|
|
75
|
+
|
|
76
|
+
dbg!(performance);
|
|
57
77
|
}
|
|
78
|
+
|
|
58
79
|
}
|
|
@@ -105,11 +105,11 @@ impl PortfolioBuilder {
|
|
|
105
105
|
/// #[tokio::main]
|
|
106
106
|
/// async fn main() -> Result<(), Box<dyn Error>> {
|
|
107
107
|
/// // Construct the Portfolio Object
|
|
108
|
-
/// let ticker_symbols = Vec::from(["NVDA", "BRK-A", "AAPL", "
|
|
108
|
+
/// let ticker_symbols = Vec::from(["NVDA", "BRK-A", "AAPL", "MSFT", "BTC-USD"]);
|
|
109
109
|
/// let portfolio = PortfolioBuilder::new().ticker_symbols(ticker_symbols)
|
|
110
110
|
/// .benchmark_symbol("^GSPC")
|
|
111
|
-
/// .start_date("
|
|
112
|
-
/// .end_date("2023-
|
|
111
|
+
/// .start_date("2023-01-01")
|
|
112
|
+
/// .end_date("2023-12-31")
|
|
113
113
|
/// .interval(Interval::OneDay)
|
|
114
114
|
/// .confidence_level(0.95)
|
|
115
115
|
/// .risk_free_rate(0.02)
|
|
@@ -144,7 +144,7 @@ impl TickerBuilder {
|
|
|
144
144
|
/// // Instantiate the Ticker Object
|
|
145
145
|
/// let ticker = TickerBuilder::new().ticker("AAPL")
|
|
146
146
|
/// .start_date("2023-01-01")
|
|
147
|
-
/// .end_date("2023-
|
|
147
|
+
/// .end_date("2023-12-31")
|
|
148
148
|
/// .interval(Interval::OneDay)
|
|
149
149
|
/// .benchmark_symbol("^GSPC")
|
|
150
150
|
/// .confidence_level(0.95)
|
|
@@ -30,7 +30,6 @@ pub struct Article {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
pub async fn scrape_text(url: &str, title: &str) -> Result<Article, Box<dyn Error>> {
|
|
33
|
-
let client = Client::new();
|
|
34
33
|
let response = REQUEST_CLIENT.get(url).send().await?;
|
|
35
34
|
let body = response.text().await?;
|
|
36
35
|
|
|
@@ -43,7 +42,7 @@ pub async fn scrape_text(url: &str, title: &str) -> Result<Article, Box<dyn Erro
|
|
|
43
42
|
|
|
44
43
|
for node in document.find(Name("a")) {
|
|
45
44
|
link.push_str(&node.text());
|
|
46
|
-
let response =
|
|
45
|
+
let response = REQUEST_CLIENT.get(&node.text()).send().await?;
|
|
47
46
|
let body = response.text().await?;
|
|
48
47
|
let document = Document::from_read(body.as_bytes()).unwrap();
|
|
49
48
|
for node in document.find(Name("p")) {
|
|
@@ -6,32 +6,23 @@
|
|
|
6
6
|
# -- Project information -----------------------------------------------------
|
|
7
7
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
project = 'Finalytics'
|
|
11
10
|
copyright = '2023, Nnamdi Olisaeloka'
|
|
12
11
|
author = 'Nnamdi Olisaeloka'
|
|
13
|
-
release = '0.
|
|
14
|
-
|
|
12
|
+
release = '0.4.0'
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'sphinx.ext.autosummary',
|
|
19
|
-
'sphinx.ext.intersphinx',
|
|
20
|
-
'sphinx.ext.todo',
|
|
21
|
-
'sphinx.ext.inheritance_diagram',
|
|
22
|
-
'sphinx.ext.autosectionlabel',
|
|
23
|
-
'sphinx.ext.napoleon',
|
|
24
|
-
'sphinx_rtd_theme',
|
|
25
|
-
]
|
|
14
|
+
# -- General configuration ---------------------------------------------------
|
|
15
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
autosummary_imported_members = True
|
|
17
|
+
extensions = []
|
|
29
18
|
|
|
30
19
|
templates_path = ['_templates']
|
|
31
|
-
exclude_patterns = []
|
|
20
|
+
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
21
|
+
|
|
22
|
+
|
|
32
23
|
|
|
33
24
|
# -- Options for HTML output -------------------------------------------------
|
|
34
25
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
|
35
26
|
|
|
36
|
-
html_theme = '
|
|
37
|
-
html_static_path = ['_static']
|
|
27
|
+
html_theme = 'alabaster'
|
|
28
|
+
html_static_path = ['_static']
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Welcome to Finalytics Documentation
|
|
2
|
+
====================================
|
|
3
|
+
|
|
4
|
+
Ticker Module
|
|
5
|
+
-------------
|
|
6
|
+
|
|
7
|
+
.. py:class:: Ticker
|
|
8
|
+
|
|
9
|
+
A class representing a Ticker object.
|
|
10
|
+
|
|
11
|
+
.. py:method:: __init__(symbol: str, start_date: Optional[str], end_date: Optional[str], interval: Optional[str], benchmark_symbol: Optional[str], confidence_level: Optional[float], risk_free_rate: Optional[float]) -> Ticker
|
|
12
|
+
|
|
13
|
+
Create a new Ticker object.
|
|
14
|
+
|
|
15
|
+
:param symbol: The ticker symbol of the asset.
|
|
16
|
+
:type symbol: str
|
|
17
|
+
:param start_date: Optional start date for historical data, defaults to None.
|
|
18
|
+
:type start_date: Optional[str]
|
|
19
|
+
:param end_date: Optional end date for historical data, defaults to None.
|
|
20
|
+
:type end_date: Optional[str]
|
|
21
|
+
:param interval: Optional data interval (2m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo, 3mo), defaults to None.
|
|
22
|
+
:type interval: Optional[str]
|
|
23
|
+
:param benchmark_symbol: Optional benchmark symbol, defaults to None.
|
|
24
|
+
:type benchmark_symbol: Optional[str]
|
|
25
|
+
:param confidence_level: Optional confidence level for statistics, defaults to None.
|
|
26
|
+
:type confidence_level: Optional[float]
|
|
27
|
+
:param risk_free_rate: Optional risk-free rate for calculations, defaults to None.
|
|
28
|
+
:type risk_free_rate: Optional[float]
|
|
29
|
+
:return: A Ticker object.
|
|
30
|
+
:rtype: Ticker
|
|
31
|
+
|
|
32
|
+
.. py:method:: get_quote() -> dict
|
|
33
|
+
|
|
34
|
+
Get the current ticker quote stats.
|
|
35
|
+
|
|
36
|
+
:return: Dictionary containing current ticker quote stats.
|
|
37
|
+
:rtype: dict
|
|
38
|
+
|
|
39
|
+
.. py:method:: get_summary_stats() -> dict
|
|
40
|
+
|
|
41
|
+
Get summary technical and fundamental statistics for the ticker.
|
|
42
|
+
|
|
43
|
+
:return: Dictionary containing summary statistics.
|
|
44
|
+
:rtype: dict
|
|
45
|
+
|
|
46
|
+
.. py:method:: get_price_history() -> DataFrame
|
|
47
|
+
|
|
48
|
+
Get the OHLCV data for the ticker for a given time period.
|
|
49
|
+
|
|
50
|
+
:return: Polars DataFrame containing OHLCV data.
|
|
51
|
+
:rtype: DataFrame
|
|
52
|
+
|
|
53
|
+
.. py:method:: get_options_chain() -> DataFrame
|
|
54
|
+
|
|
55
|
+
Get the options chain for the ticker.
|
|
56
|
+
|
|
57
|
+
:return: Polars DataFrame containing the options chain.
|
|
58
|
+
:rtype: DataFrame
|
|
59
|
+
|
|
60
|
+
.. py:method:: get_news(compute_sentiment: bool) -> dict
|
|
61
|
+
|
|
62
|
+
Get the latest news for the given ticker.
|
|
63
|
+
|
|
64
|
+
:param compute_sentiment: Whether to compute sentiment of news articles.
|
|
65
|
+
:type compute_sentiment: bool
|
|
66
|
+
:return: Dictionary containing news articles and sentiment results if requested.
|
|
67
|
+
:rtype: dict
|
|
68
|
+
|
|
69
|
+
.. py:method:: get_income_statement() -> DataFrame
|
|
70
|
+
|
|
71
|
+
Get the Income Statement for the ticker.
|
|
72
|
+
|
|
73
|
+
:return: Polars DataFrame containing the Income Statement.
|
|
74
|
+
:rtype: DataFrame
|
|
75
|
+
|
|
76
|
+
.. py:method:: get_balance_sheet() -> DataFrame
|
|
77
|
+
|
|
78
|
+
Get the Balance Sheet for the ticker.
|
|
79
|
+
|
|
80
|
+
:return: Polars DataFrame containing the Balance Sheet.
|
|
81
|
+
:rtype: DataFrame
|
|
82
|
+
|
|
83
|
+
.. py:method:: get_cashflow_statement() -> DataFrame
|
|
84
|
+
|
|
85
|
+
Get the Cashflow Statement for the ticker.
|
|
86
|
+
|
|
87
|
+
:return: Polars DataFrame containing the Cashflow Statement.
|
|
88
|
+
:rtype: DataFrame
|
|
89
|
+
|
|
90
|
+
.. py:method:: get_financial_ratios() -> DataFrame
|
|
91
|
+
|
|
92
|
+
Get the Financial Ratios for the ticker.
|
|
93
|
+
|
|
94
|
+
:return: Polars DataFrame containing the Financial Ratios.
|
|
95
|
+
:rtype: DataFrame
|
|
96
|
+
|
|
97
|
+
.. py:method:: volatility_surface() -> DataFrame
|
|
98
|
+
|
|
99
|
+
Get the implied volatility surface for the ticker options chain.
|
|
100
|
+
|
|
101
|
+
:return: Polars DataFrame containing the implied volatility surface.
|
|
102
|
+
:rtype: DataFrame
|
|
103
|
+
|
|
104
|
+
.. py:method:: performance_stats() -> dict
|
|
105
|
+
|
|
106
|
+
Compute the performance statistics for the ticker.
|
|
107
|
+
|
|
108
|
+
:return: Dictionary containing performance statistics.
|
|
109
|
+
:rtype: dict
|
|
110
|
+
|
|
111
|
+
.. py:method:: performance_chart(height: Optional[int], width: Optional[int]) -> Plot
|
|
112
|
+
|
|
113
|
+
Display the performance chart for the ticker.
|
|
114
|
+
|
|
115
|
+
:param height: Optional height of the plot in pixels, defaults to None.
|
|
116
|
+
:type height: Optional[int]
|
|
117
|
+
:param width: Optional width of the plot in pixels, defaults to None.
|
|
118
|
+
:type width: Optional[int]
|
|
119
|
+
:return: Plot object containing the performance chart.
|
|
120
|
+
:rtype: Plot
|
|
121
|
+
|
|
122
|
+
.. py:method:: candlestick_chart(height: Optional[int], width: Optional[int]) -> Plot
|
|
123
|
+
|
|
124
|
+
Display the candlestick chart for the ticker.
|
|
125
|
+
|
|
126
|
+
:param height: Optional height of the plot in pixels, defaults to None.
|
|
127
|
+
:type height: Optional[int]
|
|
128
|
+
:param width: Optional width of the plot in pixels, defaults to None.
|
|
129
|
+
:type width: Optional[int]
|
|
130
|
+
:return: Plot object containing the candlestick chart.
|
|
131
|
+
:rtype: Plot
|
|
132
|
+
|
|
133
|
+
.. py:method:: options_chart(chart_type: str, height: Optional[int], width: Optional[int]) -> Plot
|
|
134
|
+
|
|
135
|
+
Display the options volatility surface, smile and term structure charts for the ticker.
|
|
136
|
+
|
|
137
|
+
:param chart_type: Type of options chart (volatility_surface, smile, term_structure).
|
|
138
|
+
:type chart_type: str
|
|
139
|
+
:param height: Optional height of the plot in pixels, defaults to None.
|
|
140
|
+
:type height: Optional[int]
|
|
141
|
+
:param width: Optional width of the plot in pixels, defaults to None.
|
|
142
|
+
:type width: Optional[int]
|
|
143
|
+
:return: Plot object containing the options chart.
|
|
144
|
+
:rtype: Plot
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
Portfolio Module
|
|
148
|
+
----------------
|
|
149
|
+
|
|
150
|
+
.. py:class:: Portfolio
|
|
151
|
+
|
|
152
|
+
A class representing a Portfolio object.
|
|
153
|
+
|
|
154
|
+
.. py:method:: __init__(symbols: List[str], benchmark_symbol: str, start_date: str, end_date: str, interval: str, confidence_level: float, risk_free_rate: float, max_iterations: int, objective_function: str) -> Portfolio
|
|
155
|
+
|
|
156
|
+
Create a new Portfolio object.
|
|
157
|
+
|
|
158
|
+
:param symbols: List of ticker symbols for the assets in the portfolio.
|
|
159
|
+
:type symbols: List[str]
|
|
160
|
+
:param benchmark_symbol: The ticker symbol of the benchmark to compare against.
|
|
161
|
+
:type benchmark_symbol: str
|
|
162
|
+
:param start_date: The start date of the time period in the format YYYY-MM-DD.
|
|
163
|
+
:type start_date: str
|
|
164
|
+
:param end_date: The end date of the time period in the format YYYY-MM-DD.
|
|
165
|
+
:type end_date: str
|
|
166
|
+
:param interval: The interval of the data (2m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo, 3mo).
|
|
167
|
+
:type interval: str
|
|
168
|
+
:param confidence_level: The confidence level for the VaR and ES calculations.
|
|
169
|
+
:type confidence_level: float
|
|
170
|
+
:param risk_free_rate: The risk free rate to use in the calculations.
|
|
171
|
+
:type risk_free_rate: float
|
|
172
|
+
:param max_iterations: The maximum number of iterations to use in the optimization.
|
|
173
|
+
:type max_iterations: int
|
|
174
|
+
:param objective_function: The objective function to use in the optimization (max_sharpe, min_vol, max_return, min_var, min_cvar, min_drawdown).
|
|
175
|
+
:type objective_function: str
|
|
176
|
+
:return: A Portfolio object.
|
|
177
|
+
:rtype: Portfolio
|
|
178
|
+
|
|
179
|
+
.. py:method:: portfolio_chart(chart_type: str, height: Optional[int], width: Optional[int]) -> Plot
|
|
180
|
+
|
|
181
|
+
Display the Portfolio Optimization Chart.
|
|
182
|
+
|
|
183
|
+
:param chart_type: Type of portfolio chart (optimization, performance, asset_returns).
|
|
184
|
+
:type chart_type: str
|
|
185
|
+
:param height: Optional height of the plot in pixels, defaults to 800.
|
|
186
|
+
:type height: Optional[int]
|
|
187
|
+
:param width: Optional width of the plot in pixels, defaults to 1200.
|
|
188
|
+
:type width: Optional[int]
|
|
189
|
+
:return: Plot object containing the portfolio chart.
|
|
190
|
+
:rtype: Plot
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
Indices and tables
|
|
194
|
+
==================
|
|
195
|
+
|
|
196
|
+
* :ref:`genindex`
|
|
197
|
+
* :ref:`modindex`
|
|
198
|
+
* :ref:`search`
|
|
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
|