1use crate::PositionType;
2use debot_db::CandlePattern;
3use debot_utils::get_local_time;
4use rust_decimal::{prelude::Signed, Decimal};
5use serde::{Deserialize, Serialize};
6use std::{cell::RefCell, fmt};
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum ReasonForClose {
10 Liquidated,
11 Expired,
12 TakeProfit,
13 CutLoss,
14 Other(String),
15}
16
17impl fmt::Display for ReasonForClose {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 ReasonForClose::Liquidated => write!(f, "Liquidated"),
21 ReasonForClose::Expired => write!(f, "Expired"),
22 ReasonForClose::TakeProfit => write!(f, "TakeProfit"),
23 ReasonForClose::CutLoss => write!(f, "CutLoss"),
24 ReasonForClose::Other(s) => write!(f, "{}", s),
25 }
26 }
27}
28
29#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
30pub enum PositionState {
31 #[default]
32 Ready,
33 Open,
34 Closing(String),
35 Closed(String),
36}
37
38impl fmt::Display for PositionState {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 PositionState::Ready => write!(f, "Ready"),
42 PositionState::Open => write!(f, "Open"),
43 PositionState::Closing(reason) => write!(f, "Closing({})", reason),
44 PositionState::Closed(reason) => write!(f, "Closed({})", reason),
45 }
46 }
47}
48
49#[derive(Serialize, Deserialize, Clone, Debug, Default)]
50pub struct Position {
51 id: u32,
52 fund_name: String,
53 state: PositionState,
54 token_name: String,
55 tick_count: u32,
56 actual_entry_tick: u32,
57 actual_hold_tick: u32,
58 max_holding_tick_count: u32,
59 exit_timeout_tick_count: u32,
60 open_time_str: String,
61 open_timestamp: i64,
62 close_time_str: String,
63 average_open_price: Decimal,
64 position_type: PositionType,
65 target_price: Decimal,
66 take_profit_price: Option<Decimal>,
67 cut_loss_price: Option<Decimal>,
68 close_price: Decimal,
69 close_asset_in_usd: Decimal,
70 amount: Decimal,
71 asset_in_usd: Decimal,
72 pnl: Decimal,
73 fee: Decimal,
74 trailing_peak_price: RefCell<Option<Decimal>>,
75 atr: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
77 adx: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
78 rsi: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
79 stochastic: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
80 price: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
81 candle_pattern: (
82 CandlePattern,
83 CandlePattern,
84 CandlePattern,
85 CandlePattern,
86 CandlePattern,
87 CandlePattern,
88 ),
89 take_profit_ratio: Decimal,
90 atr_spread: Decimal,
91 risk_reward: Decimal,
92 atr_term: Decimal,
93 tick_spread: i64,
94 bias_ticks: i64,
95 last_volume: Option<Decimal>,
96 last_num_trades: Option<u64>,
97 last_funding_rate: Option<Decimal>,
98 last_open_interest: Option<Decimal>,
99 last_oracle_price: Option<Decimal>,
100 volume_change_ratio: Decimal,
101 pid_proportional: Decimal,
102 pid_integral: Decimal,
103 pid_derivative: Decimal,
104 pid_error_mean: Decimal,
105}
106
107#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
108pub enum OrderState {
109 #[default]
110 Open,
111 Filled,
112}
113
114impl fmt::Display for OrderState {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 OrderState::Open => write!(f, "Open"),
118 OrderState::Filled => write!(f, "Filled"),
119 }
120 }
121}
122
123#[derive(Serialize, Deserialize, Clone, Debug, Default)]
124pub struct Order {
125 id: String,
126 unfilled_amount: Decimal,
127 state: OrderState,
128 tick_count: u32,
129 entry_timeout_tick_count: u32,
130}
131
132enum UpdateResult {
133 Closed,
134 Decreased,
135 Inverted,
136}
137
138pub enum OrderType {
139 OpenOrder,
140 CloseOrder,
141}
142
143impl Position {
144 pub fn new(
145 id: u32,
146 fund_name: &str,
147 exit_timeout_tick_count: u32,
148 max_holding_tick_count: u32,
149 token_name: &str,
150 position_type: PositionType,
151 target_price: Decimal,
152 atr: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
153 adx: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
154 rsi: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
155 stochastic: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
156 price: (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal),
157 candle_pattern: (
158 CandlePattern,
159 CandlePattern,
160 CandlePattern,
161 CandlePattern,
162 CandlePattern,
163 CandlePattern,
164 ),
165 take_profit_ratio: Decimal,
166 atr_spread: Decimal,
167 risk_reward: Decimal,
168 atr_term: Decimal,
169 tick_spread: i64,
170 bias_ticks: i64,
171 last_volume: Option<Decimal>,
172 last_num_trades: Option<u64>,
173 last_funding_rate: Option<Decimal>,
174 last_open_interest: Option<Decimal>,
175 last_oracle_price: Option<Decimal>,
176 volume_change_ratio: Decimal,
177 pid_proportional: Decimal,
178 pid_integral: Decimal,
179 pid_derivative: Decimal,
180 pid_error_mean: Decimal,
181 ) -> Self {
182 let decimal_0 = Decimal::new(0, 0);
183 Self {
184 id,
185 fund_name: fund_name.to_owned(),
186 tick_count: 0,
187 actual_entry_tick: 0,
188 actual_hold_tick: 0,
189 max_holding_tick_count,
190 exit_timeout_tick_count,
191 state: PositionState::Ready,
192 token_name: token_name.to_owned(),
193 open_time_str: String::new(),
194 open_timestamp: 0,
195 close_time_str: String::new(),
196 average_open_price: decimal_0,
197 position_type,
198 target_price,
199 take_profit_price: None,
200 cut_loss_price: None,
201 close_price: decimal_0,
202 close_asset_in_usd: decimal_0,
203 amount: decimal_0,
204 asset_in_usd: decimal_0,
205 pnl: decimal_0,
206 fee: decimal_0,
207 trailing_peak_price: None.into(),
208 atr,
209 adx,
210 rsi,
211 price,
212 candle_pattern,
213 take_profit_ratio,
214 stochastic,
215 atr_spread,
216 risk_reward,
217 atr_term,
218 tick_spread,
219 bias_ticks,
220 last_volume,
221 last_num_trades,
222 last_funding_rate,
223 last_open_interest,
224 last_oracle_price,
225 volume_change_ratio,
226 pid_proportional,
227 pid_integral,
228 pid_derivative,
229 pid_error_mean,
230 }
231 }
232
233 pub fn on_filled(
234 &mut self,
235 position_type: PositionType,
236 filled_price: Decimal,
237 amount: Decimal,
238 asset_in_usd: Decimal,
239 fee: Decimal,
240 take_profit_price: Option<Decimal>,
241 cut_loss_price: Option<Decimal>,
242 current_price: Decimal,
243 ) -> Result<(), ()> {
244 if matches!(self.state, PositionState::Closed(_)) {
245 log::error!("on_filled: Invalid position state: {:?}", self);
246 return Err(());
247 }
248
249 log::trace!("state = {}, amount = {}", self.state, amount);
250
251 self.fee += fee;
252
253 if self.state == PositionState::Ready {
254 self.position_type = position_type.clone();
255 }
256
257 if self.position_type == position_type {
258 self.increase(
259 position_type,
260 filled_price,
261 take_profit_price,
262 cut_loss_price,
263 amount,
264 asset_in_usd,
265 current_price,
266 );
267 } else {
268 self.decrease(
269 position_type,
270 filled_price,
271 take_profit_price,
272 cut_loss_price,
273 amount,
274 asset_in_usd,
275 current_price,
276 );
277 }
278
279 return Ok(());
280 }
281
282 pub fn on_liquidated(
283 &mut self,
284 close_price: Decimal,
285 fee: Decimal,
286 do_liquidate: bool,
287 liquidated_reason: Option<String>,
288 ) -> Result<(), ()> {
289 self.fee += fee;
290
291 let reason = if do_liquidate {
292 match liquidated_reason {
293 Some(r) => format!("Liquidated, {}", r),
294 None => String::from("Liquidated"),
295 }
296 } else {
297 match self.state.clone() {
298 PositionState::Closing(reason) => reason,
299 _ => {
300 log::error!("delete: Invalid PositionState: {}", self.state);
301 return Err(());
302 }
303 }
304 };
305
306 self.delete(close_price, &reason);
307
308 return Ok(());
309 }
310
311 pub fn request_close(&mut self, reason: &str) -> Result<(), ()> {
312 if !matches!(self.state, PositionState::Open) {
313 log::error!("request_close: Invalid position state: {:?}", self);
314 return Err(());
315 }
316
317 self.update_state(PositionState::Closing(reason.to_owned()));
318
319 return Ok(());
320 }
321
322 fn increase(
323 &mut self,
324 position_type: PositionType,
325 filled_price: Decimal,
326 take_profit_price: Option<Decimal>,
327 cut_loss_price: Option<Decimal>,
328 amount: Decimal,
329 asset_in_usd: Decimal,
330 current_price: Decimal,
331 ) {
332 let current_amount = self.amount.abs();
333
334 self.average_open_price = (self.average_open_price * current_amount
335 + filled_price * amount)
336 / (current_amount + amount);
337
338 self.take_profit_price = match take_profit_price {
339 Some(new_price) => match self.take_profit_price {
340 Some(current_price) => Some(
341 (current_price * current_amount + new_price * amount)
342 / (current_amount + amount),
343 ),
344 None => Some(new_price),
345 },
346 None => None,
347 };
348
349 self.cut_loss_price = match cut_loss_price {
350 Some(new_price) => match self.cut_loss_price {
351 Some(current_price) => Some(
352 (current_price * current_amount + new_price * amount)
353 / (current_amount + amount),
354 ),
355 None => Some(new_price),
356 },
357 None => None,
358 };
359
360 self.update_amount(position_type, amount, asset_in_usd);
361 self.update_state(PositionState::Open);
362
363 log::info!(
364 "+ Increase the position: {}",
365 self.format_position(current_price)
366 );
367 }
368
369 fn decrease(
370 &mut self,
371 position_type: PositionType,
372 filled_price: Decimal,
373 take_profit_price: Option<Decimal>,
374 cut_loss_price: Option<Decimal>,
375 amount: Decimal,
376 asset_in_usd: Decimal,
377 current_price: Decimal,
378 ) {
379 self.close_asset_in_usd += asset_in_usd;
380
381 match self.update_amount_and_pnl(position_type, amount, asset_in_usd, filled_price) {
382 UpdateResult::Closed => {
383 let reason = if self.pnl > Decimal::ZERO {
384 "TakeProfit"
385 } else {
386 "CutLoss"
387 };
388 self.delete(filled_price, reason);
389 return;
390 }
391 UpdateResult::Inverted => {
392 self.average_open_price = filled_price;
393 self.take_profit_price = take_profit_price;
394 self.cut_loss_price = cut_loss_price;
395 self.position_type = self.position_type.opposite();
396 log::info!(
397 "- The position is inverted: {}",
398 self.format_position(filled_price)
399 );
400 }
401 UpdateResult::Decreased => {
402 log::info!(
403 "** The position is decreased: {}",
404 self.format_position(current_price)
405 );
406 }
407 }
408 }
409
410 fn delete(&mut self, close_price: Decimal, reason: &str) {
411 if let PositionState::Closing(closing_reason) = self.state.clone() {
412 self.update_state(PositionState::Closed(closing_reason));
413 } else {
414 self.update_state(PositionState::Closed(reason.to_owned()));
415 }
416
417 let close_amount = self.amount;
418 self.close_price = close_price;
419 self.pnl += Self::unrealized_pnl(close_price, self.amount, self.asset_in_usd);
420 self.pnl -= self.fee;
421 self.amount = Decimal::new(0, 0);
422 self.asset_in_usd = Decimal::new(0, 0);
423
424 log::info!(
425 "-- Close the position[{}][{}]: {}, amount: {:.3}, pnl: {:.3?}",
426 self.id,
427 self.position_type,
428 self.state,
429 close_amount,
430 self.pnl
431 );
432 }
433
434 fn update_state(&mut self, new_state: PositionState) {
435 match new_state {
436 PositionState::Closing(_) => {
437 self.actual_hold_tick = self.tick_count;
438 self.tick_count = 0;
439 }
440 PositionState::Open => match self.state {
441 PositionState::Ready => {
442 self.actual_entry_tick = self.tick_count;
443 self.tick_count = 0;
444 self.set_open_time();
445 }
446 PositionState::Closing(_) => {
447 return;
448 }
449 _ => {}
450 },
451 PositionState::Closed(_) => {
452 self.set_close_time();
453 }
454 _ => {}
455 }
456
457 self.state = new_state
458 }
459
460 fn update_amount_and_pnl(
461 &mut self,
462 position_type: PositionType,
463 amount: Decimal,
464 asset_in_usd: Decimal,
465 close_price: Decimal,
466 ) -> UpdateResult {
467 let prev_asset_in_usd = self.asset_in_usd;
468 let prev_amount = self.amount;
469
470 self.update_amount(position_type, amount, asset_in_usd);
471
472 let update_result = if self.amount.is_zero() {
473 UpdateResult::Closed
474 } else if prev_amount.signum() != self.amount.signum() {
475 UpdateResult::Inverted
476 } else {
477 UpdateResult::Decreased
478 };
479
480 let pnl = self.calculate_pnl_for_update(
481 &update_result,
482 prev_amount,
483 close_price,
484 prev_asset_in_usd,
485 );
486 self.realize_pnl(pnl);
487
488 update_result
489 }
490
491 fn calculate_pnl_for_update(
492 &self,
493 update_result: &UpdateResult,
494 prev_amount: Decimal,
495 close_price: Decimal,
496 prev_asset_in_usd: Decimal,
497 ) -> Decimal {
498 match update_result {
499 UpdateResult::Decreased => {
500 (close_price - self.average_open_price) * (prev_amount - self.amount)
501 }
502 _ => Self::unrealized_pnl(close_price, prev_amount, prev_asset_in_usd),
503 }
504 }
505
506 fn update_amount(
507 &mut self,
508 position_type: PositionType,
509 amount: Decimal,
510 asset_in_usd: Decimal,
511 ) {
512 if position_type == PositionType::Long {
513 self.amount += amount;
514 self.asset_in_usd -= asset_in_usd;
515 } else {
516 self.amount -= amount;
517 self.asset_in_usd += asset_in_usd;
518 }
519 }
520
521 fn realize_pnl(&mut self, pnl: Decimal) {
522 self.pnl += pnl;
523 self.asset_in_usd -= pnl;
524 }
525
526 fn unrealized_pnl(price: Decimal, amount: Decimal, asset_in_usd: Decimal) -> Decimal {
527 amount * price + asset_in_usd
528 }
529
530 pub fn update_counter(&mut self) {
531 self.tick_count += 1;
532 }
533
534 pub fn should_close(&self, close_price: Decimal, use_trailing: bool) -> Option<ReasonForClose> {
535 if self.should_take_profit(close_price, use_trailing) {
536 return Some(ReasonForClose::TakeProfit);
537 }
538
539 if self.should_cut_loss(close_price) {
540 Some(ReasonForClose::CutLoss)
541 } else {
542 None
543 }
544 }
545
546 pub fn pnl(&self) -> (Decimal, Decimal) {
547 if self.close_asset_in_usd.is_zero() {
548 (self.pnl, Decimal::ZERO)
549 } else {
550 (self.pnl, self.pnl / self.close_asset_in_usd.abs())
551 }
552 }
553
554 pub fn notional(&self) -> Decimal {
555 self.amount.abs() * self.average_open_price
556 }
557
558 pub fn unrealized_pnl_at(&self, price: Decimal) -> Decimal {
559 Self::unrealized_pnl(price, self.amount, self.asset_in_usd)
560 }
561
562 pub fn unrealized_roe_at(&self, price: Decimal, leverage: u32) -> Option<Decimal> {
563 let denom = self.notional() / Decimal::from(leverage);
564 if denom.is_zero() {
565 None
566 } else {
567 Some(self.unrealized_pnl_at(price) / denom)
568 }
569 }
570
571 pub fn id(&self) -> u32 {
572 self.id
573 }
574
575 pub fn fund_name(&self) -> &str {
576 &self.fund_name
577 }
578
579 pub fn average_open_price(&self) -> Decimal {
580 self.average_open_price
581 }
582
583 pub fn target_price(&self) -> Decimal {
584 self.target_price
585 }
586
587 pub fn state(&self) -> PositionState {
588 self.state.clone()
589 }
590
591 pub fn token_name(&self) -> &str {
592 &self.token_name
593 }
594
595 pub fn amount(&self) -> Decimal {
596 self.amount
597 }
598
599 pub fn position_type(&self) -> PositionType {
600 self.position_type.clone()
601 }
602
603 pub fn asset_in_usd(&self) -> Decimal {
604 self.asset_in_usd
605 }
606
607 pub fn close_asset_in_usd(&self) -> Decimal {
608 self.close_asset_in_usd
609 }
610
611 pub fn open_timestamp(&self) -> i64 {
612 self.open_timestamp
613 }
614
615 pub fn open_time_str(&self) -> &str {
616 &self.open_time_str
617 }
618
619 pub fn close_time_str(&self) -> &str {
620 &self.close_time_str
621 }
622
623 pub fn close_price(&self) -> Decimal {
624 self.close_price
625 }
626
627 pub fn last_volume(&self) -> Option<Decimal> {
628 self.last_volume
629 }
630
631 pub fn last_num_trades(&self) -> Option<u64> {
632 self.last_num_trades
633 }
634
635 pub fn last_funding_rate(&self) -> Option<Decimal> {
636 self.last_funding_rate
637 }
638
639 pub fn last_open_interest(&self) -> Option<Decimal> {
640 self.last_open_interest
641 }
642
643 pub fn last_oracle_price(&self) -> Option<Decimal> {
644 self.last_oracle_price
645 }
646
647 pub fn rsi(&self) -> (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal) {
648 self.rsi
649 }
650
651 pub fn atr(&self) -> (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal) {
652 self.atr
653 }
654
655 pub fn adx(&self) -> (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal) {
656 self.adx
657 }
658
659 pub fn stochastic(&self) -> (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal) {
660 self.stochastic
661 }
662
663 pub fn price(&self) -> (Decimal, Decimal, Decimal, Decimal, Decimal, Decimal) {
664 self.price
665 }
666
667 pub fn candle_pattern(
668 &self,
669 ) -> (
670 CandlePattern,
671 CandlePattern,
672 CandlePattern,
673 CandlePattern,
674 CandlePattern,
675 CandlePattern,
676 ) {
677 self.candle_pattern
678 }
679
680 pub fn take_profit_ratio(&self) -> Decimal {
681 self.take_profit_ratio
682 }
683
684 pub fn atr_spread(&self) -> Decimal {
685 self.atr_spread
686 }
687
688 pub fn risk_reward(&self) -> Decimal {
689 self.risk_reward
690 }
691
692 pub fn atr_term(&self) -> Decimal {
693 self.atr_term
694 }
695
696 pub fn fee(&self) -> Decimal {
697 self.fee
698 }
699
700 pub fn actual_entry_tick(&self) -> u32 {
701 self.actual_entry_tick
702 }
703
704 pub fn actual_hold_tick(&self) -> u32 {
705 self.actual_hold_tick
706 }
707
708 pub fn tick_spread(&self) -> i64 {
709 self.tick_spread
710 }
711
712 pub fn bias_ticks(&self) -> i64 {
713 self.bias_ticks
714 }
715
716 pub fn volume_change_ratio(&self) -> Decimal {
717 self.volume_change_ratio
718 }
719
720 pub fn pid_proportional(&self) -> Decimal {
721 self.pid_proportional
722 }
723
724 pub fn pid_integral(&self) -> Decimal {
725 self.pid_integral
726 }
727
728 pub fn pid_derivative(&self) -> Decimal {
729 self.pid_derivative
730 }
731
732 pub fn pid_error_mean(&self) -> Decimal {
733 self.pid_error_mean
734 }
735
736 pub fn should_open_expired(&self, close_price: Decimal) -> bool {
737 if matches!(self.state, PositionState::Open) {
738 self.tick_count > self.max_holding_tick_count
739 && !self.has_reached_take_profit(close_price)
740 } else {
741 false
742 }
743 }
744
745 pub fn take_profit_price(&self) -> Option<Decimal> {
746 self.take_profit_price
747 }
748
749 pub fn cut_loss_price(&self) -> Option<Decimal> {
750 self.cut_loss_price
751 }
752
753 fn is_trailing_stop_triggered(&self, close_price: Decimal) -> bool {
754 let open_price = self.average_open_price;
755
756 let Some(tp_price) = self.take_profit_price else {
757 return false;
758 };
759
760 let expected_profit = match self.position_type {
761 PositionType::Long => tp_price - open_price,
762 PositionType::Short => open_price - tp_price,
763 };
764
765 let trailing_stop_ratio = expected_profit / open_price * Decimal::new(5, 1);
766
767 match self.position_type {
768 PositionType::Long => {
769 if let Some(peak) = *self.trailing_peak_price.borrow() {
770 let stop_price = peak * (Decimal::ONE - trailing_stop_ratio);
771 return close_price <= stop_price && close_price > open_price;
772 }
773 }
774 PositionType::Short => {
775 if let Some(trough) = *self.trailing_peak_price.borrow() {
776 let stop_price = trough * (Decimal::ONE + trailing_stop_ratio);
777 return close_price >= stop_price && close_price < open_price;
778 }
779 }
780 }
781
782 false
783 }
784
785 pub fn should_take_profit(&self, close_price: Decimal, use_trailing: bool) -> bool {
786 if !matches!(self.state, PositionState::Open) {
787 return false;
788 }
789
790 let open_price = self.average_open_price;
791 let mut reached_tp = false;
792
793 if let Some(tp_price) = self.take_profit_price {
794 match self.position_type {
795 PositionType::Long => {
796 if close_price >= tp_price {
797 reached_tp = true;
798 let mut peak = self.trailing_peak_price.borrow_mut();
799 let current_peak = peak.get_or_insert(close_price.max(open_price));
800 if close_price > *current_peak {
801 *current_peak = close_price;
802 }
803 }
804 }
805 PositionType::Short => {
806 if close_price <= tp_price {
807 reached_tp = true;
808 let mut trough = self.trailing_peak_price.borrow_mut();
809 let current_trough = trough.get_or_insert(close_price.min(open_price));
810 if close_price < *current_trough {
811 *current_trough = close_price;
812 }
813 }
814 }
815 }
816 } else {
817 return false;
818 }
819
820 if !use_trailing {
821 return reached_tp;
822 }
823
824 let triggered = self.is_trailing_stop_triggered(close_price);
825
826 match self.position_type {
827 PositionType::Long => {
828 if let Some(peak) = *self.trailing_peak_price.borrow() {
829 let expected = self.take_profit_price.unwrap() - open_price;
830 let ratio = expected / open_price * Decimal::new(5, 1);
831 let stop = peak * (Decimal::ONE - ratio);
832 log::warn!(
833 "Trailing Stop [Long][{}]: {} - price: {:.2}, open: {:.2}, peak: {:.2}, stop: {:.2}, ratio: {:.4}",
834 self.id, triggered, close_price, open_price, peak, stop, ratio
835 );
836 }
837 }
838 PositionType::Short => {
839 if let Some(trough) = *self.trailing_peak_price.borrow() {
840 let expected = open_price - self.take_profit_price.unwrap();
841 let ratio = expected / open_price * Decimal::new(5, 1);
842 let stop = trough * (Decimal::ONE + ratio);
843 log::warn!(
844 "Trailing Stop [Short][{}]: {} - price: {:.2}, open: {:.2}, trough: {:.2}, stop: {:.2}, ratio: {:.4}",
845 self.id, triggered, close_price, open_price, trough, stop, ratio
846 );
847 }
848 }
849 }
850
851 triggered
852 }
853
854 fn has_reached_take_profit(&self, close_price: Decimal) -> bool {
855 match self.position_type {
856 PositionType::Long => {
857 if let Some(tp) = self.take_profit_price {
858 if close_price >= tp {
859 return true;
860 }
861 }
862 }
863 PositionType::Short => {
864 if let Some(tp) = self.take_profit_price {
865 if close_price <= tp {
866 return true;
867 }
868 }
869 }
870 }
871
872 self.is_trailing_stop_triggered(close_price)
874 }
875
876 fn should_cut_loss(&self, close_price: Decimal) -> bool {
877 if !matches!(self.state, PositionState::Open) {
878 return false;
879 }
880
881 match self.cut_loss_price {
882 Some(cut_loss_price) => {
883 if self.position_type == PositionType::Long {
884 close_price <= cut_loss_price
885 } else {
886 close_price >= cut_loss_price
887 }
888 }
889 None => false,
890 }
891 }
892
893 pub fn should_cancel_closing(&self) -> bool {
894 match self.state {
895 PositionState::Closing(_) => self.tick_count > self.exit_timeout_tick_count,
896 _ => false,
897 }
898 }
899
900 pub fn cancel_closing(&mut self) {
901 if !matches!(self.state, PositionState::Closing(_)) {
902 log::warn!("cancel_closing: invalid state: {:?}", self);
903 }
904 self.state = PositionState::Open;
905 }
906
907 fn set_open_time(&mut self) {
908 let (timestamp, time_str) = get_local_time();
909 self.open_timestamp = timestamp;
910 self.open_time_str = time_str;
911 }
912
913 fn set_close_time(&mut self) {
914 let (_, time_str) = get_local_time();
915 self.close_time_str = time_str;
916 }
917
918 fn format_position(&self, current_price: Decimal) -> String {
919 let open_price = self.average_open_price;
920 let take_profit_price = self.take_profit_price.unwrap_or_default();
921 let cut_loss_price = self.cut_loss_price.unwrap_or_default();
922
923 let unrealized_pnl = Self::unrealized_pnl(current_price, self.amount, self.asset_in_usd);
924 let decimal_100 = Decimal::new(100, 0);
925
926 format!(
927 "ID:{} {:<6}({}) tick: {}/{}, un-pnl: {:3.3}({:.2}%), [{}] price: {:>6.5}/{:>6.5}({:.3}%), cut: {:>6.3}, take: {:>6.3}, amount: {:6.6}/{:6.6}",
928 self.id,
929 self.token_name,
930 self.state,
931 self.tick_count,
932 if matches!(self.state, PositionState::Closing(_)) {
933 self.exit_timeout_tick_count
934 } else {
935 self.max_holding_tick_count
936 },
937 unrealized_pnl,
938 unrealized_pnl / self.asset_in_usd.abs() * decimal_100,
939 self.position_type,
940 current_price,
941 open_price,
942 if self.position_type == PositionType::Long {
943 current_price - open_price
944 }
945 else {
946 open_price - current_price
947 } / open_price * decimal_100,
948 cut_loss_price,
949 take_profit_price,
950 self.amount,
951 self.asset_in_usd
952 )
953 }
954
955 pub fn get_info(&self, current_price: Decimal) -> Option<String> {
956 if self.amount.is_zero() {
957 None
958 } else {
959 Some(format!("{}", self.format_position(current_price)))
960 }
961 }
962}
963
964impl Order {
965 pub fn new(id: String, amount: Decimal, entry_timeout_tick_count: u32) -> Order {
966 Self {
967 id,
968 unfilled_amount: amount,
969 state: OrderState::Open,
970 tick_count: 0,
971 entry_timeout_tick_count,
972 }
973 }
974
975 pub fn on_filled(&mut self, amount: Decimal) -> Result<(), ()> {
976 if matches!(self.state, OrderState::Filled) {
977 log::warn!(
978 "The order is filled unexpectedly: id = {}, state = {}, amount = {}",
979 self.id,
980 self.state,
981 amount
982 );
983 return Err(());
984 }
985
986 self.unfilled_amount -= amount;
987 if self.unfilled_amount.is_zero() {
988 self.state = OrderState::Filled;
989 }
990
991 log::info!(
992 "Order filled: id = {}, state = {}, unfilled_amount = {}",
993 self.id,
994 self.state,
995 self.unfilled_amount
996 );
997
998 return Ok(());
999 }
1000
1001 pub fn should_cancel_order(&self) -> bool {
1002 if matches!(self.state, OrderState::Open) {
1003 self.tick_count > self.entry_timeout_tick_count
1004 } else {
1005 false
1006 }
1007 }
1008
1009 pub fn update_counter(&mut self) {
1010 if matches!(self.state, OrderState::Open) {
1011 self.tick_count += 1;
1012 }
1013 }
1014
1015 pub fn id(&self) -> &str {
1016 &self.id
1017 }
1018
1019 pub fn state(&self) -> OrderState {
1020 self.state.clone()
1021 }
1022}