1use serde::{Deserialize, Serialize};
5
6mod form;
7mod view;
8
9use super::dom::Ladder;
10use super::format::{Numbers, TradingFormatter};
11use super::widgets::{grouped, side_name};
12use crate::chart::TradingLineAction;
13use crate::model::{
14 Capabilities, Order, OrderBook, OrderId, OrderPreview, OrderRequest, OrderSide, OrderType,
15 Position, TakeProfitLevel, Tick, TimeInForce, TradingRules,
16};
17use crate::scales::{decimals, to_step};
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
21pub enum BracketMode {
22 #[default]
24 Price,
25 Ticks,
27 Percent,
29 Money,
31}
32
33impl BracketMode {
34 const ALL: [Self; 4] = [Self::Price, Self::Ticks, Self::Percent, Self::Money];
35
36 fn name(self, currency: &str) -> &str {
37 match self {
38 Self::Price => "price",
39 Self::Ticks => "ticks",
40 Self::Percent => "%",
41 Self::Money if currency.is_empty() => "money",
42 Self::Money => currency,
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
49pub struct Bracket {
50 pub mode: BracketMode,
52 pub value: f64,
54}
55
56impl Bracket {
57 fn sign(side: OrderSide, profit: bool) -> f64 {
60 if (side == OrderSide::Buy) == profit {
61 1.0
62 } else {
63 -1.0
64 }
65 }
66
67 pub(crate) fn price(
71 &self,
72 side: OrderSide,
73 entry: f64,
74 tick: f64,
75 per_point: f64,
76 profit: bool,
77 ) -> f64 {
78 let sign = Self::sign(side, profit);
79 match self.mode {
80 BracketMode::Price => self.value,
81 BracketMode::Ticks => entry + sign * self.value * tick,
82 BracketMode::Percent => entry * (1.0 + sign * self.value / 100.0),
83 BracketMode::Money => entry + sign * self.value / per_point,
84 }
85 }
86
87 fn at(
89 mode: BracketMode,
90 price: f64,
91 side: OrderSide,
92 entry: f64,
93 tick: f64,
94 per_point: f64,
95 profit: bool,
96 ) -> Self {
97 let distance = Self::sign(side, profit) * (price - entry);
98 let value = match mode {
99 BracketMode::Price => price,
100 BracketMode::Ticks => (distance / tick).round(),
101 BracketMode::Percent => distance / entry * 100.0,
102 BracketMode::Money => distance * per_point,
103 };
104 Self { mode, value }
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111pub struct OrderPreset {
112 pub name: String,
114 pub quantity: f64,
116 pub take_profit: Option<Bracket>,
118 pub stop_loss: Option<Bracket>,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123enum Kind {
124 Market,
125 Limit,
126 Stop,
127 StopLimit,
128}
129
130impl Kind {
131 const ALL: [(Self, &'static str); 4] = [
132 (Self::Market, "Market"),
133 (Self::Limit, "Limit"),
134 (Self::Stop, "Stop"),
135 (Self::StopLimit, "Stop limit"),
136 ];
137
138 fn accepted(self, capabilities: &Capabilities) -> bool {
139 match self {
140 Self::Market => capabilities.market,
141 Self::Limit => capabilities.limit,
142 Self::Stop => capabilities.stop,
143 Self::StopLimit => capabilities.stop_limit,
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150enum Sizing {
151 Units,
153 Value,
155 Risk,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161enum Tab {
162 Order,
163 Dom,
164}
165
166pub(crate) struct Market<'a> {
168 pub symbol: &'a str,
170 pub formatter: Option<&'a dyn TradingFormatter>,
171 pub pip_value: Option<f64>,
173 pub last: Option<f64>,
175 pub bid: Option<f64>,
177 pub ask: Option<f64>,
178 pub tick: f64,
180 pub decimals: usize,
182 pub currency: &'a str,
184 pub balance: Option<f64>,
187 pub capabilities: Capabilities,
188 pub rules: &'a TradingRules,
189 pub rules_ready: bool,
190 pub offline: Option<crate::ui::model::ConnectionState>,
194 pub ladder: Option<std::borrow::Cow<'a, OrderBook>>,
197 pub ladder_unavailable: Option<crate::data::OrderFilterUnavailable>,
199 pub highlights: Vec<(bool, f64, egui::Color32)>,
202 pub orders: &'a [Order],
204 pub positions: &'a [Position],
205 pub trades: &'a [&'a Tick],
207 pub session_open: Option<chrono::DateTime<chrono::Utc>>,
209 pub last_fill: Option<f64>,
211 pub logo: Option<&'a str>,
213}
214
215impl<'a> Market<'a> {
216 pub fn numbers(&self) -> Numbers<'a> {
218 Numbers {
219 symbol: self.symbol,
220 decimals: self.decimals,
221 formatter: self.formatter,
222 }
223 }
224}
225
226#[derive(Debug, PartialEq)]
228pub(crate) enum Action {
229 Place(OrderRequest),
231 Preview(OrderRequest),
233 Cancel(Vec<OrderId>),
235 Confirm(Box<TradingLineAction>),
237}
238
239pub(crate) struct OrderTicket {
241 pub open: bool,
242 pub symbol: String,
243 side: OrderSide,
244 kind: Kind,
245 price: f64,
247 limit: f64,
249 price_in_ticks: bool,
251 sizing: Sizing,
252 quantity: f64,
253 amount: f64,
255 take_profit: (bool, Bracket),
256 levels: Vec<(Bracket, f64)>,
259 stop_loss: (bool, Bracket),
260 time_in_force: TimeInForce,
261 preset_name: String,
262 tab: Tab,
263 exits_open: bool,
264 ladder: Ladder,
265 confirm: Option<OrderRequest>,
267 cancel: Option<Vec<OrderId>>,
268 dom_error: Option<String>,
270}
271
272impl Default for OrderTicket {
273 fn default() -> Self {
274 let bracket = Bracket {
275 mode: BracketMode::Price,
276 value: 0.0,
277 };
278 Self {
279 open: false,
280 symbol: String::new(),
281 side: OrderSide::Buy,
282 kind: Kind::Market,
283 price: 0.0,
284 limit: 0.0,
285 price_in_ticks: false,
286 sizing: Sizing::Units,
287 quantity: 1.0,
288 amount: 1_000.0,
289 take_profit: (false, bracket),
290 levels: Vec::new(),
291 stop_loss: (false, bracket),
292 time_in_force: TimeInForce::Day,
293 preset_name: String::new(),
294 tab: Tab::Order,
295 exits_open: true,
296 ladder: Ladder::default(),
297 confirm: None,
298 cancel: None,
299 dom_error: None,
300 }
301 }
302}
303
304impl OrderTicket {
305 pub fn open(&mut self, request: &OrderRequest) {
307 self.confirm = None;
308 self.cancel = None;
309 if self.symbol != request.symbol {
310 self.ladder = Ladder::default();
311 self.take_profit = (
312 false,
313 Bracket {
314 mode: BracketMode::Price,
315 value: 0.0,
316 },
317 );
318 self.stop_loss = self.take_profit;
319 }
320 self.open = true;
321 self.tab = Tab::Order;
322 self.symbol.clone_from(&request.symbol);
323 self.side = request.side;
324 self.sizing = Sizing::Units;
325 self.quantity = request.quantity;
326 (self.kind, self.price, self.limit) = match request.order_type {
327 OrderType::Limit(price) => (Kind::Limit, price, price),
328 OrderType::Stop(price) => (Kind::Stop, price, price),
329 OrderType::StopLimit { stop, limit } => (Kind::StopLimit, stop, limit),
330 _ => (Kind::Market, 0.0, 0.0),
331 };
332 self.time_in_force = request.time_in_force;
333 let at_price = |value| Bracket {
334 mode: BracketMode::Price,
335 value,
336 };
337 let (first, rest) = match request.take_profit_levels.split_first() {
338 Some((first, rest)) => (Some(first.price), rest),
339 None => (request.take_profit, &[][..]),
340 };
341 self.levels = rest
342 .iter()
343 .map(|level| {
344 let share = level.quantity / request.quantity * 100.0;
345 (at_price(level.price), share)
346 })
347 .collect();
348 for (target, value) in [
349 (&mut self.take_profit, first),
350 (&mut self.stop_loss, request.stop_loss),
351 ] {
352 target.0 = value.is_some();
353 if let Some(value) = value {
354 target.1 = at_price(value);
355 }
356 }
357 }
358
359 pub(super) fn change_symbol(&mut self, symbol: &str) {
361 let request = OrderRequest::new(symbol, self.side, self.quantity, OrderType::Market);
362 self.open(&request);
363 }
364
365 fn touch(&self, market: &Market<'_>) -> Option<f64> {
367 match self.side {
368 OrderSide::Buy => market.ask,
369 OrderSide::Sell => market.bid,
370 }
371 .filter(|price| price.is_finite())
372 }
373
374 fn entry(&self, market: &Market<'_>) -> Option<f64> {
377 match self.kind {
378 Kind::Market => self.touch(market).or(market.last),
379 Kind::Limit | Kind::Stop => Some(self.price),
380 Kind::StopLimit => Some(self.limit),
381 }
382 .filter(|price| price.is_finite() && *price > 0.0)
383 }
384
385 fn exit(
389 &self,
390 market: &Market<'_>,
391 side: OrderSide,
392 entry: f64,
393 quantity: Option<f64>,
394 profit: bool,
395 ) -> Result<Option<f64>, String> {
396 let (enabled, bracket) = if profit {
397 self.take_profit
398 } else {
399 self.stop_loss
400 };
401 if !(market.capabilities.brackets && enabled) {
402 return Ok(None);
403 }
404 let per_point = match (bracket.mode, quantity) {
405 (BracketMode::Money, None) => {
406 return Err("Sizing by risk needs the stop loss as a price, ticks or %".into());
407 }
408 (_, quantity) => quantity.unwrap_or(1.0) * market.rules.point_value,
409 };
410 exit_price(market, side, entry, bracket, per_point, profit).map(Some)
411 }
412
413 fn take_profits(
418 &self,
419 market: &Market<'_>,
420 side: OrderSide,
421 entry: f64,
422 quantity: f64,
423 ) -> Result<(Option<f64>, Vec<TakeProfitLevel>), String> {
424 let capabilities = &market.capabilities;
425 if !(self.take_profit.0 && capabilities.brackets && capabilities.take_profit_levels)
426 || self.levels.is_empty()
427 {
428 let take_profit = self.exit(market, side, entry, Some(quantity), true)?;
429 return Ok((take_profit, Vec::new()));
430 }
431 let rules = market.rules;
432 let mut levels = Vec::with_capacity(self.levels.len() + 1);
433 levels.push((self.take_profit.1, 0.0));
434 for (bracket, share) in &self.levels {
435 if !share.is_finite() || *share <= 0.0 {
436 return Err("Enter a share above 0% for each take-profit level".into());
437 }
438 levels.push((
439 *bracket,
440 round_down(quantity * share / 100.0, rules.quantity_step),
441 ));
442 }
443 let extra: f64 = levels.iter().map(|(_, quantity)| quantity).sum();
444 levels[0].1 = to_step(quantity - extra, rules.quantity_step, f64::round);
445 let levels = levels
446 .into_iter()
447 .map(|(bracket, quantity)| {
448 if quantity < rules.min_quantity {
449 return Err(format!(
450 "Each take-profit level needs at least the minimum quantity of {}",
451 rules.min_quantity
452 ));
453 }
454 let per_point = quantity * rules.point_value;
455 let price = exit_price(market, side, entry, bracket, per_point, true)?;
456 Ok(TakeProfitLevel::new(price, quantity))
457 })
458 .collect::<Result<_, String>>()?;
459 Ok((None, levels))
460 }
461
462 fn with_brackets(
465 &self,
466 mut order: OrderRequest,
467 market: &Market<'_>,
468 ) -> Result<OrderRequest, String> {
469 let entry = match order.order_type {
470 OrderType::Limit(price) | OrderType::Stop(price) => Some(price),
471 OrderType::StopLimit { limit, .. } => Some(limit),
472 OrderType::Market => match order.side {
473 OrderSide::Buy => market.ask,
474 OrderSide::Sell => market.bid,
475 }
476 .or(market.last),
477 }
478 .map(|price| to_step(price, market.tick, f64::round))
479 .filter(|price| price.is_finite() && *price > 0.0)
480 .ok_or("Enter a positive price")?;
481 order.stop_loss = self.exit(market, order.side, entry, Some(order.quantity), false)?;
482 (order.take_profit, order.take_profit_levels) =
483 self.take_profits(market, order.side, entry, order.quantity)?;
484 Ok(order)
485 }
486
487 pub fn request(&self, market: &Market<'_>) -> Result<OrderRequest, String> {
489 if market.offline.is_some() {
490 return Err("Waiting for the broker to connect".into());
491 }
492 if !market.rules_ready {
493 return Err("Waiting for the symbol's trading rules".into());
494 }
495 if !self.kind.accepted(&market.capabilities) {
496 return Err("The account does not accept this order type".into());
497 }
498 let snap = |value| to_step(value, market.tick, f64::round);
499 let entry = self
500 .entry(market)
501 .map(snap)
502 .filter(|price| price.is_finite() && *price > 0.0)
503 .ok_or("Enter a positive price")?;
504 let order_type = match self.kind {
505 Kind::Market => OrderType::Market,
506 Kind::Limit => OrderType::Limit(snap(self.price)),
507 Kind::Stop => OrderType::Stop(snap(self.price)),
508 Kind::StopLimit if self.price.is_finite() && snap(self.price) > 0.0 => {
509 OrderType::StopLimit {
510 stop: snap(self.price),
511 limit: snap(self.limit),
512 }
513 }
514 Kind::StopLimit => return Err("Enter a positive stop price".into()),
515 };
516 let rules = market.rules;
517 let mut quantity = match self.sizing {
519 Sizing::Units => Some(order_quantity(self.quantity, rules)?),
520 Sizing::Value => Some(order_quantity(
521 self.amount / (entry * rules.point_value),
522 rules,
523 )?),
524 Sizing::Risk => None,
525 };
526 let stop_loss = self.exit(market, self.side, entry, quantity, false)?;
527 let quantity = match quantity.take() {
528 Some(quantity) => quantity,
529 None => {
530 let stop = stop_loss.ok_or("Sizing by risk needs a stop loss")?;
531 let balance = market
532 .balance
533 .filter(|balance| balance.is_finite() && *balance > 0.0)
534 .ok_or("Sizing by risk needs the account's balance")?;
535 let risk = balance * self.amount / 100.0;
536 order_quantity(risk / ((entry - stop).abs() * rules.point_value), rules)?
537 }
538 };
539 let (take_profit, levels) = self.take_profits(market, self.side, entry, quantity)?;
540
541 let mut request = OrderRequest::new(&self.symbol, self.side, quantity, order_type);
542 request.take_profit = take_profit;
543 request.take_profit_levels = levels;
544 request.stop_loss = stop_loss;
545 request.time_in_force = if rules.time_in_force.contains(&self.time_in_force) {
546 self.time_in_force
547 } else {
548 rules.time_in_force[0]
549 };
550 Ok(request)
551 }
552
553 fn apply(&mut self, preset: &OrderPreset) {
554 self.sizing = Sizing::Units;
555 self.quantity = preset.quantity;
556 self.take_profit = preset
557 .take_profit
558 .map_or((false, self.take_profit.1), |bracket| (true, bracket));
559 self.levels.clear();
560 self.stop_loss = preset
561 .stop_loss
562 .map_or((false, self.stop_loss.1), |bracket| (true, bracket));
563 }
564
565 fn preset(&self, name: String, market: &Market<'_>) -> OrderPreset {
566 let quantity = self
567 .request(market)
568 .map_or(self.quantity, |request| request.quantity);
569 OrderPreset {
570 name,
571 quantity,
572 take_profit: self.take_profit.0.then_some(self.take_profit.1),
573 stop_loss: self.stop_loss.0.then_some(self.stop_loss.1),
574 }
575 }
576}
577
578fn exit_price(
582 market: &Market<'_>,
583 side: OrderSide,
584 entry: f64,
585 bracket: Bracket,
586 per_point: f64,
587 profit: bool,
588) -> Result<f64, String> {
589 let price = to_step(
590 bracket.price(side, entry, market.tick, per_point, profit),
591 market.tick,
592 f64::round,
593 );
594 let above = (price > entry) == (side == OrderSide::Buy);
596 if !price.is_finite() || price <= 0.0 || price == entry || above != profit {
597 return Err(if profit {
598 "The take profit must lie on the profit side of the entry"
599 } else {
600 "The stop loss must lie on the loss side of the entry"
601 }
602 .into());
603 }
604 Ok(price)
605}
606
607fn reward(request: &OrderRequest, entry: f64) -> Option<f64> {
610 if request.take_profit_levels.is_empty() {
611 let price = request.take_profit?;
612 return Some((price - entry).abs() * request.quantity);
613 }
614 let levels = request.take_profit_levels.iter();
615 Some(
616 levels
617 .map(|level| (level.price - entry).abs() * level.quantity)
618 .sum(),
619 )
620}
621
622fn round_down(quantity: f64, step: f64) -> f64 {
624 to_step(quantity, step, |steps| (steps + 1e-9).floor())
626}
627
628pub(super) fn order_quantity(quantity: f64, rules: &TradingRules) -> Result<f64, String> {
630 let quantity = round_down(quantity, rules.quantity_step);
631 if !quantity.is_finite() || quantity < rules.min_quantity {
632 return Err(format!(
633 "The quantity is below the minimum of {}",
634 rules.min_quantity
635 ));
636 }
637 if rules.max_quantity.is_some_and(|max| quantity > max) {
638 return Err(format!(
639 "The quantity is above the maximum of {}",
640 rules.max_quantity.unwrap_or_default()
641 ));
642 }
643 Ok(quantity)
644}
645
646fn summary(request: &OrderRequest, numbers: &Numbers<'_>, quantity_decimals: usize) -> String {
648 let at = |price: f64, kind: &str| format!(" @ {} {kind}", numbers.price(price));
649 let order_type = match request.order_type {
650 OrderType::Limit(price) => at(price, "LIMIT"),
651 OrderType::Stop(price) => at(price, "STOP"),
652 OrderType::StopLimit { stop, .. } => at(stop, "STOP LIMIT"),
653 _ => " MKT".to_owned(),
654 };
655 format!(
656 "{} {}{order_type}",
657 numbers.quantity(request.quantity, quantity_decimals),
658 request.symbol
659 )
660}
661
662fn confirmation(request: &OrderRequest, numbers: &Numbers<'_>, quantity_decimals: usize) -> String {
664 let mut text = format!(
665 "{} {}?",
666 side_name(request.side),
667 summary(request, numbers, quantity_decimals)
668 );
669 if let Some(price) = request.take_profit {
670 text.push_str(&format!("\nTake profit {}", numbers.price(price)));
671 }
672 for level in &request.take_profit_levels {
673 text.push_str(&format!(
674 "\nTake profit {} for {}",
675 numbers.price(level.price),
676 numbers.quantity(level.quantity, quantity_decimals)
677 ));
678 }
679 if let Some(price) = request.stop_loss {
680 text.push_str(&format!("\nStop loss {}", numbers.price(price)));
681 }
682 text
683}
684
685pub(super) fn preview_message(
688 request: &OrderRequest,
689 estimate: &OrderPreview,
690 rules: &TradingRules,
691 numbers: &Numbers<'_>,
692 currency: &str,
693) -> String {
694 let mut text = confirmation(request, numbers, decimals(rules.quantity_step));
695 for (label, value) in [
696 ("Cost", estimate.cost),
697 ("Commission", estimate.commission),
698 ("Margin", estimate.margin),
699 ] {
700 if let Some(value) = value.filter(|value| value.is_finite()) {
701 text.push_str(&format!(
702 "\n{label}: {} {currency}",
703 grouped(value, 2, false)
704 ));
705 }
706 }
707 for warning in &estimate.warnings {
708 text.push_str(&format!("\n{warning}"));
709 }
710 text
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn opening_a_ticket_replaces_brackets_and_prices() {
719 let mut ticket = OrderTicket::default();
720 let mut request = OrderRequest::new("ES", OrderSide::Buy, 2.0, OrderType::Limit(100.0));
721 request.take_profit = Some(105.0);
722 request.stop_loss = Some(95.0);
723 for brackets in [true, false] {
724 if !brackets {
725 request = OrderRequest::new("NQ", OrderSide::Sell, 1.0, OrderType::Market);
726 }
727 ticket.confirm = Some(request.clone());
728 ticket.cancel = Some(vec![OrderId::new("old")]);
729 ticket.open(&request);
730 assert!(ticket.confirm.is_none());
731 assert!(ticket.cancel.is_none());
732 assert_eq!(ticket.take_profit.0, brackets);
733 assert_eq!(ticket.stop_loss.0, brackets);
734 if brackets {
735 assert_eq!(
736 ticket.take_profit.1,
737 Bracket {
738 mode: BracketMode::Price,
739 value: 105.0
740 }
741 );
742 assert_eq!(
743 ticket.stop_loss.1,
744 Bracket {
745 mode: BracketMode::Price,
746 value: 95.0
747 }
748 );
749 } else {
750 assert_eq!((ticket.price, ticket.limit), (0.0, 0.0));
751 }
752 }
753 }
754
755 #[test]
760 fn pending_entries_are_sized_by_risk_and_ladder_orders_carry_the_brackets() {
761 let rules = TradingRules::new("ES");
762 let mut capabilities = Capabilities::default();
763 (capabilities.limit, capabilities.stop, capabilities.brackets) = (true, true, true);
764 capabilities.take_profit_levels = true;
765 let market = Market {
766 symbol: "ES",
767 formatter: None,
768 pip_value: None,
769 last: Some(100.0),
770 bid: Some(99.75),
771 ask: Some(100.25),
772 tick: 0.25,
773 decimals: 2,
774 currency: "USD",
775 balance: Some(10_000.0),
776 capabilities,
777 rules: &rules,
778 rules_ready: true,
779 offline: None,
780 ladder: None,
781 ladder_unavailable: None,
782 highlights: Vec::new(),
783 orders: &[],
784 positions: &[],
785 trades: &[],
786 session_open: None,
787 last_fill: None,
788 logo: None,
789 };
790 let ticks = |value| {
791 (
792 true,
793 Bracket {
794 mode: BracketMode::Ticks,
795 value,
796 },
797 )
798 };
799 let mut ticket = OrderTicket {
801 symbol: "ES".into(),
802 kind: Kind::Limit,
803 price: 99.0,
804 sizing: Sizing::Risk,
805 amount: 1.0,
806 take_profit: ticks(16.0),
807 stop_loss: ticks(8.0),
808 ..OrderTicket::default()
809 };
810 let request = ticket.request(&market).unwrap();
811 assert_eq!(
812 (request.quantity, request.take_profit, request.stop_loss),
813 (50.0, Some(103.0), Some(97.0))
814 );
815 let ctx = egui::Context::default();
817 let output = ctx.run_ui(egui::RawInput::default(), |ui| {
818 ticket.show(ui, &market, &mut vec![], &mut false);
819 });
820 let risk = output.shapes.iter().any(|shape| {
821 matches!(&shape.shape, egui::Shape::Text(text) if text.galley.text() == "Risk, % balance")
822 });
823 output.drop_without_applying_deltas();
824 assert!(risk, "limit orders offer the risk control");
825 let order = OrderRequest::new("ES", OrderSide::Sell, 3.0, OrderType::Stop(98.0));
827 let order = ticket.with_brackets(order, &market).unwrap();
828 assert_eq!(
829 (order.take_profit, order.stop_loss),
830 (Some(94.0), Some(100.0))
831 );
832 assert_eq!(
833 confirmation(&order, &market.numbers(), 0),
834 "Sell 3 ES @ 98.00 STOP?\nTake profit 94.00\nStop loss 100.00"
835 );
836 ticket.levels = vec![(ticks(24.0).1, 50.0)];
838 let order = OrderRequest::new("ES", OrderSide::Sell, 3.0, OrderType::Stop(98.0));
839 let order = ticket.with_brackets(order, &market).unwrap();
840 assert_eq!(order.take_profit, None);
841 assert_eq!(
842 order.take_profit_levels,
843 [
844 TakeProfitLevel::new(94.0, 2.0),
845 TakeProfitLevel::new(92.0, 1.0)
846 ]
847 );
848 assert_eq!(
849 confirmation(&order, &market.numbers(), 0),
850 "Sell 3 ES @ 98.00 STOP?\nTake profit 94.00 for 2\nTake profit 92.00 for 1\nStop loss 100.00"
851 );
852 ticket.stop_loss = ticks(0.0);
853 let order = OrderRequest::new("ES", OrderSide::Sell, 3.0, OrderType::Stop(98.0));
854 assert!(ticket.with_brackets(order, &market).is_err());
855 }
856
857 #[test]
864 fn tickets_size_and_place_brackets_by_the_rules() {
865 let mut rules = TradingRules::new("ES");
866 rules.quantity_step = 0.5;
867 rules.min_quantity = 0.5;
868 rules.max_quantity = Some(10.0);
869 rules.point_value = 50.0;
870 let mut capabilities = Capabilities::default();
871 (
872 capabilities.market,
873 capabilities.limit,
874 capabilities.brackets,
875 ) = (true, true, true);
876 let mut market = Market {
877 symbol: "ES",
878 formatter: None,
879 pip_value: None,
880 last: Some(4_000.0),
881 bid: None,
882 ask: None,
883 tick: 0.25,
884 decimals: 2,
885 currency: "USD",
886 balance: Some(100_000.0),
887 capabilities,
888 rules: &rules,
889 rules_ready: true,
890 offline: None,
891 ladder: None,
892 ladder_unavailable: None,
893 highlights: Vec::new(),
894 orders: &[],
895 positions: &[],
896 trades: &[],
897 session_open: None,
898 last_fill: None,
899 logo: None,
900 };
901 let ticks = |value| Bracket {
902 mode: BracketMode::Ticks,
903 value,
904 };
905 let percent = |value| Bracket {
906 mode: BracketMode::Percent,
907 value,
908 };
909 let money = |value| Bracket {
910 mode: BracketMode::Money,
911 value,
912 };
913 let cases = [
915 (
917 OrderSide::Buy,
918 Sizing::Value,
919 1_000_000.0,
920 None,
921 None,
922 Ok((5.0, None, None)),
923 ),
924 (
926 OrderSide::Buy,
927 Sizing::Value,
928 1_100_000.0,
929 None,
930 None,
931 Ok((5.5, None, None)),
932 ),
933 (
935 OrderSide::Sell,
936 Sizing::Value,
937 1_140_000.0,
938 None,
939 None,
940 Ok((5.5, None, None)),
941 ),
942 (
944 OrderSide::Buy,
945 Sizing::Risk,
946 1.0,
947 Some(ticks(16.0)),
948 Some(ticks(8.0)),
949 Ok((10.0, Some(4_004.0), Some(3_998.0))),
950 ),
951 (
953 OrderSide::Sell,
954 Sizing::Risk,
955 1.0,
956 Some(percent(2.0)),
957 Some(percent(1.0)),
958 Ok((0.5, Some(3_920.0), Some(4_040.0))),
959 ),
960 (
962 OrderSide::Sell,
963 Sizing::Risk,
964 0.9,
965 None,
966 Some(percent(1.0)),
967 Err(()),
968 ),
969 (
971 OrderSide::Buy,
972 Sizing::Risk,
973 1.5,
974 None,
975 Some(ticks(4.0)),
976 Err(()),
977 ),
978 (
979 OrderSide::Buy,
980 Sizing::Units,
981 2.0,
982 None,
983 None,
984 Ok((2.0, None, None)),
985 ),
986 (OrderSide::Buy, Sizing::Risk, 1.0, None, None, Err(())),
987 (
988 OrderSide::Buy,
989 Sizing::Units,
990 1.0,
991 None,
992 Some(ticks(0.0)),
993 Err(()),
994 ),
995 (
996 OrderSide::Sell,
997 Sizing::Units,
998 1.0,
999 Some(ticks(0.0)),
1000 None,
1001 Err(()),
1002 ),
1003 (
1006 OrderSide::Buy,
1007 Sizing::Risk,
1008 0.025,
1009 Some(percent(0.014)),
1010 Some(Bracket {
1011 mode: BracketMode::Price,
1012 value: 3999.6,
1013 }),
1014 Ok((1.0, Some(4000.5), Some(3999.5))),
1015 ),
1016 (
1018 OrderSide::Buy,
1019 Sizing::Units,
1020 2.0,
1021 Some(money(500.0)),
1022 Some(money(250.0)),
1023 Ok((2.0, Some(4_005.0), Some(3_997.5))),
1024 ),
1025 (
1027 OrderSide::Sell,
1028 Sizing::Risk,
1029 1.0,
1030 None,
1031 Some(money(250.0)),
1032 Err(()),
1033 ),
1034 ];
1035 for (side, sizing, amount, take_profit, stop_loss, expected) in cases {
1036 let ticket = |take_profit: Option<Bracket>, stop_loss: Option<Bracket>| OrderTicket {
1037 symbol: "ES".into(),
1038 side,
1039 sizing,
1040 quantity: amount,
1041 amount,
1042 take_profit: (take_profit.is_some(), take_profit.unwrap_or(ticks(1.0))),
1043 stop_loss: (stop_loss.is_some(), stop_loss.unwrap_or(ticks(1.0))),
1044 ..OrderTicket::default()
1045 };
1046 let actual = ticket(take_profit, stop_loss)
1047 .request(&market)
1048 .map(|r| (r.quantity, r.take_profit, r.stop_loss))
1049 .map_err(|_| ());
1050 assert_eq!(actual, expected, "{side:?} {sizing:?} {amount}");
1051 let Ok((quantity, Some(take_profit), Some(stop_loss))) = expected else {
1052 continue;
1053 };
1054 if sizing == Sizing::Risk {
1055 continue;
1056 }
1057 for mode in BracketMode::ALL {
1058 let at = |price, profit| {
1059 let per_point = quantity * rules.point_value;
1060 Bracket::at(mode, price, side, 4_000.0, 0.25, per_point, profit)
1061 };
1062 let actual = ticket(Some(at(take_profit, true)), Some(at(stop_loss, false)))
1063 .request(&market)
1064 .map(|r| (r.quantity, r.take_profit, r.stop_loss));
1065 assert_eq!(actual, Ok(expected.unwrap()), "{side:?} {mode:?}");
1066 }
1067 }
1068 let level = TakeProfitLevel::new;
1073 let cases = [
1074 (
1076 OrderSide::Buy,
1077 Sizing::Units,
1078 2.0,
1079 ticks(4.0),
1080 None,
1081 vec![(ticks(8.0), 50.0)],
1082 true,
1083 Ok((None, vec![level(4_001.0, 1.0), level(4_002.0, 1.0)], 150.0)),
1084 ),
1085 (
1087 OrderSide::Buy,
1088 Sizing::Units,
1089 1.5,
1090 ticks(4.0),
1091 None,
1092 vec![(ticks(8.0), 50.0)],
1093 true,
1094 Ok((None, vec![level(4_001.0, 1.0), level(4_002.0, 0.5)], 100.0)),
1095 ),
1096 (
1098 OrderSide::Buy,
1099 Sizing::Units,
1100 1.0,
1101 ticks(4.0),
1102 None,
1103 vec![(ticks(8.0), 30.0)],
1104 true,
1105 Err(()),
1106 ),
1107 (
1109 OrderSide::Buy,
1110 Sizing::Units,
1111 2.0,
1112 ticks(4.0),
1113 None,
1114 vec![(ticks(8.0), 50.0), (ticks(12.0), 50.0)],
1115 true,
1116 Err(()),
1117 ),
1118 (
1119 OrderSide::Buy,
1120 Sizing::Units,
1121 2.0,
1122 ticks(4.0),
1123 None,
1124 vec![(ticks(8.0), f64::NAN)],
1125 true,
1126 Err(()),
1127 ),
1128 (
1129 OrderSide::Buy,
1130 Sizing::Units,
1131 2.0,
1132 ticks(4.0),
1133 None,
1134 vec![(ticks(8.0), 0.0)],
1135 true,
1136 Err(()),
1137 ),
1138 (
1140 OrderSide::Sell,
1141 Sizing::Units,
1142 3.0,
1143 money(150.0),
1144 None,
1145 vec![(money(300.0), 50.0)],
1146 true,
1147 Ok((None, vec![level(3_998.0, 1.5), level(3_996.0, 1.5)], 450.0)),
1148 ),
1149 (
1151 OrderSide::Buy,
1152 Sizing::Units,
1153 2.0,
1154 ticks(4.0),
1155 None,
1156 vec![(ticks(8.0), 50.0)],
1157 false,
1158 Ok((Some(4_001.0), vec![], 100.0)),
1159 ),
1160 (
1162 OrderSide::Buy,
1163 Sizing::Risk,
1164 1.0,
1165 ticks(16.0),
1166 Some(ticks(8.0)),
1167 vec![(ticks(24.0), 25.0)],
1168 true,
1169 Ok((
1170 None,
1171 vec![level(4_004.0, 7.5), level(4_006.0, 2.5)],
1172 2_250.0,
1173 )),
1174 ),
1175 ];
1176 for (side, sizing, amount, take_profit, stop_loss, levels, accepted, expected) in cases {
1177 market.capabilities.take_profit_levels = accepted;
1178 let ticket = OrderTicket {
1179 symbol: "ES".into(),
1180 side,
1181 sizing,
1182 quantity: amount,
1183 amount,
1184 take_profit: (true, take_profit),
1185 levels,
1186 stop_loss: (stop_loss.is_some(), stop_loss.unwrap_or(ticks(1.0))),
1187 ..OrderTicket::default()
1188 };
1189 let request = ticket.request(&market);
1190 let actual = request.as_ref().map_err(|_| ()).map(|request| {
1191 (
1192 request.take_profit,
1193 request.take_profit_levels.clone(),
1194 reward(request, 4_000.0).unwrap() * rules.point_value,
1195 )
1196 });
1197 assert_eq!(actual, expected, "{side:?} {sizing:?} {amount}");
1198 if let Ok(request) = request {
1199 let mut reopened = OrderTicket::default();
1200 reopened.open(&request);
1201 assert_eq!(reopened.request(&market), Ok(request));
1202 }
1203 }
1204 let quoted = Market {
1206 bid: Some(3_999.75),
1207 ask: Some(4_000.25),
1208 ..market
1209 };
1210 for (side, take_profit) in [(OrderSide::Buy, 4_001.25), (OrderSide::Sell, 3_998.75)] {
1211 let ticket = OrderTicket {
1212 symbol: "ES".into(),
1213 side,
1214 take_profit: (true, ticks(4.0)),
1215 ..OrderTicket::default()
1216 };
1217 let request = ticket.request("ed).unwrap();
1218 assert_eq!(request.take_profit, Some(take_profit), "{side:?}");
1219 }
1220 }
1221}