Skip to main content

sandbox_quant/app/
runtime.rs

1use crate::app::commands::AppCommand;
2use crate::app::bootstrap::AppBootstrap;
3use crate::execution::command::ExecutionCommand;
4use crate::storage::event_log::log;
5use serde_json::json;
6
7#[derive(Debug, Default)]
8pub struct AppRuntime {
9    pub last_command: Option<AppCommand>,
10}
11
12impl AppRuntime {
13    pub fn record_command(&mut self, command: AppCommand) {
14        self.last_command = Some(command);
15    }
16
17    pub fn run<E: crate::exchange::facade::ExchangeFacade<Error = crate::error::exchange_error::ExchangeError>>(
18        &mut self,
19        app: &mut AppBootstrap<E>,
20        command: AppCommand,
21    ) -> Result<(), crate::error::app_error::AppError> {
22        self.record_command(command.clone());
23
24        match command {
25            AppCommand::Execution(command) => {
26                let report = app
27                    .portfolio_sync
28                    .refresh_authoritative(&app.exchange, &mut app.portfolio_store)?;
29                log(
30                    &mut app.event_log,
31                    "app.portfolio.refreshed",
32                    json!({
33                        "positions": report.positions,
34                        "open_order_groups": report.open_order_groups,
35                        "balances": report.balances,
36                    }),
37                );
38
39                if let ExecutionCommand::SetTargetExposure { instrument, .. } = &command {
40                    let market = app
41                        .portfolio_store
42                        .snapshot
43                        .positions
44                        .get(instrument)
45                        .map(|position| position.market)
46                        .or_else(|| {
47                            if app
48                                .exchange
49                                .load_symbol_rules(instrument, crate::domain::market::Market::Futures)
50                                .is_ok()
51                            {
52                                Some(crate::domain::market::Market::Futures)
53                            } else if app
54                                .exchange
55                                .load_symbol_rules(instrument, crate::domain::market::Market::Spot)
56                                .is_ok()
57                            {
58                                Some(crate::domain::market::Market::Spot)
59                            } else {
60                                None
61                            }
62                        });
63
64                    if let Some(market) = market {
65                        let price = app.market_data.refresh_price(
66                            &app.exchange,
67                            &mut app.price_store,
68                            instrument.clone(),
69                            market,
70                        )?;
71                        log(
72                            &mut app.event_log,
73                            "app.market_data.price_refreshed",
74                            json!({
75                                "instrument": instrument.0,
76                                "market": format!("{market:?}"),
77                                "price": price,
78                            }),
79                        );
80                    }
81                }
82                let outcome = app.execution.execute(
83                    &app.exchange,
84                    &app.portfolio_store,
85                    &app.price_store,
86                    command.clone(),
87                )?;
88                log(
89                    &mut app.event_log,
90                    "app.execution.completed",
91                    execution_payload(&command, &outcome),
92                );
93            }
94            AppCommand::RefreshAuthoritativeState => {
95                let report = app
96                    .portfolio_sync
97                    .refresh_authoritative(&app.exchange, &mut app.portfolio_store)?;
98                log(
99                    &mut app.event_log,
100                    "app.portfolio.refreshed",
101                    json!({
102                        "positions": report.positions,
103                        "open_order_groups": report.open_order_groups,
104                        "balances": report.balances,
105                    }),
106                );
107            }
108        }
109
110        Ok(())
111    }
112}
113
114fn execution_payload(
115    command: &ExecutionCommand,
116    outcome: &crate::execution::service::ExecutionOutcome,
117) -> serde_json::Value {
118    match (command, outcome) {
119        (
120            ExecutionCommand::SetTargetExposure {
121                instrument, target, ..
122            },
123            crate::execution::service::ExecutionOutcome::TargetExposureSubmitted { .. },
124        ) => json!({
125            "command_kind": "set_target_exposure",
126            "instrument": instrument.0,
127            "target": target.value(),
128            "outcome_kind": "submitted",
129        }),
130        (
131            ExecutionCommand::CloseSymbol { instrument, .. },
132            crate::execution::service::ExecutionOutcome::CloseSymbol(result),
133        ) => json!({
134            "command_kind": "close_symbol",
135            "instrument": instrument.0,
136            "outcome_kind": format!("{:?}", result.result),
137        }),
138        (
139            ExecutionCommand::CloseAll { .. },
140            crate::execution::service::ExecutionOutcome::CloseAll(result),
141        ) => {
142            let submitted = result
143                .results
144                .iter()
145                .filter(|item| matches!(item.result, crate::execution::close_symbol::CloseSubmitResult::Submitted))
146                .count();
147            let skipped = result
148                .results
149                .iter()
150                .filter(|item| matches!(item.result, crate::execution::close_symbol::CloseSubmitResult::SkippedNoPosition))
151                .count();
152            let rejected = result
153                .results
154                .iter()
155                .filter(|item| matches!(item.result, crate::execution::close_symbol::CloseSubmitResult::Rejected))
156                .count();
157            json!({
158                "command_kind": "close_all",
159                "batch_id": result.batch_id.0,
160                "submitted": submitted,
161                "skipped": skipped,
162                "rejected": rejected,
163                "outcome_kind": "batch_completed",
164            })
165        }
166        _ => json!({
167            "command_kind": "unknown",
168            "outcome_kind": "unknown",
169        }),
170    }
171}