commit
d7f8cbdc7d
18 changed files with 338 additions and 0 deletions
@ -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()) |
||||||
@ -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()) |
||||||
@ -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={ |
||||||
|
}, |
||||||
|
) |
||||||
@ -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 = [] |
||||||
|
|
||||||
@ -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 |
||||||
|
|
||||||
|
|
||||||
@ -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 |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
|
||||||
|
class Feed: |
||||||
|
""" |
||||||
|
Interface for data source |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
pass |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
|
||||||
|
class NaibackException(Exception): |
||||||
|
pass |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
__all__ = ['strategy'] |
||||||
|
from .singleassetstrategy import SingleAssetStrategy |
||||||
|
from .strategy import Strategy |
||||||
@ -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])) |
||||||
@ -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() |
||||||
@ -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) |
||||||
@ -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 |
||||||
|
|
||||||
Loading…
Reference in new issue