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