finance_query/backtesting/
position.rs1use serde::{Deserialize, Serialize};
4
5use super::signal::Signal;
6
7#[non_exhaustive]
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum PositionSide {
11 Long,
13 Short,
15}
16
17impl std::fmt::Display for PositionSide {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 Self::Long => write!(f, "LONG"),
21 Self::Short => write!(f, "SHORT"),
22 }
23 }
24}
25
26#[non_exhaustive]
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Position {
30 pub side: PositionSide,
32
33 pub entry_timestamp: i64,
35
36 pub entry_price: f64,
38
39 pub quantity: f64,
41
42 pub entry_commission: f64,
44
45 pub entry_signal: Signal,
47}
48
49impl Position {
50 pub fn new(
52 side: PositionSide,
53 entry_timestamp: i64,
54 entry_price: f64,
55 quantity: f64,
56 entry_commission: f64,
57 entry_signal: Signal,
58 ) -> Self {
59 Self {
60 side,
61 entry_timestamp,
62 entry_price,
63 quantity,
64 entry_commission,
65 entry_signal,
66 }
67 }
68
69 pub fn current_value(&self, current_price: f64) -> f64 {
71 self.quantity * current_price
72 }
73
74 pub fn unrealized_pnl(&self, current_price: f64) -> f64 {
76 let gross_pnl = match self.side {
77 PositionSide::Long => (current_price - self.entry_price) * self.quantity,
78 PositionSide::Short => (self.entry_price - current_price) * self.quantity,
79 };
80 gross_pnl - self.entry_commission
81 }
82
83 pub fn unrealized_return_pct(&self, current_price: f64) -> f64 {
85 let entry_value = self.entry_price * self.quantity;
86 if entry_value == 0.0 {
87 return 0.0;
88 }
89 let pnl = self.unrealized_pnl(current_price);
90 (pnl / entry_value) * 100.0
91 }
92
93 pub fn is_profitable(&self, current_price: f64) -> bool {
95 self.unrealized_pnl(current_price) > 0.0
96 }
97
98 pub fn is_long(&self) -> bool {
100 matches!(self.side, PositionSide::Long)
101 }
102
103 pub fn is_short(&self) -> bool {
105 matches!(self.side, PositionSide::Short)
106 }
107
108 pub fn close(
110 self,
111 exit_timestamp: i64,
112 exit_price: f64,
113 exit_commission: f64,
114 exit_signal: Signal,
115 ) -> Trade {
116 let total_commission = self.entry_commission + exit_commission;
117
118 let gross_pnl = match self.side {
119 PositionSide::Long => (exit_price - self.entry_price) * self.quantity,
120 PositionSide::Short => (self.entry_price - exit_price) * self.quantity,
121 };
122 let pnl = gross_pnl - total_commission;
123
124 let entry_value = self.entry_price * self.quantity;
125 let return_pct = if entry_value > 0.0 {
126 (pnl / entry_value) * 100.0
127 } else {
128 0.0
129 };
130
131 Trade {
132 side: self.side,
133 entry_timestamp: self.entry_timestamp,
134 exit_timestamp,
135 entry_price: self.entry_price,
136 exit_price,
137 quantity: self.quantity,
138 commission: total_commission,
139 pnl,
140 return_pct,
141 entry_signal: self.entry_signal,
142 exit_signal,
143 }
144 }
145}
146
147#[non_exhaustive]
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct Trade {
151 pub side: PositionSide,
153
154 pub entry_timestamp: i64,
156
157 pub exit_timestamp: i64,
159
160 pub entry_price: f64,
162
163 pub exit_price: f64,
165
166 pub quantity: f64,
168
169 pub commission: f64,
171
172 pub pnl: f64,
174
175 pub return_pct: f64,
177
178 pub entry_signal: Signal,
180
181 pub exit_signal: Signal,
183}
184
185impl Trade {
186 pub fn is_profitable(&self) -> bool {
188 self.pnl > 0.0
189 }
190
191 pub fn is_loss(&self) -> bool {
193 self.pnl < 0.0
194 }
195
196 pub fn is_long(&self) -> bool {
198 matches!(self.side, PositionSide::Long)
199 }
200
201 pub fn is_short(&self) -> bool {
203 matches!(self.side, PositionSide::Short)
204 }
205
206 pub fn duration_secs(&self) -> i64 {
208 self.exit_timestamp - self.entry_timestamp
209 }
210
211 pub fn entry_value(&self) -> f64 {
213 self.entry_price * self.quantity
214 }
215
216 pub fn exit_value(&self) -> f64 {
218 self.exit_price * self.quantity
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 fn make_entry_signal() -> Signal {
227 Signal::long(1000, 100.0)
228 }
229
230 fn make_exit_signal() -> Signal {
231 Signal::exit(2000, 110.0)
232 }
233
234 #[test]
235 fn test_position_long_profit() {
236 let pos = Position::new(
237 PositionSide::Long,
238 1000,
239 100.0,
240 10.0,
241 1.0, make_entry_signal(),
243 );
244
245 let pnl = pos.unrealized_pnl(110.0);
247 assert!((pnl - 99.0).abs() < 0.01);
249 assert!(pos.is_profitable(110.0));
250 }
251
252 #[test]
253 fn test_position_long_loss() {
254 let pos = Position::new(
255 PositionSide::Long,
256 1000,
257 100.0,
258 10.0,
259 1.0,
260 make_entry_signal(),
261 );
262
263 let pnl = pos.unrealized_pnl(90.0);
265 assert!((pnl - (-101.0)).abs() < 0.01);
267 assert!(!pos.is_profitable(90.0));
268 }
269
270 #[test]
271 fn test_position_short_profit() {
272 let pos = Position::new(
273 PositionSide::Short,
274 1000,
275 100.0,
276 10.0,
277 1.0,
278 Signal::short(1000, 100.0),
279 );
280
281 let pnl = pos.unrealized_pnl(90.0);
283 assert!((pnl - 99.0).abs() < 0.01);
285 assert!(pos.is_profitable(90.0));
286 }
287
288 #[test]
289 fn test_position_close_to_trade() {
290 let pos = Position::new(
291 PositionSide::Long,
292 1000,
293 100.0,
294 10.0,
295 1.0,
296 make_entry_signal(),
297 );
298
299 let trade = pos.close(2000, 110.0, 1.0, make_exit_signal());
300
301 assert_eq!(trade.entry_price, 100.0);
302 assert_eq!(trade.exit_price, 110.0);
303 assert_eq!(trade.quantity, 10.0);
304 assert_eq!(trade.commission, 2.0); assert!((trade.pnl - 98.0).abs() < 0.01);
307 assert!(trade.is_profitable());
308 assert!(trade.is_long());
309 assert_eq!(trade.duration_secs(), 1000);
310 }
311
312 #[test]
313 fn test_trade_return_pct() {
314 let pos = Position::new(
315 PositionSide::Long,
316 1000,
317 100.0,
318 10.0,
319 0.0,
320 make_entry_signal(),
321 );
322
323 let trade = pos.close(2000, 110.0, 0.0, make_exit_signal());
324
325 assert!((trade.return_pct - 10.0).abs() < 0.01);
327 }
328}