Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
5 changes: 3 additions & 2 deletions BUILD.bazed
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

- CLI + upcoming UI

[![indicator plot](/static/indicator_banner.png)](/examples/python/main.py)

## Quick Links

- [Home](https://qpace.dev)
Expand Down
2 changes: 1 addition & 1 deletion cli/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class Profile {
public async ping(): Promise<void> {
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(
Expand Down
2 changes: 1 addition & 1 deletion cli/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "qpace_core"
version = "0.2.6"
version = "0.2.8"
edition = "2021"

[lib]
Expand Down
79 changes: 66 additions & 13 deletions core/backtest.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,6 +15,7 @@ use crate::{
signal::{Signal, SignalKind},
stats::returns,
sym::Sym,
timeframe::Timeframe,
trade::{Trade, TradeError, TradeEvent},
utils::with_suffix,
};
Expand All @@ -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 {
Expand All @@ -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,
}
}
}
Expand All @@ -55,6 +62,8 @@ impl BacktestConfig {
initial_capital,
process_orders_on_close,
debug: false,
risk_free_rate: f64::NAN,
annualization_factor: f64::NAN,
};
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -117,12 +136,19 @@ pub struct Backtest {

impl Backtest {
#[inline]
pub fn new(ctx: Rc<RefCell<Ctx>>, config: BacktestConfig) -> Self {
pub fn new(ctx: Rc<RefCell<Ctx>>, 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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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<OrderConfig> {
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 {
Expand All @@ -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());
// }

Expand Down Expand Up @@ -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<OrderConfig> = 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 {
Expand Down Expand Up @@ -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("%");
Expand Down Expand Up @@ -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![
Expand Down Expand Up @@ -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);
}
Expand Down
18 changes: 14 additions & 4 deletions core/backtest_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ impl NodeBacktest {
ctx: &NodeCtx,
initial_capital: Option<f64>,
process_orders_on_close: Option<bool>,
risk_free_rate: Option<f64>,
annualization_factor: Option<f64>,
) -> 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(),
Expand Down Expand Up @@ -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)]
Expand Down
Loading