use crate::trade::error::TradeError;
use crate::trade::orders::intent::{Exit, OrderIntent, Size};
use crate::trade::orders::{Fit, Order};
use crate::utils::json::Json;
use crate::utils::sign::goldilocks::Fp;
use crate::utils::sign::poseidon2::{hash_no_pad, hash_two_to_one};
use crate::utils::sign::tx::{Attributes, BadTx, Tx, TxType, element, number};
use crate::utils::{Dec, Grid, OrderKind};
pub const MOST_IN_GROUP: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Grouping {
Oto,
Oco,
Otoco,
}
impl Grouping {
#[must_use]
pub const fn code(self) -> u8 {
match self {
Grouping::Oto => 1,
Grouping::Oco => 2,
Grouping::Otoco => 3,
}
}
#[must_use]
pub const fn from_code(code: u8) -> Option<Grouping> {
match code {
1 => Some(Grouping::Oto),
2 => Some(Grouping::Oco),
3 => Some(Grouping::Otoco),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateGroupedOrders {
pub grouping: Grouping,
pub orders: Vec<Order>,
pub attributes: Attributes,
}
impl CreateGroupedOrders {
#[must_use]
pub fn oto(entry: Order, child: Order) -> CreateGroupedOrders {
CreateGroupedOrders::new(Grouping::Oto, vec![entry, sizeless(child)])
}
#[must_use]
pub fn otoco(entry: Order, take_profit: Order, stop_loss: Order) -> CreateGroupedOrders {
CreateGroupedOrders::new(Grouping::Otoco, vec![entry, sizeless(take_profit), sizeless(stop_loss)])
}
#[must_use]
pub fn oco(take_profit: Order, stop_loss: Order) -> CreateGroupedOrders {
CreateGroupedOrders::new(Grouping::Oco, vec![take_profit, stop_loss])
}
fn new(grouping: Grouping, orders: Vec<Order>) -> CreateGroupedOrders {
CreateGroupedOrders { grouping, orders, attributes: Attributes::NONE }
}
#[must_use]
pub fn with(mut self, attributes: Attributes) -> CreateGroupedOrders {
self.attributes = attributes;
self
}
pub fn bracket(
entry: Order,
take_profit: Option<Dec>,
stop_loss: Option<Dec>,
grid: Grid,
now_ms: u64,
fit: Fit,
) -> Result<CreateGroupedOrders, TradeError> {
if entry.reduce_only {
return Err(TradeError::NotFor(
"a take-profit and a stop go with an entry that opens, not a reduce-only one",
));
}
let child = |exit: Exit, trigger: Dec| -> Result<Order, TradeError> {
let intent = OrderIntent::trigger(entry.market, entry.side.opposite(), Size::Position, exit, trigger, None)
.reduce_only()
.fit(fit);
let order = intent.build(grid, now_ms)?;
Ok(Order { expiry: if entry.expiry != 0 { entry.expiry } else { order.expiry }, ..order })
};
let group = match (take_profit, stop_loss) {
(Some(take_profit), Some(stop_loss)) => CreateGroupedOrders::otoco(
entry,
child(Exit::TakeProfit, take_profit)?,
child(Exit::StopLoss, stop_loss)?,
),
(Some(take_profit), None) => CreateGroupedOrders::oto(entry, child(Exit::TakeProfit, take_profit)?),
(None, Some(stop_loss)) => CreateGroupedOrders::oto(entry, child(Exit::StopLoss, stop_loss)?),
(None, None) => return Err(TradeError::NotFor("a bracket needs a take-profit or a stop")),
};
group.check()?;
Ok(group)
}
fn check_oco(&self) -> Result<(), BadTx> {
let [first, second] = self.orders.as_slice() else {
return Err(size_invalid());
};
if first.base_amount != second.base_amount {
return Err(BadTx::new("BaseAmount", "both orders of an OCO have the same size"));
}
if first.side != second.side {
return Err(BadTx::new("IsAsk", "both orders of an OCO are on the same side"));
}
if !first.reduce_only || !second.reduce_only {
return Err(BadTx::new("ReduceOnly", "both orders of an OCO are reduce-only"));
}
if first.expiry != second.expiry {
return Err(BadTx::new("OrderExpiry", "both orders of an OCO lapse together"));
}
siblings(first, second)
}
fn check_oto(&self) -> Result<(), BadTx> {
let [entry, child] = self.orders.as_slice() else {
return Err(size_invalid());
};
if child.base_amount != 0 {
return Err(BadTx::new("BaseAmount", "a child has no size: it takes the entry's"));
}
if entry.side == child.side {
return Err(BadTx::new("IsAsk", "a child is on the other side of its entry"));
}
if entry.expiry != 0 && entry.expiry != child.expiry {
return Err(BadTx::new("OrderExpiry", "a child lapses with a resting entry"));
}
parent(entry)?;
child_rules(child)
}
fn check_otoco(&self) -> Result<(), BadTx> {
let [entry, first, second] = self.orders.as_slice() else {
return Err(size_invalid());
};
if first.base_amount != 0 || second.base_amount != 0 {
return Err(BadTx::new("BaseAmount", "a child has no size: it takes the entry's"));
}
if entry.side == first.side || entry.side == second.side {
return Err(BadTx::new("IsAsk", "a child is on the other side of its entry"));
}
if first.expiry != second.expiry {
return Err(BadTx::new("OrderExpiry", "the children lapse together"));
}
if entry.expiry != 0 && entry.expiry != first.expiry {
return Err(BadTx::new("OrderExpiry", "the children lapse with a resting entry"));
}
parent(entry)?;
siblings(first, second)
}
}
const fn sizeless(child: Order) -> Order {
Order { base_amount: 0, ..child }
}
fn size_invalid() -> BadTx {
BadTx::new("Orders", "the grouping does not take this many orders")
}
fn parent(order: &Order) -> Result<(), BadTx> {
match order.kind {
OrderKind::Market | OrderKind::Limit => order.check(),
_ => Err(BadTx::new("Type", "an entry is a market order or a limit")),
}
}
fn child_rules(order: &Order) -> Result<(), BadTx> {
match order.kind.is_trigger() {
true => order.check_trigger(),
false => Err(BadTx::new("Type", "a child is a stop or a take-profit")),
}
}
fn siblings(first: &Order, second: &Order) -> Result<(), BadTx> {
child_rules(first)?;
child_rules(second)?;
let stop = |order: &Order| matches!(order.kind, OrderKind::StopLoss | OrderKind::StopLossLimit);
let profit = |order: &Order| matches!(order.kind, OrderKind::TakeProfit | OrderKind::TakeProfitLimit);
match (stop(first) || stop(second)) && (profit(first) || profit(second)) {
true => Ok(()),
false => Err(BadTx::new("Type", "the two are a take-profit and a stop")),
}
}
impl Tx for CreateGroupedOrders {
fn tx_type(&self) -> TxType {
TxType::CreateGroupedOrders
}
fn hash_fields(&self) -> Vec<Fp> {
let folded = self
.orders
.iter()
.map(|order| hash_no_pad(&order.hash_fields()))
.reduce(hash_two_to_one)
.unwrap_or([Fp::ZERO; 4]);
let mut fields = vec![element(self.grouping.code())];
fields.extend(folded);
fields
}
fn info_fields(&self) -> Vec<(&'static str, Json)> {
let orders = self
.orders
.iter()
.map(|order| Json::Object(order.info().into_iter().map(|(key, value)| (key.to_owned(), value)).collect()))
.collect();
vec![("GroupingType", number(self.grouping.code())), ("Orders", Json::Array(orders))]
}
fn attributes(&self) -> Attributes {
self.attributes
}
fn check(&self) -> Result<(), BadTx> {
self.attributes.check()?;
let Some(first) = self.orders.first().filter(|_| self.orders.len() <= MOST_IN_GROUP) else {
return Err(size_invalid());
};
let mut indexes = Vec::with_capacity(self.orders.len());
for order in &self.orders {
if order.market != first.market {
return Err(BadTx::new("MarketIndex", "every order of a group is on one market"));
}
if let Some(index) = order.client_index {
if indexes.contains(&index) {
return Err(BadTx::new("ClientOrderIndex", "each order of a group has its own client index"));
}
indexes.push(index);
}
order.check_fields()?;
}
match self.grouping {
Grouping::Oco => self.check_oco(),
Grouping::Oto => self.check_oto(),
Grouping::Otoco => self.check_otoco(),
}
}
}
#[cfg(test)]
#[path = "tests/grouped.rs"]
mod tests;