diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 7d1a2c8..c57b0df 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -1,18 +1,18 @@ name: CD -# on: -# workflow_dispatch: -# inputs: -# target: -# description: "Triplet to build (linux-x64-gnu / win32-x64-msvc)" -# required: false -# type: string - on: - pull_request: - push: - branches: - - master + workflow_dispatch: + inputs: + target: + description: "Triplet to build (linux-x64-gnu / win32-x64-msvc)" + required: false + type: string + +# on: +# pull_request: +# push: +# branches: +# - master # workflow_run: # workflows: ["CI"] diff --git a/BUILD.bazed b/BUILD.bazed index 5feee6e..19f3013 100644 --- a/BUILD.bazed +++ b/BUILD.bazed @@ -149,8 +149,9 @@ setup( "requests", "tqdm", "plotly", - "grpcio==1.70.0", - "protobuf==5.29.1", + "grpcio==1.74.0", + "protobuf==6.31.1", + "lightweight-charts==2.1", ], # platforms=["win_amd64"], ) diff --git a/Cargo.lock b/Cargo.lock index a9f9de8..80a8085 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1805,7 +1805,7 @@ version = "0.0.1-dev.1" [[package]] name = "qpace_core" -version = "0.2.6" +version = "0.2.8" dependencies = [ "cfg-if", "chrono", diff --git a/README.md b/README.md index cb87a59..ddb74c5 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ - CLI + upcoming UI +[![indicator plot](/static/indicator_banner.png)](/examples/python/main.py) + ## Quick Links - [Home](https://qpace.dev) diff --git a/cli/profile.ts b/cli/profile.ts index c497acf..cd09035 100644 --- a/cli/profile.ts +++ b/cli/profile.ts @@ -84,7 +84,7 @@ export class Profile { public async ping(): Promise { const client = await this.getClient(); try { - await client.me(); + await client.user.me(); } catch (e) { if (axios.isAxiosError(e) && e.response?.status == 403) { throw new CliError( diff --git a/cli/user.ts b/cli/user.ts index fc18c5a..81cb63d 100644 --- a/cli/user.ts +++ b/cli/user.ts @@ -20,7 +20,7 @@ export const getCommands = (): Command[] => { profile.data.apiKey = apiKey; const client = await profile.getClient(false); try { - const user = await client.me(); + const user = await client.user.me(); verbose && console.log( `${QPACE_BG_PREFIX}Logged in as ${chalk.yellowBright( diff --git a/core/Cargo.toml b/core/Cargo.toml index 712c871..4726ed5 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qpace_core" -version = "0.2.6" +version = "0.2.8" edition = "2021" [lib] diff --git a/core/backtest.rs b/core/backtest.rs index 1f6fef0..8bb2917 100644 --- a/core/backtest.rs +++ b/core/backtest.rs @@ -1,10 +1,12 @@ +use chrono::{DateTime, Utc}; + use crate::{ ctx::{Ctx, CtxSkip}, legacy::Float64Utils, metrics::{ - avg_losing_trade, avg_trade, avg_win_loss_ratio, avg_winning_trade, gross_loss_pct, - gross_profit_pct, net_profit_pct, profit_factor, sharpe_ratio_from_returns, - sortino_ratio_from_returns, win_rate, + annualization_factor, avg_losing_trade, avg_trade, avg_win_loss_ratio, avg_winning_trade, + expectancy, gross_loss_pct, gross_profit_pct, net_profit_pct, profit_factor, + sharpe_ratio_from_returns, sortino_ratio_from_returns, win_rate, }, orderbook::{ order_size_for_equity_pct, round_contracts, round_to_min_tick, validate_contracts, @@ -13,6 +15,7 @@ use crate::{ signal::{Signal, SignalKind}, stats::returns, sym::Sym, + timeframe::Timeframe, trade::{Trade, TradeError, TradeEvent}, utils::with_suffix, }; @@ -37,6 +40,8 @@ pub struct BacktestConfig { initial_capital: f64, process_orders_on_close: bool, debug: bool, + risk_free_rate: f64, + annualization_factor: f64, } impl Default for BacktestConfig { @@ -45,6 +50,8 @@ impl Default for BacktestConfig { initial_capital: 1000.0, process_orders_on_close: false, debug: false, + risk_free_rate: f64::NAN, + annualization_factor: f64::NAN, } } } @@ -55,6 +62,8 @@ impl BacktestConfig { initial_capital, process_orders_on_close, debug: false, + risk_free_rate: f64::NAN, + annualization_factor: f64::NAN, }; } @@ -87,6 +96,16 @@ impl BacktestConfig { pub fn set_debug(&mut self, debug: bool) { self.debug = debug; } + + #[inline] + pub fn set_risk_free_rate(&mut self, risk_free_rate: f64) { + self.risk_free_rate = risk_free_rate; + } + + #[inline] + pub fn set_annualization_factor(&mut self, annualization_factor: f64) { + self.annualization_factor = annualization_factor; + } } pub struct Backtest { @@ -117,12 +136,19 @@ pub struct Backtest { impl Backtest { #[inline] - pub fn new(ctx: Rc>, config: BacktestConfig) -> Self { + pub fn new(ctx: Rc>, mut config: BacktestConfig) -> Self { let sym = ctx.borrow().sym().clone(); assert!( !f64::is_nan(sym.min_qty()) && !f64::is_nan(sym.min_tick()), "Ctx Symbol is not suitable for backtesting, min_qty is NaN or min_tick is NaN" ); + if config.risk_free_rate.is_nan() { + config.risk_free_rate = 0.0; + } + if config.annualization_factor.is_nan() { + config.annualization_factor = + annualization_factor(ctx.borrow().ohlcv().timeframe(), sym.kind().periods()); + } let initial_capital = config.initial_capital; Self { ctx, @@ -273,13 +299,20 @@ impl Backtest { } #[inline] - pub fn sharpe_ratio(&self, rfr: f64) -> f64 { - sharpe_ratio_from_returns(&self.returns_list(), rfr) + pub fn sharpe_ratio(&self) -> f64 { + sharpe_ratio_from_returns(&self.returns_list(), self.config.risk_free_rate) + * self.config.annualization_factor } #[inline] - pub fn sortino_ratio(&self, rfr: f64) -> f64 { - sortino_ratio_from_returns(&self.returns_list(), rfr) + pub fn sortino_ratio(&self) -> f64 { + sortino_ratio_from_returns(&self.returns_list(), self.config.risk_free_rate) + * self.config.annualization_factor + } + + #[inline] + pub fn expectancy(&self) -> f64 { + expectancy(&self.pnl_list()) } #[inline] @@ -411,8 +444,18 @@ impl Backtest { return trade; } + // @TODO + // pub fn maybe_reset_equity_pct(&mut self) { + // if self.position_size() == 0.0 { + // self.prev_equity_pct = 0.0; + // } + // } + pub fn compute_equity_pct(&mut self, equity_pct: f64) -> Option { let ctx = self.ctx.borrow(); + // if self.bar_index() == 21454 || self.bar_index() == 21293 { + // println!("[{} -> compute_equity_pct]: equity(): {:?} | equity_pct: {:?} | prev_equity_pct: {:?} ", self.bar_index(), self.equity(), equity_pct, self.prev_equity_pct); + // } if self.equity() > 0.0 { if !equity_pct.compare(self.prev_equity_pct) { // if true { @@ -429,7 +472,7 @@ impl Backtest { let order_size = round_contracts(base_order_size, ctx.sym().min_qty(), ctx.sym().qty_scale()); - // if self.config.debug { + // if self.bar_index() == 21454 || self.bar_index() == 21293 { // println!("[{} -> compute_equity_pct]: equity_pct: {:?} | prev_equity_pct: {:?} | base_order_size: {:?} | order_size: {:?} | min_qty: {:?} | price_scale: {:?}", self.bar_index(), equity_pct, self.prev_equity_pct, base_order_size, order_size, ctx.sym().min_qty(), ctx.sym().price_scale()); // } @@ -656,10 +699,16 @@ impl Backtest { #[inline] pub fn signal(&mut self, signal: Signal) { + // if self.config.debug { + // println!("[{} ->raw signal]: {:?}", self.bar_index(), &signal); + // } let order: Option = match signal.kind() { SignalKind::EquityPct(pct) => self.compute_equity_pct(*pct), SignalKind::Size(size) => Some(OrderConfig::new(*size, None)), - SignalKind::CloseAll() => Some(OrderConfig::new(-self.position_size, None)), + SignalKind::CloseAll() => { + self.prev_equity_pct = 0.0; + Some(OrderConfig::new(-self.position_size, None)) + } _ => None, }; if self.config.debug { @@ -801,7 +850,6 @@ for i = 0 to array.size(trades) - 1 #[cfg(feature = "pretty_table")] pub fn print_table(&self) { - let rfr = 0.0; let sym = self.ctx.borrow().sym().clone(); let f_price = with_suffix(&format!(" {}", sym._currency())); let f_percent = with_suffix("%"); @@ -830,12 +878,12 @@ for i = 0 to array.size(trades) - 1 table.add_row(Row::from(vec![ Cell::new("Sharpe Ratio"), - Cell::new(format!("{:0.3}", self.sharpe_ratio(rfr))), + Cell::new(format!("{:0.3}", self.sharpe_ratio())), ])); table.add_row(Row::from(vec![ Cell::new("Sortino Ratio"), - Cell::new(format!("{:0.3}", self.sortino_ratio(rfr))), + Cell::new(format!("{:0.3}", self.sortino_ratio())), ])); table.add_row(Row::from(vec![ @@ -888,6 +936,11 @@ for i = 0 to array.size(trades) - 1 Cell::new(f_raw(self.avg_win_loss_ratio())), ])); + table.add_row(Row::from(vec![ + Cell::new("Expectancy"), + Cell::new(f_raw(self.expectancy())), + ])); + // Print the table println!("{}", table); } diff --git a/core/backtest_node.rs b/core/backtest_node.rs index 31ee679..a3ee28d 100644 --- a/core/backtest_node.rs +++ b/core/backtest_node.rs @@ -31,12 +31,16 @@ impl NodeBacktest { ctx: &NodeCtx, initial_capital: Option, process_orders_on_close: Option, + risk_free_rate: Option, + annualization_factor: Option, ) -> Self { let initial_capital = initial_capital.unwrap_or(1000.0); let process_orders_on_close = process_orders_on_close.unwrap_or(false); let mut config = BacktestConfig::default(); config.set_initial_capital(initial_capital); config.set_process_orders_on_close(process_orders_on_close); + config.set_risk_free_rate(risk_free_rate.unwrap_or(f64::NAN)); + config.set_annualization_factor(annualization_factor.unwrap_or(f64::NAN)); Self { inner: Rc::new(RefCell::new(Backtest::new(ctx.inner().clone(), config))), ctx: ctx.clone(), @@ -177,14 +181,20 @@ impl NodeBacktest { #[napi(js_name = "sharpeRatio")] #[inline] - pub fn node_sharpe_ratio(&self, rfr: f64) -> f64 { - self.inner.borrow().sharpe_ratio(rfr) + pub fn node_sharpe_ratio(&self) -> f64 { + self.inner.borrow().sharpe_ratio() } #[napi(js_name = "sortinoRatio")] #[inline] - pub fn node_sortino_ratio(&self, rfr: f64) -> f64 { - self.inner.borrow().sortino_ratio(rfr) + pub fn node_sortino_ratio(&self) -> f64 { + self.inner.borrow().sortino_ratio() + } + + #[napi(js_name = "expectancy")] + #[inline] + pub fn node_expectancy(&self) -> f64 { + self.inner.borrow().expectancy() } #[napi(js_name = winningTradesCount)] diff --git a/core/backtest_py.rs b/core/backtest_py.rs index 4722228..de2570c 100644 --- a/core/backtest_py.rs +++ b/core/backtest_py.rs @@ -29,7 +29,7 @@ impl PyBacktest { #[gen_stub_pymethods] #[pymethods] impl PyBacktest { - #[pyo3(signature = (ctx, initial_capital=1000.0, process_orders_on_close=false, debug=false))] + #[pyo3(signature = (ctx, initial_capital=1000.0, process_orders_on_close=false, debug=false, risk_free_rate=None, annualization_factor=None))] #[new] #[inline] pub fn py_new( @@ -37,11 +37,15 @@ impl PyBacktest { initial_capital: f64, process_orders_on_close: bool, debug: bool, + risk_free_rate: Option, + annualization_factor: Option, ) -> Self { let mut config = BacktestConfig::default(); config.set_initial_capital(initial_capital); config.set_process_orders_on_close(process_orders_on_close); config.set_debug(debug); + config.set_risk_free_rate(risk_free_rate.unwrap_or(f64::NAN)); + config.set_annualization_factor(annualization_factor.unwrap_or(f64::NAN)); Self { inner: Rc::new(RefCell::new(Backtest::new(ctx.inner().clone(), config))), ctx, @@ -183,14 +187,20 @@ impl PyBacktest { #[pyo3(name = "sharpe_ratio")] #[inline] - pub fn py_sharpe_ratio(&self, rfr: f64) -> f64 { - self.inner.borrow().sharpe_ratio(rfr) + pub fn py_sharpe_ratio(&self) -> f64 { + self.inner.borrow().sharpe_ratio() } #[pyo3(name = "sortino_ratio")] #[inline] - pub fn py_sortino_ratio(&self, rfr: f64) -> f64 { - self.inner.borrow().sortino_ratio(rfr) + pub fn py_sortino_ratio(&self) -> f64 { + self.inner.borrow().sortino_ratio() + } + + #[pyo3(name = "expectancy")] + #[inline] + pub fn py_expectancy(&self) -> f64 { + self.inner.borrow().expectancy() } #[getter(position_size)] @@ -302,6 +312,33 @@ impl PyBacktest { pub fn py_display(&self) { self.inner.borrow().display(None); } + + // pub fn create_hold( + // ctx: Rc>, + // config: BacktestConfig, + // range: (usize, usize), + // qty: f64, + // ) -> Backtest { + // let (entry, exit) = range; + + // let mut bt = Backtest::new(ctx.clone(), config); + + // for bar_index in ctx.borrow_mut().into_iter() { + // bt.on_bar_open(); + + // if bar_index == entry { + // let order = OrderConfig::new(qty, Some("hold_entry".to_string())); + // bt.order(order).unwrap(); + // } else if bar_index == exit { + // let order = OrderConfig::new(-qty, Some("hold_exit".to_string())); + // bt.order(order).unwrap(); + // } + + // bt.on_bar_close(); + // } + // return bt; + // } + // #[pyo3(name = "summary", signature = (risk_free_rate=0.0))] // #[inline] // pub fn py_summary(&self, risk_free_rate: f64) -> PyBacktestSummary { diff --git a/core/backtest_wasm.rs b/core/backtest_wasm.rs index 60da7a5..85ea247 100644 --- a/core/backtest_wasm.rs +++ b/core/backtest_wasm.rs @@ -42,12 +42,16 @@ impl WasmBacktest { ctx: WasmCtx, initial_capital: Option, process_orders_on_close: Option, + risk_free_rate: Option, + annualization_factor: Option, ) -> Self { let initial_capital = initial_capital.unwrap_or(1000.0); let process_orders_on_close = process_orders_on_close.unwrap_or(false); let mut config = BacktestConfig::default(); config.set_initial_capital(initial_capital); config.set_process_orders_on_close(process_orders_on_close); + config.set_risk_free_rate(risk_free_rate.unwrap_or(f64::NAN)); + config.set_annualization_factor(annualization_factor.unwrap_or(f64::NAN)); Self { inner: Rc::new(RefCell::new(Backtest::new(ctx.inner().clone(), config))), ctx, @@ -188,14 +192,20 @@ impl WasmBacktest { #[wasm_bindgen(js_name = "sharpeRatio")] #[inline] - pub fn wasm_sharpe_ratio(&self, rfr: f64) -> f64 { - self.inner.borrow().sharpe_ratio(rfr) + pub fn wasm_sharpe_ratio(&self) -> f64 { + self.inner.borrow().sharpe_ratio() } #[wasm_bindgen(js_name = "sortinoRatio")] #[inline] - pub fn wasm_sortino_ratio(&self, rfr: f64) -> f64 { - self.inner.borrow().sortino_ratio(rfr) + pub fn wasm_sortino_ratio(&self) -> f64 { + self.inner.borrow().sortino_ratio() + } + + #[wasm_bindgen(js_name = "expectancy")] + #[inline] + pub fn wasm_expectancy(&self) -> f64 { + self.inner.borrow().expectancy() } #[wasm_bindgen(getter = winningTradesCount)] diff --git a/core/lib.rs b/core/lib.rs index dac0538..733e59e 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -23,6 +23,7 @@ pub mod legacy; pub mod metrics; pub mod ohlcv; pub mod orderbook; +pub mod plot; pub mod signal; pub mod stats; pub mod sym; @@ -51,6 +52,7 @@ cfg_if::cfg_if! { if #[cfg(feature = "bindings_py")] { pub mod signal_py; pub mod orderbook_py; pub mod backtest_py; + pub mod plot_py; use timeframe_py::PyTimeframe; use sym_py::PySym; use sym_py::PySymKind; @@ -123,6 +125,8 @@ fn py_lib_mod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(get_version, m)?)?; m.add_function(wrap_pyfunction!(metrics_py::py_expectancy, m)?)?; m.add_function(wrap_pyfunction!(metrics_py::py_expectancy_score, m)?)?; @@ -146,6 +150,7 @@ fn py_lib_mod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(metrics_py::py_precision, m)?)?; m.add_function(wrap_pyfunction!(metrics_py::py_recall, m)?)?; m.add_function(wrap_pyfunction!(metrics_py::py_f1, m)?)?; + m.add_function(wrap_pyfunction!(metrics_py::py_annualization_factor, m)?)?; Ok(()) } define_stub_info_gatherer!(stub_info); diff --git a/core/metrics.rs b/core/metrics.rs index 099dfc2..a746263 100644 --- a/core/metrics.rs +++ b/core/metrics.rs @@ -1,4 +1,9 @@ -use crate::stats::{mean, stdev, sum}; +use chrono::Duration; + +use crate::{ + stats::{mean, stdev, sum}, + timeframe::Timeframe, +}; #[inline] pub fn expectancy(pnl: &[f64]) -> f64 { @@ -237,3 +242,11 @@ pub fn f1(precision: f64, recall: f64) -> f64 { // pub fn max_run_up_pct(max_run_up: f64, bar_equity_max: f64) -> f64 { // return max_run_up / bar_equity_max; // } + +#[inline] +pub fn annualization_factor(timeframe: Timeframe, trading_days: f64) -> f64 { + let timeframe_duration: Duration = timeframe.try_into().unwrap(); + let seconds_per_year = trading_days * 24.0 * 60.0 * 60.0; + let seconds_per_timeframe = timeframe_duration.num_seconds() as f64; + return seconds_per_year / seconds_per_timeframe * trading_days as f64; +} diff --git a/core/metrics_node.rs b/core/metrics_node.rs index 0257962..154b042 100644 --- a/core/metrics_node.rs +++ b/core/metrics_node.rs @@ -1,9 +1,13 @@ -use crate::metrics::{ - accuracy, avg_losing_trade, avg_trade, avg_win_loss_ratio, avg_winning_trade, expectancy, - expectancy_score, f1, gross_loss_pct, gross_profit_pct, long_net_profit_pct, - long_net_profit_ratio, net_profit_pct, omega_ratio, omega_ratio_from_returns, pnl, precision, - profit_factor, recall, sharpe_ratio, sharpe_ratio_from_returns, short_net_profit_pct, - sortino_ratio, sortino_ratio_from_returns, win_rate, +use crate::{ + metrics::{ + accuracy, annualization_factor, avg_losing_trade, avg_trade, avg_win_loss_ratio, + avg_winning_trade, expectancy, expectancy_score, f1, gross_loss_pct, gross_profit_pct, + long_net_profit_pct, long_net_profit_ratio, net_profit_pct, omega_ratio, + omega_ratio_from_returns, pnl, precision, profit_factor, recall, sharpe_ratio, + sharpe_ratio_from_returns, short_net_profit_pct, sortino_ratio, sortino_ratio_from_returns, + win_rate, + }, + timeframe_node::NodeTimeframe, }; use napi_derive::napi; @@ -164,3 +168,9 @@ pub fn node_recall(tp_count: f64, fn_count: f64) -> f64 { pub fn node_f1(precision: f64, recall: f64) -> f64 { f1(precision, recall) } + +#[napi(js_name = "annualization_factor")] +#[inline] +pub fn node_annualization_factor(timeframe: &NodeTimeframe, trading_days: f64) -> f64 { + return annualization_factor(timeframe.into(), trading_days); +} diff --git a/core/metrics_py.rs b/core/metrics_py.rs index 62f7ab1..497cbd7 100644 --- a/core/metrics_py.rs +++ b/core/metrics_py.rs @@ -1,9 +1,13 @@ -use crate::metrics::{ - accuracy, avg_losing_trade, avg_trade, avg_win_loss_ratio, avg_winning_trade, expectancy, - expectancy_score, f1, gross_loss_pct, gross_profit_pct, long_net_profit_pct, - long_net_profit_ratio, net_profit_pct, omega_ratio, omega_ratio_from_returns, pnl, precision, - profit_factor, recall, sharpe_ratio, sharpe_ratio_from_returns, short_net_profit_pct, - sortino_ratio, sortino_ratio_from_returns, win_rate, +use crate::{ + metrics::{ + accuracy, annualization_factor, avg_losing_trade, avg_trade, avg_win_loss_ratio, + avg_winning_trade, expectancy, expectancy_score, f1, gross_loss_pct, gross_profit_pct, + long_net_profit_pct, long_net_profit_ratio, net_profit_pct, omega_ratio, + omega_ratio_from_returns, pnl, precision, profit_factor, recall, sharpe_ratio, + sharpe_ratio_from_returns, short_net_profit_pct, sortino_ratio, sortino_ratio_from_returns, + win_rate, + }, + timeframe_py::PyTimeframe, }; use pyo3::prelude::*; use pyo3_stub_gen::derive::gen_stub_pyfunction; @@ -190,3 +194,10 @@ pub fn py_recall(tp_count: f64, fn_count: f64) -> f64 { pub fn py_f1(precision: f64, recall: f64) -> f64 { return f1(precision, recall); } + +#[gen_stub_pyfunction] +#[pyfunction(name = "annualization_factor")] +#[inline] +pub fn py_annualization_factor(timeframe: PyTimeframe, trading_days: f64) -> f64 { + return annualization_factor(timeframe.into(), trading_days); +} diff --git a/core/metrics_wasm.rs b/core/metrics_wasm.rs index 944a9e4..99b5837 100644 --- a/core/metrics_wasm.rs +++ b/core/metrics_wasm.rs @@ -1,9 +1,13 @@ -use crate::metrics::{ - accuracy, avg_losing_trade, avg_trade, avg_win_loss_ratio, avg_winning_trade, expectancy, - expectancy_score, f1, gross_loss_pct, gross_profit_pct, long_net_profit_pct, - long_net_profit_ratio, net_profit_pct, omega_ratio, omega_ratio_from_returns, pnl, precision, - profit_factor, recall, sharpe_ratio, sharpe_ratio_from_returns, short_net_profit_pct, - sortino_ratio, sortino_ratio_from_returns, win_rate, +use crate::{ + metrics::{ + accuracy, annualization_factor, avg_losing_trade, avg_trade, avg_win_loss_ratio, + avg_winning_trade, expectancy, expectancy_score, f1, gross_loss_pct, gross_profit_pct, + long_net_profit_pct, long_net_profit_ratio, net_profit_pct, omega_ratio, + omega_ratio_from_returns, pnl, precision, profit_factor, recall, sharpe_ratio, + sharpe_ratio_from_returns, short_net_profit_pct, sortino_ratio, sortino_ratio_from_returns, + win_rate, + }, + timeframe_wasm::WasmTimeframe, }; use wasm_bindgen::prelude::*; @@ -164,3 +168,9 @@ pub fn wasm_recall(tp_count: f64, fn_count: f64) -> f64 { pub fn wasm_f1(precision: f64, recall: f64) -> f64 { f1(precision, recall) } + +#[wasm_bindgen(js_name = "annualization_factor")] +#[inline] +pub fn wasm_annualization_factor(timeframe: WasmTimeframe, trading_days: f64) -> f64 { + return annualization_factor(timeframe.into(), trading_days); +} diff --git a/core/ohlcv.rs b/core/ohlcv.rs index dcc551d..ae22b56 100644 --- a/core/ohlcv.rs +++ b/core/ohlcv.rs @@ -283,6 +283,10 @@ pub trait OhlcvReader: fmt::Debug { self.get(idx) } + fn timeframe(&self) -> Timeframe { + Timeframe::Unknown() + } + fn slice(&self, range: Range) -> Vec; fn bars(&self) -> Vec { return self.slice(0..self.len()); @@ -395,18 +399,18 @@ pub trait OhlcvReader: fmt::Debug { } #[inline] - fn sanity_check(&self) -> Result<(), Vec> { - let mut messages = vec![]; + fn sanity_check(&self) -> Result<(), String> { if self.len() == 0 { - messages.push("ohlcv is empty".to_string()); - } - let all_volume_nan = self - .slice(0..self.len()) - .iter() - .all(|bar| bar.volume().is_nan()); - if all_volume_nan { - messages.push("ohlcv volume is all NaN".to_string()); + return Err("ohlcv is empty".to_string()); } + // let all_volume_nan = self + // .slice(0..self.len()) + // .iter() + // .all(|bar| bar.volume().is_nan()); + // if all_volume_nan { + // messages.push("ohlcv volume is all NaN".to_string()); + // } + let mut bar_messages = vec![]; for bar_index in 0..self.len() { let bar: OhlcvBar = self.get(bar_index).unwrap(); let prev_bar: Option = if bar_index > 0 { @@ -414,7 +418,6 @@ pub trait OhlcvReader: fmt::Debug { } else { None }; - let mut bar_messages = vec![]; if bar.close_time() <= bar.open_time() { bar_messages.push("close_time <= open_time".to_string()); } @@ -441,11 +444,26 @@ pub trait OhlcvReader: fmt::Debug { if bar.close().is_nan() { bar_messages.push("close is NaN".to_string()); } - if !all_volume_nan && bar.volume().is_nan() { + if bar.volume().is_nan() { bar_messages.push("volume is NaN".to_string()); } + if bar.open() <= 0.0 { + bar_messages.push("open <= 0".to_string()); + } + if bar.high() <= 0.0 { + bar_messages.push("high <= 0".to_string()); + } + if bar.low() <= 0.0 { + bar_messages.push("low <= 0".to_string()); + } + if bar.close() <= 0.0 { + bar_messages.push("close <= 0".to_string()); + } + if bar.volume() < 0.0 { + bar_messages.push("volume < 0".to_string()); + } if bar_messages.len() > 0 { - messages.push(format!( + return Err(format!( "bar[{} | {:?} | {:?}]: {}", bar_index, bar.open_time(), @@ -454,11 +472,7 @@ pub trait OhlcvReader: fmt::Debug { )); } } - return if messages.len() > 0 { - Err(messages) - } else { - Ok(()) - }; + return Ok(()); } #[inline] @@ -625,6 +639,11 @@ impl OhlcvReader for Ohlcv { fn as_any(&self) -> &dyn Any { self } + + #[inline] + fn timeframe(&self) -> Timeframe { + self.timeframe + } } impl OhlcvWriter for Ohlcv { @@ -799,6 +818,11 @@ impl OhlcvReader for RcOhlcv { fn as_any(&self) -> &dyn Any { self } + + #[inline] + fn timeframe(&self) -> Timeframe { + self.inner.borrow().timeframe() + } } impl OhlcvWriter for RcOhlcv { @@ -953,6 +977,11 @@ impl OhlcvReader for ArcOhlcv { fn as_any(&self) -> &dyn Any { self } + + #[inline] + fn timeframe(&self) -> Timeframe { + self.inner.read().unwrap().timeframe() + } } impl OhlcvWriter for ArcOhlcv { diff --git a/core/ohlcv_node.rs b/core/ohlcv_node.rs index 72da0b3..1e035b2 100644 --- a/core/ohlcv_node.rs +++ b/core/ohlcv_node.rs @@ -395,13 +395,13 @@ impl NodeOhlcv { format!("{:?}", self.inner) } - #[napi(js_name = "sanityCheck")] - pub fn node_sanity_check(&self) -> Vec { - match self.inner.sanity_check() { - Ok(_) => vec![], - Err(e) => e, - } - } + // #[napi(js_name = "sanityCheck")] + // pub fn node_sanity_check(&self) -> Vec { + // match self.inner.sanity_check() { + // Ok(_) => vec![], + // Err(e) => e, + // } + // } #[napi(js_name = "ref")] #[inline] diff --git a/core/ohlcv_py.rs b/core/ohlcv_py.rs index e05ff35..6daa7d0 100644 --- a/core/ohlcv_py.rs +++ b/core/ohlcv_py.rs @@ -520,10 +520,10 @@ impl PyOhlcv { #[pyo3(name = "sanity_check")] #[inline] - pub fn py_sanity_check(&self) -> (bool, Vec) { + pub fn py_sanity_check(&self) -> (bool, Option) { match self.inner.sanity_check() { - Ok(_) => (true, vec![]), - Err(e) => (false, e), + Ok(_) => (true, None), + Err(e) => (false, Some(e)), } } diff --git a/core/ohlcv_wasm.rs b/core/ohlcv_wasm.rs index 7606acb..1cca803 100644 --- a/core/ohlcv_wasm.rs +++ b/core/ohlcv_wasm.rs @@ -415,14 +415,14 @@ impl WasmOhlcv { format!("{:?}", self.inner) } - #[wasm_bindgen(js_name = "sanityCheck")] - #[inline] - pub fn wasm_sanity_check(&self) -> Vec { - match self.inner.sanity_check() { - Ok(_) => vec![], - Err(e) => e, - } - } + // #[wasm_bindgen(js_name = "sanityCheck")] + // #[inline] + // pub fn wasm_sanity_check(&self) -> Vec { + // match self.inner.sanity_check() { + // Ok(_) => vec![], + // Err(e) => e, + // } + // } #[wasm_bindgen(js_name = "ref")] #[inline] diff --git a/core/plot.rs b/core/plot.rs new file mode 100644 index 0000000..9984c7c --- /dev/null +++ b/core/plot.rs @@ -0,0 +1,100 @@ +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum LineStyle { + Solid, + Dashed, + Dotted, +} + +impl From for String { + fn from(value: LineStyle) -> Self { + match value { + LineStyle::Solid => "solid".to_string(), + LineStyle::Dashed => "dashed".to_string(), + LineStyle::Dotted => "dotted".to_string(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Position { + TopCenter, + BottomCenter, +} + +impl From for String { + fn from(value: Position) -> Self { + match value { + Position::TopCenter => "top_center".to_string(), + Position::BottomCenter => "bottom_center".to_string(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Pane { + pub boxes: Vec, + pub labels: Vec