Browse Source

Strategy: enter/exit methods

master
Denis Tereshkin 8 years ago
parent
commit
6192f4435c
  1. 2
      examples/multiple_assets.py
  2. 2
      examples/single_asset.py
  3. 13
      src/naiback/broker/broker.py
  4. 3
      src/naiback/broker/position.py
  5. 15
      src/naiback/data/bars.py
  6. 19
      src/naiback/data/feeds/genericcsvfeed.py
  7. 159
      src/naiback/strategy/strategy.py
  8. 2
      tests/test_bars.py
  9. 4
      tests/test_genericcsvfeed.py
  10. 270
      tests/test_strategy.py

2
examples/multiple_assets.py

@ -3,7 +3,7 @@ from naiback.strategy import Strategy
from naiback.data.feeds import FinamCSVFeed from naiback.data.feeds import FinamCSVFeed
from naiback.indicators import SMA, RSI from naiback.indicators import SMA, RSI
class MyStrategy(BarStrategy): class MyStrategy(Strategy):
def __init__(self): def __init__(self):
super().__init__() super().__init__()

2
examples/single_asset.py

@ -13,7 +13,7 @@ class MyStrategy(SingleAssetStrategy):
sma = SMA(self.bars.close, 200) sma = SMA(self.bars.close, 200)
rsi = RSI(self.bars.close, 2) rsi = RSI(self.bars.close, 2)
stop = 0 stop = 0
for i, bar in self.bars[200:]: for i in self.bars.index[200:]:
if self.last_position_is_active(): if self.last_position_is_active():
if not self.exit_at_stop(i, self.last_position(), stop): if not self.exit_at_stop(i, self.last_position(), stop):
if self.bars.close[i] < exit_sma[i]: if self.bars.close[i] < exit_sma[i]:

13
src/naiback/broker/broker.py

@ -37,10 +37,23 @@ class Broker:
self.cash_ += price * size self.cash_ += price * size
self.cash_ -= volume * 0.01 * self.commission_percentage self.cash_ -= volume * 0.01 * self.commission_percentage
return True
def set_commission(self, percentage): def set_commission(self, percentage):
self.commission_percentage = percentage self.commission_percentage = percentage
def last_position(self):
return self.positions[-1]
def all_positions(self): def all_positions(self):
return self.positions return self.positions
def last_position_is_active(self):
if len(self.positions) == 0:
return False
if self.last_position().exit_price() is None:
return True
return False

3
src/naiback/broker/position.py

@ -19,6 +19,9 @@ class Position:
def size(self): def size(self):
return self.size_ return self.size_
def is_long(self):
return self.size_ > 0
def entry_commission(self): def entry_commission(self):
return self.entry_metadata['commission'] return self.entry_metadata['commission']

15
src/naiback/data/bars.py

@ -7,7 +7,8 @@ class Bars:
Basic bar series structure Basic bar series structure
""" """
def __init__(self): def __init__(self, ticker):
self.ticker = ticker
self.index = [] self.index = []
self.open = [] self.open = []
self.high = [] self.high = []
@ -28,11 +29,19 @@ class Bars:
self.volume.append(volume) self.volume.append(volume)
self.timestamp.append(timestamp) self.timestamp.append(timestamp)
def insert_bar(self, index, open_, high, low, close, volume, timestamp):
self.open.insert(index, open_)
self.high.insert(index, high)
self.low.insert(index, low)
self.close.insert(index, close)
self.volume.insert(index, volume)
self.timestamp.insert(index, timestamp)
@classmethod @classmethod
def from_feed(feed): def from_feed(cls, feed):
if feed.type() != 'bars': if feed.type() != 'bars':
raise NaibackException('Invalid feed type: "{}", should be "bars"'.format(feed.type())) raise NaibackException('Invalid feed type: "{}", should be "bars"'.format(feed.type()))
bars = Bars() bars = Bars(feed.ticker())
for bar in feed.items(): for bar in feed.items():
bars.append_bar(bar.open, bar.high, bar.low, bar.close, bar.volume, bar.timestamp) bars.append_bar(bar.open, bar.high, bar.low, bar.close, bar.volume, bar.timestamp)
return bars return bars

19
src/naiback/data/feeds/genericcsvfeed.py

@ -8,16 +8,17 @@ class GenericCSVFeed(Feed):
def __init__(self, fp): def __init__(self, fp):
self.bars = [] self.bars = []
self.ticker_ = None
reader = csv.reader(fp, delimiter=',') reader = csv.reader(fp, delimiter=',')
next(reader) next(reader)
next(reader)
for row in reader: for row in reader:
try: try:
open_ = row[4] self.ticker_ = row[0]
high = row[5] open_ = float(row[4])
low = row[6] high = float(row[5])
close = row[7] low = float(row[6])
volume = row[8] close = float(row[7])
volume = int(row[8])
date = row[2] date = row[2]
time = row[3] time = row[3]
dt = datetime.datetime.strptime(date + "_" + time, "%Y%m%d_%H%M%S") dt = datetime.datetime.strptime(date + "_" + time, "%Y%m%d_%H%M%S")
@ -25,5 +26,11 @@ class GenericCSVFeed(Feed):
except IndexError: except IndexError:
pass pass
def type(self):
return 'bars'
def items(self): def items(self):
return self.bars return self.bars
def ticker(self):
return self.ticker_

159
src/naiback/strategy/strategy.py

@ -1,12 +1,17 @@
from abc import abstractmethod from abc import abstractmethod
from naiback.broker.position import Position
from naiback.broker.broker import Broker
from naiback.data.bars import Bars
class Strategy: class Strategy:
""" """
Internal base class for strategies. User should use it's subclasses (i.e. SingleAssetStrategy)
""" """
def __init__(self): def __init__(self):
self.feeds = [] self.feeds = []
self.all_bars = []
self.all_positions = []
self.broker = Broker()
def add_feed(self, feed): def add_feed(self, feed):
""" """
@ -25,4 +30,156 @@ class Strategy:
""" """
By default, just calls execute. By default, just calls execute.
""" """
self._prepare_bars()
self.execute() self.execute()
def _prepare_bars(self):
if len(self.feeds) == 0:
raise NaibackException('No feeds added to strategy')
self.all_bars.clear()
for feed in self.feeds:
self.all_bars.append(Bars.from_feed(feed))
all_dates = list(sorted(self._combine_dates()))
for bars in self.all_bars:
self._synchronize_bars(bars, all_dates)
def _get_bars(self, ticker):
for bars in self.all_bars:
if bars.ticker == ticker:
return bars
return None
def last_position(self):
return self.broker.last_position()
def all_positions(self):
return self.broker.all_positions()
def last_position_is_active(self):
return self.broker.last_position_is_active()
def _synchronize_bars(self, bars, all_dates):
bar_pos = 0
for dt in all_dates:
if bars.timestamp[bar_pos] > dt:
open_ = bars.open[bar_pos]
high = bars.high[bar_pos]
low = bars.low[bar_pos]
close = bars.close[bar_pos]
volume = bars.volume[bar_pos]
bars.insert_bar(bar_pos, open_, high, low, close, volume, dt)
def _combine_dates(self):
dates = set()
for bars in self.all_bars:
dates.update(bars.timestamp)
return dates
def buy_at_open(self, bar, ticker):
bars = self._get_bars(ticker)
return self.broker.add_position(ticker, bars.open[bar], 1)
def buy_at_limit(self, bar, price, ticker):
bars = self._get_bars(ticker)
if bars.low[bar] <= price:
if bars.open[bar] > price:
return self.broker.add_position(ticker, price, 1)
else:
return self.broker.add_position(ticker, bars.open[bar], 1)
else:
return None
def buy_at_stop(self, bar, price, ticker):
bars = self._get_bars(ticker)
if bars.high[bar] >= price:
if bars.open[bar] < price:
return self.broker.add_position(ticker, price, 1)
else:
return self.broker.add_position(ticker, bars.open[bar], 1)
else:
return None
def buy_at_close(self, bar, ticker):
bars = self._get_bars(ticker)
return self.broker.add_position(ticker, bars.close[bar], 1)
def short_at_open(self, bar, ticker):
bars = self._get_bars(ticker)
return self.broker.add_position(ticker, bars.open[bar], -1)
def short_at_limit(self, bar, price, ticker):
bars = self._get_bars(ticker)
if bars.high[bar] >= price:
if bars.open[bar] < price:
return self.broker.add_position(ticker, price, -1)
else:
return self.broker.add_position(ticker, bars.open[bar], -1)
else:
return None
def short_at_stop(self, bar, price, ticker):
bars = self._get_bars(ticker)
if bars.low[bar] <= price:
if bars.open[bar] > price:
return self.broker.add_position(ticker, price, -1)
else:
return self.broker.add_position(ticker, bars.open[bar], -1)
else:
return None
def short_at_close(self, bar, ticker):
bars = self._get_bars(ticker)
return self.broker.add_position(ticker, bars.close[bar], -1)
def exit_at_open(self, bar, pos):
bars = self._get_bars(pos.ticker)
return self.broker.close_position(pos, bars.open[bar])
def exit_at_limit(self, bar, price, pos):
bars = self._get_bars(pos.ticker)
if pos.is_long():
if bars.high[bar] >= price:
if bars.open[bar] < price:
return self.broker.close_position(pos, price)
else:
return self.broker.close_position(pos, bars.open[bar])
else:
return False
else:
if bars.low[bar] <= price:
if bars.open[bar] > price:
return self.broker.close_position(pos, price)
else:
return self.broker.close_position(pos, bars.open[bar])
else:
return False
def exit_at_stop(self, bar, price, pos):
bars = self._get_bars(pos.ticker)
if pos.is_long():
if bars.low[bar] <= price:
if bars.open[bar] > price:
return self.broker.close_position(pos, price)
else:
return self.broker.close_position(pos, bars.open[bar])
else:
return False
else:
if bars.high[bar] >= price:
if bars.open[bar] < price:
return self.broker.close_position(pos, price)
else:
return self.broker.close_position(pos, bars.open[bar])
else:
return False
def exit_at_close(self, bar, pos):
bars = self._get_bars(pos.ticker)
return self.broker.close_position(pos, bars.close[bar])

2
tests/test_bars.py

@ -5,7 +5,7 @@ import datetime
from naiback.data.bars import Bars from naiback.data.bars import Bars
def test_bar_append(): def test_bar_append():
bars = Bars() bars = Bars('FOO')
bars.append_bar(10, 20, 5, 11, 100, datetime.datetime(2017, 1, 1)) bars.append_bar(10, 20, 5, 11, 100, datetime.datetime(2017, 1, 1))
assert bars.open[0] == 10 assert bars.open[0] == 10

4
tests/test_genericcsvfeed.py

@ -1,5 +1,4 @@
import pytest import pytest
import datetime import datetime
import io import io
@ -9,8 +8,7 @@ from naiback.data.feeds.genericcsvfeed import GenericCSVFeed
@pytest.fixture @pytest.fixture
def sample(): def sample():
return ''' return '''<TICKER>,<PER>,<DATE>,<TIME>,<OPEN>,<HIGH>,<LOW>,<CLOSE>,<VOL>
<TICKER>,<PER>,<DATE>,<TIME>,<OPEN>,<HIGH>,<LOW>,<CLOSE>,<VOL>
MICEX,D,20100111,000000,1411.3700000,1456.7600000,1411.3700000,1444.7800000,51634239250 MICEX,D,20100111,000000,1411.3700000,1456.7600000,1411.3700000,1444.7800000,51634239250
MICEX,D,20100112,000000,1444.7800000,1445.6400000,1424.8700000,1427.6700000,38343314792 MICEX,D,20100112,000000,1444.7800000,1445.6400000,1424.8700000,1427.6700000,38343314792
MICEX,D,20100113,000000,1417.8000000,1444.7700000,1412.9300000,1435.0100000,42183285247 MICEX,D,20100113,000000,1417.8000000,1444.7700000,1412.9300000,1435.0100000,42183285247

270
tests/test_strategy.py

@ -0,0 +1,270 @@
import pytest
import datetime
import io
from naiback.data.feeds.genericcsvfeed import GenericCSVFeed
from naiback.strategy.strategy import Strategy
@pytest.fixture
def feed():
data = '''<TICKER>,<PER>,<DATE>,<TIME>,<OPEN>,<HIGH>,<LOW>,<CLOSE>,<VOL>
MICEX,D,20100111,000000,1411.3700000,1456.7600000,1411.3700000,1444.7800000,51634239250
MICEX,D,20100112,000000,1444.7800000,1445.6400000,1424.8700000,1427.6700000,38343314792
MICEX,D,20100113,000000,1417.8000000,1444.7700000,1412.9300000,1435.0100000,42183285247
MICEX,D,20100114,000000,1439.5500000,1456.2700000,1439.5500000,1455.6500000,44372479120
'''
f = GenericCSVFeed(io.StringIO(data))
return f
class BuyAndSell(Strategy):
def __init__(self, buy_bar, sell_bar):
super().__init__()
self.buy_bar = buy_bar
self.sell_bar = sell_bar
def execute(self):
pos = self.buy_at_open(self.buy_bar, 'MICEX')
self.exit_at_close(self.sell_bar, pos)
class BuyAndSellLimit(Strategy):
def __init__(self, buy_bar, buy_price, sell_bar, sell_price):
super().__init__()
self.buy_bar = buy_bar
self.buy_price = buy_price
self.sell_bar = sell_bar
self.sell_price = sell_price
def execute(self):
pos = self.buy_at_limit(self.buy_bar, self.buy_price, 'MICEX')
self.exit_at_limit(self.sell_bar, self.sell_price, pos)
class BuyOpenAndSellStop(Strategy):
def __init__(self, buy_bar, sell_bar, sell_price):
super().__init__()
self.buy_bar = buy_bar
self.sell_bar = sell_bar
self.sell_price = sell_price
def execute(self):
pos = self.buy_at_open(self.buy_bar, 'MICEX')
self.exit_at_stop(self.sell_bar, self.sell_price, pos)
class ShortAndCover(Strategy):
def __init__(self, short_bar, cover_bar):
super().__init__()
self.short_bar = short_bar
self.cover_bar = cover_bar
def execute(self):
pos = self.short_at_open(self.short_bar, 'MICEX')
self.exit_at_close(self.cover_bar, pos)
class ShortAndCoverLimit(Strategy):
def __init__(self, short_bar, short_price, cover_bar, cover_price):
super().__init__()
self.short_bar = short_bar
self.short_price = short_price
self.cover_bar = cover_bar
self.cover_price = cover_price
def execute(self):
pos = self.short_at_limit(self.short_bar, self.short_price, 'MICEX')
self.exit_at_limit(self.cover_bar, self.cover_price, pos)
class ShortOpenAndCoverStop(Strategy):
def __init__(self, short_bar, cover_bar, cover_price):
super().__init__()
self.short_bar = short_bar
self.cover_bar = cover_bar
self.cover_price = cover_price
def execute(self):
pos = self.short_at_open(self.short_bar, 'MICEX')
self.exit_at_stop(self.cover_bar, self.cover_price, pos)
def test_buy_and_sell_1(feed):
s = BuyAndSell(0, 0)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1444.78 - 1411.37))
def test_buy_and_sell_2(feed):
s = BuyAndSell(0, 1)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1427.67 - 1411.37))
def test_buy_and_sell_3(feed):
s = BuyAndSell(1, 2)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1435.01 - 1444.78))
def test_buy_and_sell_limit_1(feed):
s = BuyAndSellLimit(0, 1412, 1, 1445)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1445 - 1411.37))
assert(not s.last_position_is_active())
def test_buy_and_sell_limit_2(feed):
s = BuyAndSellLimit(1, 1430, 2, 1440)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1440 - 1430))
assert(not s.last_position_is_active())
def test_buy_and_sell_limit_3(feed):
s = BuyAndSellLimit(1, 1450, 2, 1410)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1417.80 - 1444.78))
assert(not s.last_position_is_active())
def test_buy_and_sell_stop_1(feed):
s = BuyOpenAndSellStop(0, 1, 1430)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1430 - 1411.37))
assert(not s.last_position_is_active())
def test_buy_and_sell_stop_2(feed):
s = BuyOpenAndSellStop(0, 1, 1450)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1444.78 - 1411.37))
assert(not s.last_position_is_active())
def test_short_and_cover_1(feed):
s = ShortAndCover(0, 0)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1411.37 - 1444.78))
def test_short_and_cover_2(feed):
s = ShortAndCover(0, 1)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1411.37 - 1427.67))
def test_short_and_cover_limit_1(feed):
s = ShortAndCoverLimit(0, 1450, 1, 1430)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1450 - 1430))
assert(not s.last_position_is_active())
def test_short_and_cover_limit_2(feed):
s = ShortAndCoverLimit(0, 1410, 1, 1430)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1411.37 - 1430))
assert(not s.last_position_is_active())
def test_short_and_cover_limit_3(feed):
s = ShortAndCoverLimit(0, 1430, 1, 1450)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1430 - 1444.78))
assert(not s.last_position_is_active())
def test_short_and_cover_stop_1(feed):
s = ShortOpenAndCoverStop(0, 1, 1445)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1411.37 - 1445))
assert(not s.last_position_is_active())
def test_short_and_cover_stop_2(feed):
s = ShortOpenAndCoverStop(0, 1, 1440)
s.add_feed(feed)
initial_cash = s.broker.cash()
s.run()
ending_cash = s.broker.cash()
assert(ending_cash == (initial_cash + 1411.37 - 1444.78))
assert(not s.last_position_is_active())
Loading…
Cancel
Save