From a1c35f034e5c173fc741b1792b56247fd4a35488 Mon Sep 17 00:00:00 2001 From: Denis Tereshkin Date: Sun, 25 Mar 2018 22:02:47 +0700 Subject: [PATCH] Basic analyzer --- src/naiback/analyzers/__init__.py | 2 + src/naiback/analyzers/analyzer.py | 9 +++ src/naiback/analyzers/statsanalyzer.py | 96 ++++++++++++++++++++++++++ src/naiback/broker/broker.py | 9 ++- src/naiback/broker/position.py | 22 +++++- src/naiback/strategy/strategy.py | 5 ++ tests/test_broker.py | 9 +++ 7 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/naiback/analyzers/__init__.py create mode 100644 src/naiback/analyzers/analyzer.py create mode 100644 src/naiback/analyzers/statsanalyzer.py diff --git a/src/naiback/analyzers/__init__.py b/src/naiback/analyzers/__init__.py new file mode 100644 index 0000000..139597f --- /dev/null +++ b/src/naiback/analyzers/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/src/naiback/analyzers/analyzer.py b/src/naiback/analyzers/analyzer.py new file mode 100644 index 0000000..7429d7b --- /dev/null +++ b/src/naiback/analyzers/analyzer.py @@ -0,0 +1,9 @@ + +class Analyzer: + + def __init__(self): + pass + + def generate_plain_text(self): + pass + diff --git a/src/naiback/analyzers/statsanalyzer.py b/src/naiback/analyzers/statsanalyzer.py new file mode 100644 index 0000000..9aa849a --- /dev/null +++ b/src/naiback/analyzers/statsanalyzer.py @@ -0,0 +1,96 @@ + +from .analyzer import Analyzer + +from prettytable import PrettyTable + +def render_float(a): + return "{:.3f}".format(a) + + +def render_ratio(a, b): + if b != 0: + return a / b + else: + return "∞" + +class StatsAnalyzer(Analyzer): + + def __init__(self, strategy): + self.strategy = strategy + + def generate_plain_text(self): + positions = self.strategy.broker.retired_positions() # TODO also add open positions + stats = self.calc_stats(positions) + + table = PrettyTable() + table.field_names = ["", "All positions", "Long only", "Short only"] + table.add_row(["Net profit", render_float(stats['all']['net_profit']), render_float(stats['long']['net_profit']), render_float(stats['short']['net_profit'])]) + table.add_row(["Bars in trade", stats['all']['bars_in_trade'], stats['long']['bars_in_trade'], stats['short']['bars_in_trade']]) + table.add_row(["Profit per bar", render_float(stats['all']['profit_per_bar']), render_float(stats['long']['profit_per_bar']), render_float(stats['short']['profit_per_bar'])]) + table.add_row(["Number of trades", stats['all']['number_of_trades'], stats['long']['number_of_trades'], stats['short']['number_of_trades']]) + table.add_row(["Avg. profit", render_float(stats['all']['avg']), render_float(stats['long']['avg']), render_float(stats['short']['avg'])]) + table.add_row(["Avg. profit, %", render_float(stats['all']['avg_percentage']), render_float(stats['long']['avg_percentage']), render_float(stats['short']['avg_percentage'])]) + table.add_row(["Avg. bars in trade", render_float(stats['all']['avg_bars']), render_float(stats['long']['avg_bars']), render_float(stats['short']['avg_bars'])]) + table.add_row(["Winning trades", stats['all']['won'], stats['long']['won'], stats['short']['won']]) + table.add_row(["Gross profit", render_float(stats['all']['total_won']), render_float(stats['long']['total_won']), render_float(stats['short']['total_won'])]) + table.add_row(["Losing trades", stats['all']['lost'], stats['long']['lost'], stats['short']['lost']]) + table.add_row(["Gross loss", render_float(stats['all']['total_lost']), render_float(stats['long']['total_lost']), render_float(stats['short']['total_lost'])]) + table.add_row(["Profit factor", render_float(stats['all']['profit_factor']), render_float(stats['long']['profit_factor']), render_float(stats['short']['profit_factor'])]) + + return table.get_string() + + def calc_stats(self, positions): + longs = list(filter(lambda x: x.is_long(), positions)) + shorts = list(filter(lambda x: x.is_short(), positions)) + + result = { 'all' : {}, 'long' : {}, 'short' : {} } + + result['all']['net_profit'] = sum([pos.pnl() for pos in positions]) + result['long']['net_profit'] = sum([pos.pnl() for pos in longs]) + result['short']['net_profit'] = sum([pos.pnl() for pos in shorts]) + + result['all']['bars_in_trade'] = sum([pos.bars_in_trade() for pos in positions]) + result['long']['bars_in_trade'] = sum([pos.bars_in_trade() for pos in longs]) + result['short']['bars_in_trade'] = sum([pos.bars_in_trade() for pos in shorts]) + + result['all']['profit_per_bar'] = render_ratio(result['all']['net_profit'], result['all']['bars_in_trade']) + result['long']['profit_per_bar'] = render_ratio(result['long']['net_profit'], result['long']['bars_in_trade']) + result['short']['profit_per_bar'] = render_ratio(result['short']['net_profit'], result['short']['bars_in_trade']) + + result['all']['number_of_trades'] = len(positions) + result['long']['number_of_trades'] = len(list(longs)) + result['short']['number_of_trades'] = len(list(shorts)) + + result['all']['avg'] = render_ratio(result['all']['net_profit'], result['all']['number_of_trades']) + result['long']['avg'] = render_ratio(result['long']['net_profit'], result['long']['number_of_trades']) + result['short']['avg'] = render_ratio(result['short']['net_profit'], result['short']['number_of_trades']) + + result['all']['avg_percentage'] = render_ratio(sum([pos.profit_percentage() for pos in positions]), result['all']['number_of_trades']) + result['long']['avg_percentage'] = render_ratio(sum([pos.profit_percentage() for pos in longs]), result['long']['number_of_trades']) + result['short']['avg_percentage'] = render_ratio(sum([pos.profit_percentage() for pos in shorts]), result['short']['number_of_trades']) + + result['all']['avg_bars'] = render_ratio(result['all']['bars_in_trade'], result['all']['number_of_trades']) + result['long']['avg_bars'] = render_ratio(result['long']['bars_in_trade'], result['long']['number_of_trades']) + result['short']['avg_bars'] = render_ratio(result['short']['bars_in_trade'], result['short']['number_of_trades']) + + result['all']['won'] = len(list(filter(lambda pos: pos.pnl() > 0, positions))) + result['long']['won'] = len(list(filter(lambda pos: pos.pnl() > 0, longs))) + result['short']['won'] = len(list(filter(lambda pos: pos.pnl() > 0, shorts))) + + result['all']['lost'] = len(list(filter(lambda pos: pos.pnl() <= 0, positions))) + result['long']['lost'] = len(list(filter(lambda pos: pos.pnl() <= 0, longs))) + result['short']['lost'] = len(list(filter(lambda pos: pos.pnl() <= 0, shorts))) + + result['all']['total_won'] = sum(map(lambda pos: pos.pnl(), filter(lambda pos: pos.pnl() > 0, positions))) + result['long']['total_won'] = sum(map(lambda pos: pos.pnl(), filter(lambda pos: pos.pnl() > 0, longs))) + result['short']['total_won'] = sum(map(lambda pos: pos.pnl(), filter(lambda pos: pos.pnl() > 0, shorts))) + + result['all']['total_lost'] = sum(map(lambda pos: pos.pnl(), filter(lambda pos: pos.pnl() <= 0, positions))) + result['long']['total_lost'] = sum(map(lambda pos: pos.pnl(), filter(lambda pos: pos.pnl() <= 0, longs))) + result['short']['total_lost'] = sum(map(lambda pos: pos.pnl(), filter(lambda pos: pos.pnl() <= 0, shorts))) + + result['all']['profit_factor'] = render_ratio(result['all']['total_won'], -result['all']['total_lost']) + result['long']['profit_factor'] = render_ratio(result['long']['total_won'], -result['long']['total_lost']) + result['short']['profit_factor'] = render_ratio(result['short']['total_won'], -result['short']['total_lost']) + + return result diff --git a/src/naiback/broker/broker.py b/src/naiback/broker/broker.py index e838d52..ba0a1c1 100644 --- a/src/naiback/broker/broker.py +++ b/src/naiback/broker/broker.py @@ -13,6 +13,7 @@ class Broker: def __init__(self, initial_cash=100000.): self.cash_ = initial_cash self.positions = [] + self.retired_positions_ = [] self.commission_percentage = 0 def cash(self): @@ -35,6 +36,9 @@ class Broker: size = pos.size() pos.exit(price, bar=bar_index) + self.retired_positions_.append(pos) + self.positions.remove(pos) + self.cash_ += price * size self.cash_ -= volume * 0.01 * self.commission_percentage return True @@ -46,7 +50,10 @@ class Broker: return self.positions[-1] def all_positions(self): - return self.positions + return self.positions[:] + + def retired_positions(self): + return self.retired_positions_ def last_position_is_active(self): if len(self.positions) == 0: diff --git a/src/naiback/broker/position.py b/src/naiback/broker/position.py index b9b87b5..0e3a308 100644 --- a/src/naiback/broker/position.py +++ b/src/naiback/broker/position.py @@ -8,6 +8,7 @@ class Position: self.exit_price_ = None self.exit_metadata = {} self.size_ = None + self.original_size_ = None self.total_pnl = 0 def entry_price(self): @@ -19,8 +20,14 @@ class Position: def size(self): return self.size_ + def original_size(self): + return self.orignal_size_ + def is_long(self): - return self.size_ > 0 + return self.original_size_ > 0 + + def is_short(self): + return self.original_size_ < 0 def entry_commission(self): return self.entry_metadata['commission'] @@ -28,12 +35,25 @@ class Position: def entry_bar(self): return self.entry_metadata['bar'] + def exit_bar(self): + return self.exit_metadata['bar'] + + def bars_in_trade(self): + return self.exit_bar() - self.entry_bar() + def pnl(self): return self.total_pnl + def profit_percentage(self): + if self.is_long(): + return (self.exit_price() / self.entry_price() - 1) * 100. + else: + return -(self.exit_price() / self.entry_price() - 1) * 100. + def enter(self, price, amount, **kwargs): self.entry_price_ = price self.size_ = amount + self.original_size_ = amount for k, v in kwargs.items(): self.entry_metadata[k] = v diff --git a/src/naiback/strategy/strategy.py b/src/naiback/strategy/strategy.py index 7e04ba8..75dde54 100644 --- a/src/naiback/strategy/strategy.py +++ b/src/naiback/strategy/strategy.py @@ -2,6 +2,7 @@ from abc import abstractmethod from naiback.broker.position import Position from naiback.broker.broker import Broker from naiback.data.bars import Bars +from naiback.analyzers.statsanalyzer import StatsAnalyzer class Strategy: """ @@ -12,6 +13,10 @@ class Strategy: self.all_bars = [] self.broker = Broker() self.bars = None + self.analyzer = StatsAnalyzer(self) + + def get_analyzer(self, analyzer_id): + return self.analyzer def add_feed(self, feed): """ diff --git a/tests/test_broker.py b/tests/test_broker.py index 2352a4f..541f6af 100644 --- a/tests/test_broker.py +++ b/tests/test_broker.py @@ -59,3 +59,12 @@ def test_broker_close_position_with_commission(): assert broker.cash() == 100 + 2 - (10 + 12) * 0.01 +def test_broker_close_position_places_it_in_retired_list(): + broker = Broker(initial_cash=100) + + pos = broker.add_position('FOO', price=10, amount=1, bar_index=0) + broker.close_position(pos, price=12, bar_index=1) + + assert pos not in broker.all_positions() + assert pos in broker.retired_positions() +