#!/usr/bin/env python
"""Position Module - Position tracking and management.
This module provides the Position class for tracking the size and price
of trading positions. It maintains position state including opening
and closing amounts.
Classes:
Position: Tracks position size, price, and related attributes.
Example:
Getting position from broker:
>>> position = broker.getposition(data)
>>> print(f"Size: {position.size}, Price: {position.price}")
"""
# Position class, keeps and updates position size and price, has no relationship with any other assets, only keeps size and price
[docs]
class Position:
"""Keeps and updates the size and price of a position.
The Position object has no relationship to any specific asset. It only
keeps size and price information.
Attributes:
size: Current position size (positive for long, negative for short).
price: Current price of the position.
price_orig: Original price when position was opened.
upopened: Amount of position opened in last update.
upclosed: Amount of position closed in last update.
adjbase: Adjustment base for position calculations.
Example:
>>> position = Position(size=100, price=50.0)
>>> print(len(position)) # Returns True if size != 0
"""
# Information displayed when printing position
def __str__(self):
return (
"--- Position Begin\n"
f"- Size: {self.size}\n"
f"- Price: {self.price}\n"
f"- Price orig: {self.price_orig}\n"
f"- Closed: {self.upclosed}\n"
f"- Opened: {self.upopened}\n"
f"- Adjbase: {self.adjbase}\n"
"--- Position End"
)
# Initialize based on different size and price
[docs]
def __init__(self, size=0, price=0.0):
"""Initialize a Position instance.
Args:
size: Initial position size (default: 0).
price: Initial price for the position (default: 0.0).
"""
self.datetime = None
self.size = size
if size:
self.price = self.price_orig = price
else:
self.price = 0.0
self.adjbase = None
self.upopened = 0
self.upclosed = 0
self.set(size, price)
self.updt = None
# Modify position's size and price
[docs]
def fix(self, size, price):
"""Fix the position to a specific size and price.
Args:
size: The new position size.
price: The new position price.
Returns:
bool: True if size equals oldsize, False otherwise.
"""
oldsize = self.size
self.size = size
self.price = price
return self.size == oldsize
# Set position's size and price
[docs]
def set(self, size, price):
"""Set position's size and price, calculating opening and closing amounts.
Args:
size: The new position size.
price: The new position price.
Returns:
tuple: (size, price, upopened, upclosed) calculated values.
"""
# If current position > 0 and theoretical size > current size, means new position opening;
# If theoretical size <= current position, opening amount is minimum of 0 and theoretical size;
# Closing amount equals minimum of current position and current position minus theoretical position
if self.size > 0:
if size > self.size:
self.upopened = size - self.size # new 10 - old 5 -> 5
self.upclosed = 0
else:
# same side min(0, 3) -> 0 / reversal min(0, -3) -> -3
self.upopened = min(0, size)
# same side min(10, 10 - 5) -> 5
# reversal min(10, 10 - -5) -> min(10, 15) -> 10
self.upclosed = min(self.size, self.size - size)
# Similar effect when current position < 0
elif self.size < 0:
if size < self.size:
self.upopened = size - self.size # ex: -5 - -3 -> -2
self.upclosed = 0
else:
# same side max(0, -5) -> 0 / reversal max(0, 5) -> 5
self.upopened = max(0, size)
# same side max(-10, -10 - -5) -> max(-10, -5) -> -5
# reversal max(-10, -10 - 5) -> max(-10, -15) -> -10
self.upclosed = max(self.size, self.size - size)
# If current position equals 0, opening equals the new size being set
else: # self.size == 0
self.upopened = size
self.upclosed = 0
# Actual position size
self.size = size
# Original price
self.price_orig = self.price
# If position size > 0, current price equals price, otherwise current price equals 0
if size:
self.price = price
else:
self.price = 0.0
return self.size, self.price, self.upopened, self.upclosed
def __repr__(self):
return f"Position(size={self.size}, price={self.price})"
def __eq__(self, other):
if not isinstance(other, Position):
return NotImplemented
return self.size == other.size and self.price == other.price
# Explicit: Position is mutable, so it must not be hashable
__hash__ = None
# When calling len(position), return absolute value of position
def __len__(self):
return abs(self.size)
# When calling bool(position), check if current size equals 0
def __bool__(self):
return bool(self.size != 0)
__nonzero__ = __bool__
# Clone position information
[docs]
def clone(self):
"""Create a copy of the current position.
Returns:
Position: A new Position instance with the same size and price,
including all internal state fields.
"""
pos = Position(size=self.size, price=self.price)
pos.adjbase = self.adjbase
pos.datetime = self.datetime
pos.updt = self.updt
pos.upopened = self.upopened
pos.upclosed = self.upclosed
pos.price_orig = self.price_orig
return pos
# Create a position instance, then update size and price
[docs]
def pseudoupdate(self, size, price):
"""Create a pseudo-update by cloning and updating.
Args:
size: The size to add to the position.
price: The price for the update.
Returns:
Position: A new Position instance after update.
"""
return Position(self.size, self.price).update(size, price)
# Update size and price
[docs]
def update(self, size, price, dt=None):
"""
Updates the current position and returns the updated size, price and
units used to open/close a position
# Update current position and return updated size, price and position size needed to open/close
Args:
size (int): amount to update the position's size
if size < 0: A sell operation has taken place
if size > 0: A buy operation has taken place
# Amount to update position size, if size < 0, a sell operation will be executed, if size > 0, a buy operation will be executed
price (float):
Must always be positive to ensure consistency
# Price, must always be positive to maintain consistency
dt (datetime.datetime): record datetime update (datetime.datetime)
Returns:
A tuple (non-named) contaning
size - new position size
Simply the sum of the existing size plus the "size" argument
price - new position price
If a position is increased, the new average price will be
returned
If a position is reduced, the price of the remaining size
does not change
If a position is closed, the price is nullified
If a position is reversed, the price is the price given as
argument
opened - amount of contracts from argument "size" that were used
to open/increase a position.
A position can be opened from 0 or can be a reversal.
If a reversal is performed, then opened is less than "size",
because part of "size" will have been used to close the
existing position
closed - the amount of units from arguments "size" that were used to
close/reduce a position
Both opened and closed carry the same sign as the "size" argument
because they refer to a part of the "size" argument
# Result will return a tuple containing the following data:
# size represents new position size, simply existing position size plus new position increment
# price represents new position price, returns different price based on different position
# opened represents position size to be newly opened
# closed represents position size to be closed
"""
# Update position time
self.datetime = dt # record datetime update (datetime.datetime)
# Original price
self.price_orig = self.price
# Old position size
oldsize = self.size
# New position size
self.size += size
# If size is 0
if not self.size:
# Update closed existing position
# Update opening, closing and price
opened, closed = 0, size
self.price = 0.0
# If position size is 0, need to open size amount
elif not oldsize:
# Update opened a position from 0
opened, closed = size, 0
self.price = price
# If original position size is 0
elif oldsize > 0: # existing "long" position updated
# If increased position > 0, need new opening, and calculate average position price
if size > 0: # increased position
opened, closed = size, 0
self.price = (self.price * oldsize + size * price) / self.size
# If after closing size, position still > 0, close size
elif self.size > 0: # reduced position
opened, closed = 0, size
# self.price = self.price
# In other cases, need to open self.size, close -oldsize
else: # self.size < 0 # reversed position form plus to minus
opened, closed = self.size, -oldsize
self.price = price
# Original position is negative
else: # oldsize < 0 - existing short position updated
# If newly added position is also negative, open size
if size < 0: # increased position
opened, closed = size, 0
self.price = (self.price * oldsize + size * price) / self.size
# If current self.size < 0, close size
elif self.size < 0: # reduced position
opened, closed = 0, size
# self.price = self.price
# In other cases, need to open self.size, close -oldsize
else: # self.size > 0 - reversed position from minus to plus
opened, closed = self.size, -oldsize
self.price = price
# Opening and closing amounts
self.upopened = opened
self.upclosed = closed
return self.size, self.price, opened, closed