Skip to content

Commit e53aa42

Browse files
committed
rw: strategy: proper types for protocols (some v_exchanges typing use)
1 parent 3ab7105 commit e53aa42

File tree

8 files changed

+561
-428
lines changed

8 files changed

+561
-428
lines changed

Cargo.lock

Lines changed: 155 additions & 374 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ tracing = { version = "^0.1.44", features = ["log", "std", "async-await"] }
3131
v_exchanges = { version = "^0.16.7", features = ["full"] }
3232
v_utils = { version = "2.15.11", features = ["full"] }
3333

34-
#[patch.crates-io]
35-
#v_utils = { path = "../v_utils/v_utils" }
36-
#v_utils_macros = { path = "../v_utils/v_utils_macros" }
34+
[patch.crates-io]
35+
v_exchanges = { path = "../v_exchanges/v_exchanges" }
36+
v_exchanges_adapters = { path = "../v_exchanges/v_exchanges_adapters" }
37+
v_exchanges_api_generics = { path = "../v_exchanges/v_exchanges_api_generics" }
38+
v_utils = { path = "../v_utils/v_utils" }
39+
v_utils_macros = { path = "../v_utils/v_utils_macros" }
3740

3841
[profile.dev]
3942
# codegen-backend = "cranelift" # disabled due to aws-lc-rs incompatibility

discretionary_engine_strategy/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@ cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
1616
[dependencies]
1717
clap = { workspace = true }
1818
color-eyre = { workspace = true }
19+
derive-new = "0.7"
20+
discretionary_engine_macros = { path = "../discretionary_engine_macros" }
21+
eyre = "0.6"
1922
futures-util = "0.3.31"
2023
nautilus-bybit = { path = "../libs/nautilus_trader/crates/adapters/bybit", default-features = false }
2124
nautilus-model = { path = "../libs/nautilus_trader/crates/model" }
2225
redis = { version = "0.27", features = ["tokio-comp", "streams"] }
26+
serde = { workspace = true }
2327
tokio = { version = "^1.48.0", features = ["full", "signal"] }
2428
tracing = { workspace = true }
2529
tracing-subscriber = { version = "^0.3.20", features = ["fmt", "env-filter"] }
30+
v_exchanges = { workspace = true }
2631
v_utils = { workspace = true }
2732

2833
[dev-dependencies]

discretionary_engine_strategy/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
pub mod commands;
2323
pub mod config;
2424
pub mod data;
25+
pub mod order_types;
2526
pub mod protocols;
2627
pub mod redis_bus;
2728
pub mod strategy;

discretionary_engine_strategy/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ async fn main() -> Result<()> {
120120

121121
// Build CLI string and publish to Redis
122122
let cli_string = build_cli_string(&args, cli.testnet);
123-
println!("Publishing command: {}", cli_string);
123+
println!("Publishing command: {cli_string}");
124124

125125
let mut conn = redis_bus::connect(cli.redis_port).await?;
126126
let id = redis_bus::publish_command(&mut conn, &cli_string).await?;
127-
println!("Command published with ID: {}", id);
127+
println!("Command published with ID: {id}");
128128
}
129129
Commands::Adjust(args) => {
130130
//Q: think logic should be very similar right, - we just validate, then submit over into the actual execution. Just slightly different set of commands that could be passed here
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! Order type definitions for the strategy engine.
2+
3+
use std::hash::Hash;
4+
5+
use color_eyre::eyre::{Result, bail};
6+
use derive_new::new;
7+
use serde::{Deserialize, Serialize};
8+
use v_exchanges::core::Symbol;
9+
use v_utils::{Percent, trades::Side};
10+
11+
pub trait IdRequirements: Hash + Clone + PartialEq + Default + std::fmt::Debug {}
12+
impl<T: Hash + Clone + PartialEq + Default + std::fmt::Debug> IdRequirements for T {}
13+
14+
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, new)]
15+
pub struct Order<Id: IdRequirements> {
16+
pub id: Id,
17+
pub order_type: OrderType,
18+
pub symbol: Symbol,
19+
pub side: Side,
20+
pub qty_notional: f64,
21+
}
22+
23+
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
24+
pub enum OrderType {
25+
#[default]
26+
Market,
27+
StopMarket(StopMarketOrder),
28+
}
29+
30+
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, new)]
31+
pub struct StopMarketOrder {
32+
pub price: f64,
33+
}
34+
35+
//=============================================================================
36+
// Conceptual Orders
37+
//=============================================================================
38+
39+
#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize, new)]
40+
pub struct ProtocolOrderId {
41+
pub protocol_signature: String,
42+
pub ordinal: usize,
43+
}
44+
45+
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, new)]
46+
pub struct ConceptualOrder<Id: IdRequirements> {
47+
pub id: Id,
48+
pub order_type: ConceptualOrderType,
49+
pub symbol: Symbol,
50+
pub side: Side,
51+
pub qty_notional: f64,
52+
}
53+
54+
impl<Id: IdRequirements> ConceptualOrder<Id> {
55+
pub fn price(&self) -> Result<f64> {
56+
match &self.order_type {
57+
ConceptualOrderType::Market(_) => bail!("Market orders don't have a price"),
58+
ConceptualOrderType::Limit(l) => Ok(l.price),
59+
ConceptualOrderType::StopMarket(s) => Ok(s.price),
60+
}
61+
}
62+
}
63+
64+
/// Generics for defining order types and their whereabouts. Details of execution do not concern us here.
65+
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
66+
pub enum ConceptualOrderType {
67+
Market(ConceptualMarket),
68+
Limit(ConceptualLimit),
69+
StopMarket(ConceptualStopMarket),
70+
}
71+
72+
impl Default for ConceptualOrderType {
73+
fn default() -> Self {
74+
ConceptualOrderType::Market(ConceptualMarket::default())
75+
}
76+
}
77+
78+
/// Will be executed via above-the-price limits most of the time to prevent excessive slippages.
79+
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, new)]
80+
pub struct ConceptualMarket {
81+
/// 1.0 will be translated into an actual Market order. Others, most of the time, will be expressed via limit orders.
82+
pub maximum_slippage_percent: Percent,
83+
}
84+
85+
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, new)]
86+
pub struct ConceptualStopMarket {
87+
pub price: f64,
88+
}
89+
90+
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize, new)]
91+
pub struct ConceptualLimit {
92+
pub price: f64,
93+
pub limit_only: bool,
94+
}
95+
96+
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, new)]
97+
pub struct ConceptualOrderPercents {
98+
pub order_type: ConceptualOrderType,
99+
pub symbol: Symbol,
100+
pub side: Side,
101+
pub qty_percent_of_controlled: Percent,
102+
}
103+
104+
impl ConceptualOrderPercents {
105+
pub fn to_exact<Id: IdRequirements>(self, total_controlled_size: f64, id: Id) -> ConceptualOrder<Id> {
106+
ConceptualOrder {
107+
id,
108+
order_type: self.order_type,
109+
symbol: self.symbol,
110+
side: self.side,
111+
qty_notional: total_controlled_size * *self.qty_percent_of_controlled,
112+
}
113+
}
114+
}
Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,80 @@
11
//! DummyMarket protocol - sends a single market order.
22
3-
use std::{fmt, str::FromStr};
3+
use color_eyre::eyre::Result;
4+
use discretionary_engine_macros::ProtocolWrapper;
5+
use tokio::{sync::mpsc, task::JoinSet};
6+
use v_exchanges::core::{Instrument, Symbol};
7+
use v_utils::{
8+
Percent,
9+
macros::CompactFormat,
10+
trades::{Pair, Side},
11+
};
412

5-
use color_eyre::eyre::{Result, bail};
13+
use super::{ProtocolOrders, ProtocolTrait, ProtocolType};
14+
use crate::order_types::{ConceptualMarket, ConceptualOrderPercents, ConceptualOrderType};
615

7-
/// A protocol that simply sends one market order.
8-
///
9-
/// This is the simplest possible protocol, used for testing and debugging.
10-
#[derive(Clone, Debug, Default)]
11-
pub struct DummyMarket;
16+
/// Literally just sends one market order.
17+
#[derive(Clone, CompactFormat, Debug, Default, ProtocolWrapper, derive_new::new)]
18+
pub struct DummyMarket {}
1219

13-
impl DummyMarket {
14-
/// Protocol prefix used for parsing.
15-
pub const PREFIX: &'static str = "dm";
16-
}
20+
impl ProtocolTrait for DummyMarketWrapper {
21+
type Params = DummyMarket;
22+
23+
fn attach(&self, set: &mut JoinSet<Result<()>>, tx_orders: mpsc::Sender<ProtocolOrders>, asset: String, protocol_side: Side) -> Result<()> {
24+
let symbol = Symbol::new(Pair::new(asset, "USDT".to_string()), Instrument::Perp);
25+
let m = ConceptualMarket::new(Percent(1.0));
26+
let order = ConceptualOrderPercents::new(ConceptualOrderType::Market(m), symbol, protocol_side, Percent::new(1.0));
1727

18-
impl fmt::Display for DummyMarket {
19-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20-
write!(f, "{}", Self::PREFIX)
28+
let protocol_spec = self.0.read().unwrap().to_string();
29+
let protocol_orders = ProtocolOrders::new(protocol_spec, vec![Some(order)]);
30+
set.spawn(async move {
31+
tx_orders.send(protocol_orders).await.unwrap();
32+
// LOOP: it's a dummy protocol, relax
33+
loop {
34+
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
35+
}
36+
#[expect(unreachable_code)]
37+
Ok(())
38+
});
39+
Ok(())
2140
}
22-
}
2341

24-
impl FromStr for DummyMarket {
25-
type Err = color_eyre::eyre::Report;
42+
fn update_params(&self, _params: Self::Params) -> Result<()> {
43+
unimplemented!()
44+
}
2645

27-
fn from_str(spec: &str) -> Result<Self> {
28-
// Accept "dm" or "dm:" with no additional params
29-
let trimmed = spec.trim();
30-
if trimmed == Self::PREFIX || trimmed.starts_with(&format!("{}:", Self::PREFIX)) {
31-
// For now, DummyMarket has no parameters
32-
let after_prefix = trimmed.strip_prefix(Self::PREFIX).unwrap_or("");
33-
let after_colon = after_prefix.strip_prefix(':').unwrap_or(after_prefix);
34-
if after_colon.is_empty() {
35-
return Ok(DummyMarket);
36-
}
37-
bail!("DummyMarket does not accept parameters, got: {after_colon}");
38-
}
39-
bail!("Expected protocol spec starting with '{}', got: {spec}", Self::PREFIX)
46+
fn get_type(&self) -> ProtocolType {
47+
ProtocolType::StopEntry
4048
}
4149
}
4250

4351
#[cfg(test)]
4452
mod tests {
53+
use std::str::FromStr;
54+
4555
use super::*;
4656

4757
#[test]
4858
fn parse_dm() {
49-
let dm = DummyMarket::from_str("dm").unwrap();
50-
assert_eq!(dm.to_string(), "dm");
59+
let dm = DummyMarketWrapper::from_str("dm").unwrap();
60+
assert_eq!(dm.signature(), "dm");
5161
}
5262

5363
#[test]
5464
fn parse_dm_with_colon() {
55-
let dm = DummyMarket::from_str("dm:").unwrap();
56-
assert_eq!(dm.to_string(), "dm");
65+
let dm = DummyMarketWrapper::from_str("dm:").unwrap();
66+
assert_eq!(dm.signature(), "dm");
5767
}
5868

5969
#[test]
6070
fn parse_dm_with_params_fails() {
61-
let result = DummyMarket::from_str("dm:p0.5");
71+
let result = DummyMarketWrapper::from_str("dm:p0.5");
6272
assert!(result.is_err());
6373
}
6474

6575
#[test]
6676
fn parse_wrong_prefix_fails() {
67-
let result = DummyMarket::from_str("ts:p0.5");
77+
let result = DummyMarketWrapper::from_str("ts:p0.5");
6878
assert!(result.is_err());
6979
}
7080
}

0 commit comments

Comments
 (0)