piker/piker/ui/quantdom/portfolio.py

413 lines
14 KiB
Python

"""Portfolio."""
import itertools
from contextlib import contextmanager
from enum import Enum, auto
import numpy as np
from .base import Quotes
from .performance import BriefPerformance, Performance, Stats
from .utils import fromtimestamp, timeit
__all__ = ('Portfolio', 'Position', 'Order')
class BasePortfolio:
def __init__(self, balance=100_000, leverage=5):
self._initial_balance = balance
self.balance = balance
self.equity = None
# TODO:
# self.cash
# self.currency
self.leverage = leverage
self.positions = []
self.balance_curve = None
self.equity_curve = None
self.long_curve = None
self.short_curve = None
self.mae_curve = None
self.mfe_curve = None
self.stats = None
self.performance = None
self.brief_performance = None
def clear(self):
self.positions.clear()
self.balance = self._initial_balance
@property
def initial_balance(self):
return self._initial_balance
@initial_balance.setter
def initial_balance(self, value):
self._initial_balance = value
def add_position(self, position):
position.ticket = len(self.positions) + 1
self.positions.append(position)
def position_count(self, tp=None):
if tp == Order.BUY:
return len([p for p in self.positions if p.type == Order.BUY])
elif tp == Order.SELL:
return len([p for p in self.positions if p.type == Order.SELL])
return len(self.positions)
def _close_open_positions(self):
for p in self.positions:
if p.status == Position.OPEN:
p.close(
price=Quotes[-1].open,
volume=p.volume,
time=Quotes[-1].time
)
def _get_market_position(self):
p = self.positions[0] # real postions
p = Position(
symbol=p.symbol,
ptype=Order.BUY,
volume=p.volume,
price=Quotes[0].open,
open_time=Quotes[0].time,
close_price=Quotes[-1].close,
close_time=Quotes[-1].time,
id_bar_close=len(Quotes) - 1,
status=Position.CLOSED,
)
p.profit = p.calc_profit(close_price=Quotes[-1].close)
p.profit_perc = p.profit / self._initial_balance * 100
return p
def _calc_equity_curve(self):
"""Equity curve."""
self.equity_curve = np.zeros_like(Quotes.time)
for i, p in enumerate(self.positions):
balance = np.sum(self.stats['All'][:i].abs)
for ibar in range(p.id_bar_open, p.id_bar_close):
profit = p.calc_profit(close_price=Quotes[ibar].close)
self.equity_curve[ibar] = balance + profit
# taking into account the real balance after the last trade
self.equity_curve[-1] = self.balance_curve[-1]
def _calc_buy_and_hold_curve(self):
"""Buy and Hold."""
p = self._get_market_position()
self.buy_and_hold_curve = np.array(
[p.calc_profit(close_price=price) for price in Quotes.close]
)
def _calc_long_short_curves(self):
"""Only Long/Short positions curve."""
self.long_curve = np.zeros_like(Quotes.time)
self.short_curve = np.zeros_like(Quotes.time)
for i, p in enumerate(self.positions):
if p.type == Order.BUY:
name = 'Long'
curve = self.long_curve
else:
name = 'Short'
curve = self.short_curve
balance = np.sum(self.stats[name][:i].abs)
# Calculate equity for this position
for ibar in range(p.id_bar_open, p.id_bar_close):
profit = p.calc_profit(close_price=Quotes[ibar].close)
curve[ibar] = balance + profit
for name, curve in [
('Long', self.long_curve),
('Short', self.short_curve),
]:
curve[:] = fill_zeros_with_last(curve)
# taking into account the real balance after the last trade
curve[-1] = np.sum(self.stats[name].abs)
def _calc_curves(self):
self.mae_curve = np.cumsum(self.stats['All'].mae)
self.mfe_curve = np.cumsum(self.stats['All'].mfe)
self.balance_curve = np.cumsum(self.stats['All'].abs)
self._calc_equity_curve()
self._calc_buy_and_hold_curve()
self._calc_long_short_curves()
@contextmanager
def optimization_mode(self):
"""Backup and restore current balance and positions."""
# mode='general',
self.backup_balance = self.balance
self.backup_positions = self.positions.copy()
self.balance = self._initial_balance
self.positions.clear()
yield
self.balance = self.backup_balance
self.positions = self.backup_positions.copy()
self.backup_positions.clear()
@timeit
def run_optimization(self, strategy, params):
keys = list(params.keys())
vals = list(params.values())
variants = list(itertools.product(*vals))
self.brief_performance = BriefPerformance(shape=(len(variants),))
with self.optimization_mode():
for i, vals in enumerate(variants):
kwargs = {keys[n]: val for n, val in enumerate(vals)}
strategy.start(**kwargs)
self._close_open_positions()
self.brief_performance.add(
self._initial_balance, self.positions, i, kwargs
)
self.clear()
@timeit
def summarize(self):
self._close_open_positions()
positions = {
'All': self.positions,
'Long': [p for p in self.positions if p.type == Order.BUY],
'Short': [p for p in self.positions if p.type == Order.SELL],
'Market': [self._get_market_position()],
}
self.stats = Stats(positions)
self.performance = Performance(
self._initial_balance, self.stats, positions
)
self._calc_curves()
Portfolio = BasePortfolio()
class PositionStatus(Enum):
OPEN = auto()
CLOSED = auto()
CANCELED = auto()
class Position:
OPEN = PositionStatus.OPEN
CLOSED = PositionStatus.CLOSED
CANCELED = PositionStatus.CANCELED
__slots__ = (
'type',
'symbol',
'ticket',
'open_price',
'close_price',
'open_time',
'close_time',
'volume',
'sl',
'tp',
'status',
'profit',
'profit_perc',
'commis',
'id_bar_open',
'id_bar_close',
'entry_name',
'exit_name',
'total_profit',
'comment',
)
def __init__(
self,
symbol,
ptype,
price,
volume,
open_time,
sl=None,
tp=None,
status=OPEN,
entry_name='',
exit_name='',
comment='',
**kwargs,
):
self.type = ptype
self.symbol = symbol
self.ticket = None
self.open_price = price
self.close_price = None
self.open_time = open_time
self.close_time = None
self.volume = volume
self.sl = sl
self.tp = tp
self.status = status
self.profit = None
self.profit_perc = None
self.commis = None
self.id_bar_open = np.where(Quotes.time == self.open_time)[0][0]
self.id_bar_close = None
self.entry_name = entry_name
self.exit_name = exit_name
self.total_profit = 0
self.comment = comment
# self.bars_on_trade = None
# self.is_profitable = False
for k, v in kwargs.items():
setattr(self, k, v)
def __repr__(self):
_type = 'LONG' if self.type == Order.BUY else 'SHORT'
time = fromtimestamp(self.open_time).strftime('%d.%m.%y %H:%M')
return '%s/%s/[%s - %.4f]' % (
self.status.name,
_type,
time,
self.open_price,
)
def close(self, price, time, volume=None):
# TODO: allow closing only part of the volume
self.close_price = price
self.close_time = time
self.id_bar_close = np.where(Quotes.time == self.close_time)[0][0]
self.profit = self.calc_profit(volume=volume or self.volume)
self.profit_perc = self.profit / Portfolio.balance * 100
Portfolio.balance += self.profit
self.total_profit = Portfolio.balance - Portfolio.initial_balance
self.status = self.CLOSED
def calc_profit(self, volume=None, close_price=None):
# TODO: rewrite it
close_price = close_price or self.close_price
volume = volume or self.volume
factor = 1 if self.type == Order.BUY else -1
price_delta = (close_price - self.open_price) * factor
if self.symbol.mode in [self.symbol.FOREX, self.symbol.CFD]:
# Margin: Lots*Contract_Size/Leverage
if (
self.symbol.mode == self.symbol.FOREX
and self.symbol.ticker[:3] == 'USD'
):
# Example: 'USD/JPY'
# Прибыль Размер Объем Текущий
# в пунктах пункта позиции курс
# 1 * 0.0001 * 100000 / 1.00770
# USD/CHF: 1*0.0001*100000/1.00770 = $9.92
# 0.01
# USD/JPY: 1*0.01*100000/121.35 = $8.24
# (1.00770-1.00595)/0.0001 = 17.5 пунктов
# (1.00770-1.00595)/0.0001*0.0001*100000*1/1.00770*1
_points = price_delta / self.symbol.tick_size
_profit = (
_points
* self.symbol.tick_size
* self.symbol.contract_size
/ close_price
* volume
)
elif (
self.symbol.mode == self.symbol.FOREX
and self.symbol.ticker[-3:] == 'USD'
):
# Example: 'EUR/USD'
# Profit: (close_price-open_price)*Contract_Size*Lots
# EUR/USD BUY: (1.05875-1.05850)*100000*1 = +$25 (без комиссии)
_profit = price_delta * self.symbol.contract_size * volume
else:
# Cross rates. Example: 'GBP/CHF'
# Цена пункта =
# объем поз.*размер п.*тек.курс баз.вал. к USD/тек. кросс-курс
# GBP/CHF: 100000*0.0001*1.48140/1.48985 = $9.94
# TODO: temporary patch (same as the previous choice) -
# in the future connect to some quotes provider and get rates
_profit = price_delta * self.symbol.contract_size * volume
elif self.symbol.mode == self.symbol.FUTURES:
# Margin: Lots *InitialMargin*Percentage/100
# Profit: (close_price-open_price)*TickPrice/TickSize*Lots
# CL BUY: (46.35-46.30)*10/0.01*1 = $50 (без учета комиссии!)
# EuroFX(6E) BUY:(1.05875-1.05850)*12.50/0.0001*1 =$31.25 (без ком)
# RTS (RIH5) BUY:(84510-84500)*12.26506/10*1 = @12.26506 (без ком)
# E-miniSP500 BUY:(2065.95-2065.25)*12.50/0.25 = $35 (без ком)
# http://americanclearing.ru/specifications.php
# http://www.moex.com/ru/contract.aspx?code=RTS-3.18
# http://www.cmegroup.com/trading/equity-index/us-index/e-mini-sandp500_contract_specifications.html
_profit = (
price_delta
* self.symbol.tick_value
/ self.symbol.tick_size
* volume
)
else:
# shares
_profit = price_delta * volume
return _profit
def calc_mae(self, low, high):
"""Return [MAE] Maximum Adverse Excursion."""
if self.type == Order.BUY:
return self.calc_profit(close_price=low)
return self.calc_profit(close_price=high)
def calc_mfe(self, low, high):
"""Return [MFE] Maximum Favorable Excursion."""
if self.type == Order.BUY:
return self.calc_profit(close_price=high)
return self.calc_profit(close_price=low)
class OrderType(Enum):
BUY = auto()
SELL = auto()
BUY_LIMIT = auto()
SELL_LIMIT = auto()
BUY_STOP = auto()
SELL_STOP = auto()
class Order:
BUY = OrderType.BUY
SELL = OrderType.SELL
BUY_LIMIT = OrderType.BUY_LIMIT
SELL_LIMIT = OrderType.SELL_LIMIT
BUY_STOP = OrderType.BUY_STOP
SELL_STOP = OrderType.SELL_STOP
@staticmethod
def open(symbol, otype, price, volume, time, sl=None, tp=None):
# TODO: add margin calculation
# and if the margin is not enough - do not open the position
position = Position(
symbol=symbol,
ptype=otype,
price=price,
volume=volume,
open_time=time,
sl=sl,
tp=tp,
)
Portfolio.add_position(position)
return position
@staticmethod
def close(position, price, time, volume=None):
# FIXME: may be closed not the whole volume, but
# the position status will be changed to CLOSED
position.close(price=price, time=time, volume=volume)
def fill_zeros_with_last(arr):
"""Fill empty(zero) elements (between positions)."""
index = np.arange(len(arr))
index[arr == 0] = 0
index = np.maximum.accumulate(index)
return arr[index]