debot_position_manager/
position_manager.rs

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    // for debug
76    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        // Also consider trailing stop trigger
873        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}