commit d7f8cbdc7df5f3510d4f6adada1fc67cf4436717 Author: Denis Tereshkin Date: Thu Mar 15 22:19:15 2018 +0700 Initial commit diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..e69de29 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/examples/multiple_assets.py b/examples/multiple_assets.py new file mode 100644 index 0000000..bee8d66 --- /dev/null +++ b/examples/multiple_assets.py @@ -0,0 +1,30 @@ + +from naiback.strategy import Strategy +from naiback.data.feeds import FinamCSVFeed +from naiback.indicators import SMA, RSI + +class MyStrategy(BarStrategy): + + def __init__(self): + super().__init__() + + def execute(self): + self.set_context('FOO') + rsi1 = RSI(self.bars.close, 2) + self.set_context('BAR') + rsi2 = RSI(self.bars.close, 2) + for i in self.bars.index[200:]: + if self.last_position_is_active(): + if i - self.last_position().entry_bar > 3: + for position in self.all_positions(): + position.exit_at_close(i) + else: + if rsi1[i] < 20 and rsi2[i] > 80: + self.buy_at_open(i + 1, 'FOO') + self.short_at_open(i + 1, 'BAR') + +if __name__ == "__main__": + strategy = MyStrategy() + strategy.add_feed(FinamCSVFeed('data/SBER_20100101_20161231_daily.csv')) + strategy.run(from_time='2012-01-01', to_time='2016-12-31') + print(strategy.get_analyzer('stats').generate_plain_text()) diff --git a/examples/single_asset.py b/examples/single_asset.py new file mode 100644 index 0000000..01764c0 --- /dev/null +++ b/examples/single_asset.py @@ -0,0 +1,29 @@ + +from naiback.strategy import SingleAssetStrategy +from naiback.data.feeds import FinamCSVFeed +from naiback.indicators import SMA, RSI + +class MyStrategy(SingleAssetStrategy): + + def __init__(self): + super().__init__() + + def execute(self): + exit_sma = SMA(self.bars.close, 22) + sma = SMA(self.bars.close, 200) + rsi = RSI(self.bars.close, 2) + stop = 0 + for i, bar in self.bars[200:]: + if self.last_position_is_active(): + if not self.exit_at_stop(i, self.last_position(), stop): + if self.bars.close[i] < exit_sma[i]: + self.exit_at_close(i, self.last_position()) + else: + if self.bars.close[i] > exit_sma[i] and self.bars.close[i] > sma[i] and rsi[i] < 20: + self.buy_at_open(i + 1) + +if __name__ == "__main__": + strategy = MyStrategy() + strategy.add_feed(FinamCSVFeed('data/SBER_20100101_20161231_daily.csv')) + strategy.run(from_time='2012-01-01', to_time='2016-12-31') + print(strategy.get_analyzer('stats').generate_plain_text()) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6fff494 --- /dev/null +++ b/setup.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function + +import io +import re +from glob import glob +from os.path import basename +from os.path import dirname +from os.path import join +from os.path import splitext + +from setuptools import find_packages +from setuptools import setup + + +def read(*names, **kwargs): + return io.open( + join(dirname(__file__), *names), + encoding=kwargs.get('encoding', 'utf8') + ).read() + + +setup( + name='naiback', + version='0.1.0', + license='BSD 2-Clause License', + description='Naive backtester', + long_description='%s\n%s' % ( + re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), + re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) + ), + author='Denis Tereshkin', + author_email='denis@kasan.ws', + url='https://github.com/asakul/naiback', + packages=find_packages('src'), + package_dir={'': 'src'}, + py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + include_package_data=True, + zip_safe=False, + classifiers=[ + # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 1 - Planning', + 'Intended Audience :: Financial and Insurance Industry', + 'License :: OSI Approved :: MIT License', + 'Operating System :: Unix', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Office/Business :: Financial' + ], + keywords=[ + 'backtesting' + ], + install_requires=[ + ], + extras_require={ + }, + entry_points={ + }, +) diff --git a/src/naiback/__init__.py b/src/naiback/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/naiback/broker/__init__.py b/src/naiback/broker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/naiback/broker/broker.py b/src/naiback/broker/broker.py new file mode 100644 index 0000000..4a60cde --- /dev/null +++ b/src/naiback/broker/broker.py @@ -0,0 +1,14 @@ + +class Broker: + """ + Broker has several responsibilities (so called SRP, or several responsibilities principle): + 1) Track money amount on trading account + 2) Track active positions + 3) Subtract commissions/slippage + 4) Validate issued orders and reject them if needed + """ + + def __init__(self, initial_cash=100000.): + self.cash = initial_cash + self.positions = [] + diff --git a/src/naiback/broker/position.py b/src/naiback/broker/position.py new file mode 100644 index 0000000..0a42c11 --- /dev/null +++ b/src/naiback/broker/position.py @@ -0,0 +1,34 @@ + +class Position: + + def __init__(self, ticker): + self.ticker = ticker + self.entry_price = None + self.entry_metadata = {} + self.exit_price = None + self.exit_metadata = {} + self.size = None + self.total_pnl = 0 + + def entry_commission(self): + return self.entry_metadata['commission'] + + def entry_bar(self): + return self.entry_metadata['bar'] + + def pnl(self): + return self.total_pnl + + def enter(self, price, amount, **kwargs): + self.entry_price = price + self.size = amount + + for k, v in kwargs.items(): + self.entry_metadata[k] = v + + def exit(self, price): + self.exit_price = price + self.total_pnl += (self.exit_price - self.entry_price) * self.size + self.size = 0 + + diff --git a/src/naiback/data/__init__.py b/src/naiback/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/naiback/data/bars.py b/src/naiback/data/bars.py new file mode 100644 index 0000000..8ccdddf --- /dev/null +++ b/src/naiback/data/bars.py @@ -0,0 +1,37 @@ + +from ..exceptions import NaibackException + +class Bars: + """ + Basic bar series structure + """ + + def __init__(self): + self.index = [] + self.open = [] + self.high = [] + self.low = [] + self.close = [] + self.volume = [] + self.timestamp = [] + + def append_bar(self, open_, high, low, close, volume, timestamp): + """ + Appends OHLCV data + """ + self.index.append(len(self.open)) + self.open.append(open_) + self.high.append(high) + self.low.append(low) + self.close.append(close) + self.volume.append(volume) + self.timestamp.append(timestamp) + + @classmethod + def from_feed(feed): + if feed.type() != 'bars': + raise NaibackException('Invalid feed type: "{}", should be "bars"'.format(feed.type())) + bars = Bars() + for bar in feed.items(): + bars.append_bar(bar.open, bar.high, bar.low, bar.close, bar.volume, bar.timestamp) + return bars diff --git a/src/naiback/data/feed.py b/src/naiback/data/feed.py new file mode 100644 index 0000000..ea95a4e --- /dev/null +++ b/src/naiback/data/feed.py @@ -0,0 +1,8 @@ + +class Feed: + """ + Interface for data source + """ + + def __init__(self): + pass diff --git a/src/naiback/exceptions.py b/src/naiback/exceptions.py new file mode 100644 index 0000000..09b6fff --- /dev/null +++ b/src/naiback/exceptions.py @@ -0,0 +1,3 @@ + +class NaibackException(Exception): + pass diff --git a/src/naiback/strategy/__init__.py b/src/naiback/strategy/__init__.py new file mode 100644 index 0000000..d55518f --- /dev/null +++ b/src/naiback/strategy/__init__.py @@ -0,0 +1,3 @@ +__all__ = ['strategy'] +from .singleassetstrategy import SingleAssetStrategy +from .strategy import Strategy diff --git a/src/naiback/strategy/singleassetstrategy.py b/src/naiback/strategy/singleassetstrategy.py new file mode 100644 index 0000000..6dbfd44 --- /dev/null +++ b/src/naiback/strategy/singleassetstrategy.py @@ -0,0 +1,20 @@ + +from .strategy import Strategy +from ..data.bars import Bars +from ..exceptions import NaibackException + +class SingleAssetStrategy(Strategy): + + def __init__(self): + super.__init__() + self.bars = None + + def run(self, from_time=None, to_time=None): + self._prepare_bars() + super.run(from_time, to_time) + + def _prepare_bars(self): + if len(self.feeds) == 0: + raise NaibackException('No feeds added to strategy') + + self.bars = list(Bars.from_feed(self.feeds[0])) diff --git a/src/naiback/strategy/strategy.py b/src/naiback/strategy/strategy.py new file mode 100644 index 0000000..50dff0f --- /dev/null +++ b/src/naiback/strategy/strategy.py @@ -0,0 +1,28 @@ +from abc import abstractmethod + +class Strategy: + """ + Internal base class for strategies. User should use it's subclasses (i.e. SingleAssetStrategy) + """ + + def __init__(self): + self.feeds = [] + + def add_feed(self, feed): + """ + Adds feed to feeds list. + """ + self.feeds.append(feed) + + @abstractmethod + def execute(self): + """ + Will be called by 'run' + """ + pass + + def run(self, from_time=None, to_time=None): + """ + By default, just calls execute. + """ + self.execute() diff --git a/tests/test_bars.py b/tests/test_bars.py new file mode 100644 index 0000000..3a6dcb1 --- /dev/null +++ b/tests/test_bars.py @@ -0,0 +1,16 @@ + +import pytest +import datetime + +from naiback.data.bars import Bars + +def test_bar_append(): + bars = Bars() + bars.append_bar(10, 20, 5, 11, 100, datetime.datetime(2017, 1, 1)) + + assert bars.open[0] == 10 + assert bars.high[0] == 20 + assert bars.low[0] == 5 + assert bars.close[0] == 11 + assert bars.volume[0] == 100 + assert bars.timestamp[0] == datetime.datetime(2017, 1, 1) diff --git a/tests/test_position.py b/tests/test_position.py new file mode 100644 index 0000000..5ce6d20 --- /dev/null +++ b/tests/test_position.py @@ -0,0 +1,47 @@ + +import pytest +import datetime + +from naiback.broker.position import Position + +@pytest.fixture +def position(): + return Position('FOO') + +def test_position_enter(position): + position.enter(3.50, 10) + + assert position.entry_price == 3.50 + assert position.size == 10 + +def test_position_enter_metadata(position): + position.enter(3.50, 10, commission=0.1, bar=42) + + assert position.entry_metadata['commission'] == 0.1 + assert position.entry_metadata['bar'] == 42 + +def test_position_metadata_helpers(position): + position.enter(3.50, 10, commission=0.1, bar=42) + + assert position.entry_commission() == 0.1 + assert position.entry_bar() == 42 + +def test_position_exit(position): + position.enter(3.50, 10) + position.exit(4.50) + + assert position.exit_price == 4.50 + assert position.size == 0 + +def test_position_enter_short(position): + position.enter(3.50, -10) + + assert position.entry_price == 3.50 + assert position.size == -10 + +def test_position_exit_pnl(position): + position.enter(3.50, 10) + position.exit(4.00) + + assert position.pnl() == 5 +