added initial py analytics / rust core / ts orchestrator services
This commit is contained in:
parent
680b5fd2ae
commit
c862ed496b
62 changed files with 13459 additions and 0 deletions
166
apps/stock/core/src/positions/mod.rs
Normal file
166
apps/stock/core/src/positions/mod.rs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
use crate::{Fill, Side};
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Position {
|
||||
pub symbol: String,
|
||||
pub quantity: f64,
|
||||
pub average_price: f64,
|
||||
pub realized_pnl: f64,
|
||||
pub unrealized_pnl: f64,
|
||||
pub total_cost: f64,
|
||||
pub last_update: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PositionUpdate {
|
||||
pub symbol: String,
|
||||
pub fill: Fill,
|
||||
pub resulting_position: Position,
|
||||
}
|
||||
|
||||
pub struct PositionTracker {
|
||||
positions: DashMap<String, Position>,
|
||||
}
|
||||
|
||||
impl PositionTracker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
positions: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_fill(&self, symbol: &str, fill: &Fill, side: Side) -> PositionUpdate {
|
||||
let mut entry = self.positions.entry(symbol.to_string()).or_insert_with(|| {
|
||||
Position {
|
||||
symbol: symbol.to_string(),
|
||||
quantity: 0.0,
|
||||
average_price: 0.0,
|
||||
realized_pnl: 0.0,
|
||||
unrealized_pnl: 0.0,
|
||||
total_cost: 0.0,
|
||||
last_update: fill.timestamp,
|
||||
}
|
||||
});
|
||||
|
||||
let position = entry.value_mut();
|
||||
let old_quantity = position.quantity;
|
||||
let old_avg_price = position.average_price;
|
||||
|
||||
// Calculate new position
|
||||
match side {
|
||||
Side::Buy => {
|
||||
// Adding to position
|
||||
position.quantity += fill.quantity;
|
||||
if old_quantity >= 0.0 {
|
||||
// Already long or flat, average up/down
|
||||
position.total_cost += fill.price * fill.quantity;
|
||||
position.average_price = if position.quantity > 0.0 {
|
||||
position.total_cost / position.quantity
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
} else {
|
||||
// Was short, closing or flipping
|
||||
let close_quantity = fill.quantity.min(-old_quantity);
|
||||
let open_quantity = fill.quantity - close_quantity;
|
||||
|
||||
// Realize P&L on closed portion
|
||||
position.realized_pnl += close_quantity * (old_avg_price - fill.price);
|
||||
|
||||
// Update position for remaining
|
||||
if open_quantity > 0.0 {
|
||||
position.total_cost = open_quantity * fill.price;
|
||||
position.average_price = fill.price;
|
||||
} else {
|
||||
position.total_cost = (position.quantity.abs()) * old_avg_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
Side::Sell => {
|
||||
// Reducing position
|
||||
position.quantity -= fill.quantity;
|
||||
if old_quantity <= 0.0 {
|
||||
// Already short or flat, average up/down
|
||||
position.total_cost += fill.price * fill.quantity;
|
||||
position.average_price = if position.quantity < 0.0 {
|
||||
position.total_cost / position.quantity.abs()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
} else {
|
||||
// Was long, closing or flipping
|
||||
let close_quantity = fill.quantity.min(old_quantity);
|
||||
let open_quantity = fill.quantity - close_quantity;
|
||||
|
||||
// Realize P&L on closed portion
|
||||
position.realized_pnl += close_quantity * (fill.price - old_avg_price);
|
||||
|
||||
// Update position for remaining
|
||||
if open_quantity > 0.0 {
|
||||
position.total_cost = open_quantity * fill.price;
|
||||
position.average_price = fill.price;
|
||||
} else {
|
||||
position.total_cost = (position.quantity.abs()) * old_avg_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract commission from realized P&L
|
||||
position.realized_pnl -= fill.commission;
|
||||
position.last_update = fill.timestamp;
|
||||
|
||||
PositionUpdate {
|
||||
symbol: symbol.to_string(),
|
||||
fill: fill.clone(),
|
||||
resulting_position: position.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_position(&self, symbol: &str) -> Option<Position> {
|
||||
self.positions.get(symbol).map(|p| p.clone())
|
||||
}
|
||||
|
||||
pub fn get_all_positions(&self) -> Vec<Position> {
|
||||
self.positions.iter().map(|entry| entry.value().clone()).collect()
|
||||
}
|
||||
|
||||
pub fn get_open_positions(&self) -> Vec<Position> {
|
||||
self.positions
|
||||
.iter()
|
||||
.filter(|entry| entry.value().quantity.abs() > 0.0001)
|
||||
.map(|entry| entry.value().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn update_unrealized_pnl(&self, symbol: &str, current_price: f64) {
|
||||
if let Some(mut position) = self.positions.get_mut(symbol) {
|
||||
if position.quantity > 0.0 {
|
||||
position.unrealized_pnl = position.quantity * (current_price - position.average_price);
|
||||
} else if position.quantity < 0.0 {
|
||||
position.unrealized_pnl = position.quantity * (current_price - position.average_price);
|
||||
} else {
|
||||
position.unrealized_pnl = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_total_pnl(&self) -> (f64, f64) {
|
||||
let mut realized = 0.0;
|
||||
let mut unrealized = 0.0;
|
||||
|
||||
for position in self.positions.iter() {
|
||||
realized += position.realized_pnl;
|
||||
unrealized += position.unrealized_pnl;
|
||||
}
|
||||
|
||||
(realized, unrealized)
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.positions.clear();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue