diff --git a/core/models/position.py b/core/models/position.py index d0e88b82..8ddf4d6b 100644 --- a/core/models/position.py +++ b/core/models/position.py @@ -17,24 +17,75 @@ class Position: signal: Signal side: PositionSide - size: float - entry_price: float risk_strategy: AbstractPositionRiskStrategy take_profit_strategy: AbstractPositionTakeProfitStrategy orders: Tuple[Order] = () - closed: bool = False stop_loss_price: float = field(default_factory=lambda: 0.0000001) take_profit_price: float = field(default_factory=lambda: 0.0000001) - fee: float = field(default_factory=lambda: 0) open_timestamp: float = field(default_factory=lambda: 0) closed_timestamp: float = field(default_factory=lambda: 0) last_modified: float = field(default_factory=lambda: datetime.now().timestamp()) - exit_price: float = field(default_factory=lambda: 0.0000001) @property def trade_time(self) -> int: return abs(int(self.closed_timestamp - self.open_timestamp)) + @property + def closed(self) -> bool: + closed_orders = [ + order.size for order in self.orders if order.status == OrderStatus.CLOSED + ] + closed_size = sum(closed_orders) + + failed_orders = [ + order for order in self.orders if order.status == OrderStatus.FAILED + ] + + pending_orders = [ + order for order in self.orders if order.status == OrderStatus.PENDING + ] + + if not closed_orders: + return False + + return closed_size >= self.filled_size or len(failed_orders) == len( + pending_orders + ) + + @property + def adj_count(self) -> int: + executed_orders = [ + order for order in self.orders if order.status == OrderStatus.EXECUTED + ] + return max( + 0, + len(executed_orders) - 1, + ) + + @property + def pending_size(self) -> int: + pending_orders = [ + order.size for order in self.orders if order.status == OrderStatus.PENDING + ] + + return sum(pending_orders) + + @property + def pending_price(self) -> int: + pending_orders = [ + order.price for order in self.orders if order.status == OrderStatus.PENDING + ] + + return sum(pending_orders) / len(pending_orders) if pending_orders else 0.0 + + @property + def filled_size(self) -> int: + executed_orders = [ + order.size for order in self.orders if order.status == OrderStatus.EXECUTED + ] + + return sum(executed_orders) + @property def pnl(self) -> float: pnl = 0.0 @@ -44,7 +95,36 @@ def pnl(self) -> float: factor = -1 if self.side == PositionSide.SHORT else 1 - return factor * (self.exit_price - self.entry_price) * self.size + return factor * (self.exit_price - self.entry_price) * self.filled_size + + @property + def fee(self) -> float: + executed_orders = [ + order.fee for order in self.orders if order.status == OrderStatus.EXECUTED + ] + open_fee = sum(executed_orders) + + closed_orders = [ + order.fee for order in self.orders if order.status == OrderStatus.CLOSED + ] + closed_fee = sum(closed_orders) + + return open_fee + closed_fee + + @property + def entry_price(self) -> float: + executed_orders = [ + order.price for order in self.orders if order.status == OrderStatus.EXECUTED + ] + return sum(executed_orders) / len(executed_orders) if executed_orders else 0.0 + + @property + def exit_price(self) -> float: + closed_orders = [ + order.price for order in self.orders if order.status == OrderStatus.CLOSED + ] + + return sum(closed_orders) / len(closed_orders) if closed_orders else 0.0 def add_order(self, order: Order) -> "Position": if self.closed: @@ -53,10 +133,7 @@ def add_order(self, order: Order) -> "Position": last_modified = datetime.now().timestamp() orders = (*self.orders, order) - if order.status == OrderStatus.PENDING: - return replace(self, orders=orders, last_modified=last_modified) - - if order.status == OrderStatus.EXECUTED: + if order.status == OrderStatus.PENDING or order.status == OrderStatus.EXECUTED: take_profit_price = self.take_profit_strategy.next( self.side, order.price, self.stop_loss_price ) @@ -64,30 +141,14 @@ def add_order(self, order: Order) -> "Position": return replace( self, orders=orders, - entry_price=order.price, - fee=order.fee, - size=order.size, last_modified=last_modified, take_profit_price=take_profit_price, ) - if order.status == OrderStatus.CLOSED: - return replace( - self, - closed=True, - orders=orders, - fee=self.fee + order.fee, - exit_price=order.price, - closed_timestamp=last_modified, - last_modified=last_modified, - ) - - if order.status == OrderStatus.FAILED: + if order.status == OrderStatus.CLOSED or order.status == OrderStatus.FAILED: return replace( self, - closed=True, orders=orders, - exit_price=self.entry_price, closed_timestamp=last_modified, last_modified=last_modified, ) @@ -111,7 +172,8 @@ def to_dict(self): return { "signal": self.signal.to_dict(), "side": str(self.side), - "size": self.size, + "pending_size": self.pending_size, + "filled_size": self.filled_size, "entry_price": self.entry_price, "exit_price": self.exit_price, "closed": self.closed, @@ -123,4 +185,4 @@ def to_dict(self): } def __str__(self): - return f"Position(signal={self.signal}, side={self.side}, size={self.size}, entry_price={self.entry_price}, exit_price={self.exit_price}, take_profit_price={self.take_profit_price}, stop_loss_price={self.stop_loss_price}, trade_time={self.trade_time}, closed={self.closed})" + return f"Position(signal={self.signal}, side={self.side}, pending_size={self.pending_size}, filled_size={self.filled_size}, entry_price={self.entry_price}, exit_price={self.exit_price}, take_profit_price={self.take_profit_price}, stop_loss_price={self.stop_loss_price}, trade_time={self.trade_time}, closed={self.closed})" diff --git a/executor/_paper_order_actor.py b/executor/_paper_order_actor.py index f4e02a6c..a48c9353 100644 --- a/executor/_paper_order_actor.py +++ b/executor/_paper_order_actor.py @@ -68,11 +68,10 @@ async def _execute_order(self, event: PositionInitialized): logger.debug(f"New Position: {current_position}") - size = current_position.size + size = current_position.pending_size side = current_position.side - fill_price = await self._determine_fill_price( - side, current_position.entry_price - ) + price = current_position.pending_price + fill_price = await self._determine_fill_price(side, price) if ( side == PositionSide.LONG and current_position.stop_loss_price > fill_price @@ -82,8 +81,8 @@ async def _execute_order(self, event: PositionInitialized): order = Order( status=OrderStatus.FAILED, type=OrderType.PAPER, - price=fill_price, - size=size, + price=0, + size=0, ) else: order = Order( @@ -108,12 +107,17 @@ async def _adjust_position(self, event: RiskAdjustRequested): logger.debug(f"To Adjust Position: {current_position}") - total_value = (current_position.size * current_position.entry_price) + ( - current_position.size * event.adjust_price + total_value = (current_position.filled_size * current_position.entry_price) + ( + current_position.filled_size * event.adjust_price ) - size = current_position.size + current_position.size - fill_price = total_value / size + size = round( + 1.3 * current_position.filled_size, + current_position.signal.symbol.position_precision, + ) + fill_price = round( + total_value / size, current_position.signal.symbol.price_precision + ) if ( current_position.side == PositionSide.LONG @@ -135,7 +139,7 @@ async def _adjust_position(self, event: RiskAdjustRequested): current_position = current_position.add_order(order) - logger.info(f"Adjusted Position: {current_position}") + logger.debug(f"Adjusted Position: {current_position}") await self.tell(BrokerPositionAdjusted(current_position)) @@ -148,7 +152,7 @@ async def _close_position(self, event: PositionCloseRequested): current_position.side, event.exit_price ) price = self._calculate_closing_price(current_position, fill_price) - size = current_position.size + size = current_position.filled_size order = Order( status=OrderStatus.CLOSED, diff --git a/position/_position_factory.py b/position/_position_factory.py index a122706e..8348dc09 100644 --- a/position/_position_factory.py +++ b/position/_position_factory.py @@ -5,6 +5,7 @@ AbstractPositionTakeProfitStrategy, ) from core.models.ohlcv import OHLCV +from core.models.order import Order, OrderStatus from core.models.position import Position from core.models.side import PositionSide from core.models.signal import Signal, SignalSide @@ -36,20 +37,24 @@ async def create_position( PositionSide.LONG if signal.side == SignalSide.BUY else PositionSide.SHORT ) - position_size = await self.position_size_strategy.calculate( + order_size = await self.position_size_strategy.calculate( signal, entry_price, stop_loss_price ) - adjusted_position_size = max(position_size, symbol.min_position_size) - rounded_position_size = round(adjusted_position_size, symbol.position_precision) + adjusted_order_size = max(order_size, symbol.min_position_size) + rounded_order_size = round(adjusted_order_size, symbol.position_precision) - return Position( + order = Order( + status=OrderStatus.PENDING, price=entry_price, size=rounded_order_size + ) + + position = Position( signal, position_side, - rounded_position_size, - entry_price, self.risk_strategy, self.take_profit_strategy, open_timestamp=ohlcv.timestamp, stop_loss_price=stop_loss_price, ) + + return position.add_order(order) diff --git a/sor/_router.py b/sor/_router.py index 45c8e8a3..caefd077 100644 --- a/sor/_router.py +++ b/sor/_router.py @@ -98,15 +98,14 @@ async def open_position(self, command: OpenPosition): logger.info(f"Try to open position: {position}") symbol = position.signal.symbol - position_size = position.size + position_size = position.pending_size + stop_loss = position.stop_loss_price + entry_price = position.pending_price if self.exchange.fetch_position(symbol, position.side): logging.info("Position already exists") return - stop_loss = position.stop_loss_price - entry_price = position.entry_price - distance_to_stop_loss = abs(entry_price - stop_loss) min_size = symbol.min_position_size @@ -190,7 +189,7 @@ async def adjust_position(self, command: AdjustPosition): logger.info(f"Try to adjust position: {position}") symbol = position.signal.symbol - position_size = position.size + position_size = position.filled_size stop_loss = position.stop_loss_price entry_price = command.adjust_price @@ -279,7 +278,7 @@ async def close_position(self, command: ClosePosition): logging.info("Position is not existed") return - position_size = position.size + position_size = position.filled_size position_side = position.side exit_price = command.exit_price