From 5c3e12f3c3d873d7146a0a6ea67bdc470762bd1f Mon Sep 17 00:00:00 2001 From: Caceresenzo Date: Wed, 17 Jan 2024 23:53:02 +0400 Subject: [PATCH 1/4] refacto!: change package name --- Makefile | 10 ++-- backtest/__init__.py | 4 -- bktest/__init__.py | 11 +++++ {backtest => bktest}/__main__.py | 0 bktest/__version__.py | 6 +++ {backtest => bktest}/account.py | 9 ++-- {backtest => bktest}/backtest.py | 0 {backtest => bktest}/cli.py | 0 {backtest => bktest}/constants.py | 0 {backtest => bktest}/data/__init__.py | 0 {backtest => bktest}/data/holidays.py | 0 {backtest => bktest}/data/source/__init__.py | 0 {backtest => bktest}/data/source/base.py | 0 .../data/source/coinmarketcap.py | 0 {backtest => bktest}/data/source/dataframe.py | 0 {backtest => bktest}/data/source/delegate.py | 0 {backtest => bktest}/data/source/factset.py | 0 {backtest => bktest}/data/source/yahoo.py | 0 {backtest => bktest}/export/__init__.py | 0 {backtest => bktest}/export/base.py | 0 {backtest => bktest}/export/console.py | 0 {backtest => bktest}/export/dump.py | 18 ++++---- {backtest => bktest}/export/influx.py | 0 {backtest => bktest}/export/model.py | 0 {backtest => bktest}/export/pdf.py | 0 {backtest => bktest}/export/quants.py | 0 .../export/specific_return.py | 0 {backtest => bktest}/fee.py | 3 +- {backtest => bktest}/holding.py | 0 {backtest => bktest}/order.py | 0 {backtest => bktest}/price_provider.py | 0 {backtest => bktest}/template/__init__.py | 0 {backtest => bktest}/template/models.py | 0 {backtest => bktest}/template/pdf.py | 0 {backtest => bktest}/template/sketch.py | 0 {backtest => bktest}/template/template.py | 0 {backtest => bktest}/utils.py | 0 example/yahoo.py | 15 +++--- main.py | 2 +- setup.py | 46 ++++++++++++++++--- tests/test_account.py | 45 +++++++++--------- tests/test_data_source.py | 2 +- tests/test_fee.py | 15 +++--- tests/test_holdings.py | 18 ++++---- tests/test_order.py | 31 +++++++------ tests/test_utils.py | 39 ++++++++-------- 46 files changed, 165 insertions(+), 109 deletions(-) delete mode 100644 backtest/__init__.py create mode 100644 bktest/__init__.py rename {backtest => bktest}/__main__.py (100%) create mode 100644 bktest/__version__.py rename {backtest => bktest}/account.py (95%) mode change 100755 => 100644 rename {backtest => bktest}/backtest.py (100%) mode change 100755 => 100644 rename {backtest => bktest}/cli.py (100%) mode change 100755 => 100644 rename {backtest => bktest}/constants.py (100%) rename {backtest => bktest}/data/__init__.py (100%) rename {backtest => bktest}/data/holidays.py (100%) rename {backtest => bktest}/data/source/__init__.py (100%) rename {backtest => bktest}/data/source/base.py (100%) rename {backtest => bktest}/data/source/coinmarketcap.py (100%) rename {backtest => bktest}/data/source/dataframe.py (100%) rename {backtest => bktest}/data/source/delegate.py (100%) rename {backtest => bktest}/data/source/factset.py (100%) rename {backtest => bktest}/data/source/yahoo.py (100%) rename {backtest => bktest}/export/__init__.py (100%) rename {backtest => bktest}/export/base.py (100%) rename {backtest => bktest}/export/console.py (100%) rename {backtest => bktest}/export/dump.py (97%) rename {backtest => bktest}/export/influx.py (100%) rename {backtest => bktest}/export/model.py (100%) rename {backtest => bktest}/export/pdf.py (100%) rename {backtest => bktest}/export/quants.py (100%) rename {backtest => bktest}/export/specific_return.py (100%) rename {backtest => bktest}/fee.py (99%) rename {backtest => bktest}/holding.py (100%) rename {backtest => bktest}/order.py (100%) rename {backtest => bktest}/price_provider.py (100%) rename {backtest => bktest}/template/__init__.py (100%) rename {backtest => bktest}/template/models.py (100%) rename {backtest => bktest}/template/pdf.py (100%) rename {backtest => bktest}/template/sketch.py (100%) rename {backtest => bktest}/template/template.py (100%) rename {backtest => bktest}/utils.py (100%) diff --git a/Makefile b/Makefile index 945b373..0c94aeb 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,14 @@ init: install: init $(PIP) install -e . +uninstall: + $(PIP) uninstall bktest + test: $(PYTHON) -m pytest -v -example: - PYTHONPATH=. find example/ -name "*.py" -exec $(PYTHON) {} \; +build: + rm -rf build *.egg-info dist + python setup.py sdist bdist_wheel -.PHONY: init install test example +.PHONY: init install uninstall test build diff --git a/backtest/__init__.py b/backtest/__init__.py deleted file mode 100644 index 7ebbb41..0000000 --- a/backtest/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .backtest import Backtester -from .account import * -from .holding import * -from .order import * \ No newline at end of file diff --git a/bktest/__init__.py b/bktest/__init__.py new file mode 100644 index 0000000..7a93d1f --- /dev/null +++ b/bktest/__init__.py @@ -0,0 +1,11 @@ +""" +bktest +~~~~~~ +The backtest package - a Python cli used simply +backtest your portfolio! +""" + +from .backtest import Backtester +from .account import * +from .holding import * +from .order import * diff --git a/backtest/__main__.py b/bktest/__main__.py similarity index 100% rename from backtest/__main__.py rename to bktest/__main__.py diff --git a/bktest/__version__.py b/bktest/__version__.py new file mode 100644 index 0000000..045aa41 --- /dev/null +++ b/bktest/__version__.py @@ -0,0 +1,6 @@ +__title__ = 'bktest' +__description__ = 'bktest - A simple backtester by CrunchDAO' +__version__ = '1.0.0' +__author__ = 'Enzo CACERES' +__author_email__ = 'enzo.caceres@crunchdao.com' +__url__ = 'https://github.com/crunchdao/backtest' diff --git a/backtest/account.py b/bktest/account.py old mode 100755 new mode 100644 similarity index 95% rename from backtest/account.py rename to bktest/account.py index 95fd5be..77af66b --- a/backtest/account.py +++ b/bktest/account.py @@ -1,11 +1,10 @@ import sys import typing -from backtest.utils import is_blank - +from .fee import ConstantFeeModel, FeeModel from .holding import Holding -from .order import Order, OrderResult, CloseResult -from .fee import FeeModel, ConstantFeeModel +from .order import CloseResult, Order, OrderResult +from .utils import is_blank class Account: @@ -122,4 +121,4 @@ def _handle_cash(self, order: Order, fee: float): def interest_on_cash(self, rfr): '''Interest income on cash from short sale - amount borrowed on the long position''' - self.cash += self.cash * ((((max((rfr - 0.17), 0) / 100) + 1) ** (1/365)) - 1) \ No newline at end of file + self.cash += self.cash * ((((max((rfr - 0.17), 0) / 100) + 1) ** (1/365)) - 1) diff --git a/backtest/backtest.py b/bktest/backtest.py old mode 100755 new mode 100644 similarity index 100% rename from backtest/backtest.py rename to bktest/backtest.py diff --git a/backtest/cli.py b/bktest/cli.py old mode 100755 new mode 100644 similarity index 100% rename from backtest/cli.py rename to bktest/cli.py diff --git a/backtest/constants.py b/bktest/constants.py similarity index 100% rename from backtest/constants.py rename to bktest/constants.py diff --git a/backtest/data/__init__.py b/bktest/data/__init__.py similarity index 100% rename from backtest/data/__init__.py rename to bktest/data/__init__.py diff --git a/backtest/data/holidays.py b/bktest/data/holidays.py similarity index 100% rename from backtest/data/holidays.py rename to bktest/data/holidays.py diff --git a/backtest/data/source/__init__.py b/bktest/data/source/__init__.py similarity index 100% rename from backtest/data/source/__init__.py rename to bktest/data/source/__init__.py diff --git a/backtest/data/source/base.py b/bktest/data/source/base.py similarity index 100% rename from backtest/data/source/base.py rename to bktest/data/source/base.py diff --git a/backtest/data/source/coinmarketcap.py b/bktest/data/source/coinmarketcap.py similarity index 100% rename from backtest/data/source/coinmarketcap.py rename to bktest/data/source/coinmarketcap.py diff --git a/backtest/data/source/dataframe.py b/bktest/data/source/dataframe.py similarity index 100% rename from backtest/data/source/dataframe.py rename to bktest/data/source/dataframe.py diff --git a/backtest/data/source/delegate.py b/bktest/data/source/delegate.py similarity index 100% rename from backtest/data/source/delegate.py rename to bktest/data/source/delegate.py diff --git a/backtest/data/source/factset.py b/bktest/data/source/factset.py similarity index 100% rename from backtest/data/source/factset.py rename to bktest/data/source/factset.py diff --git a/backtest/data/source/yahoo.py b/bktest/data/source/yahoo.py similarity index 100% rename from backtest/data/source/yahoo.py rename to bktest/data/source/yahoo.py diff --git a/backtest/export/__init__.py b/bktest/export/__init__.py similarity index 100% rename from backtest/export/__init__.py rename to bktest/export/__init__.py diff --git a/backtest/export/base.py b/bktest/export/base.py similarity index 100% rename from backtest/export/base.py rename to bktest/export/base.py diff --git a/backtest/export/console.py b/bktest/export/console.py similarity index 100% rename from backtest/export/console.py rename to bktest/export/console.py diff --git a/backtest/export/dump.py b/bktest/export/dump.py similarity index 97% rename from backtest/export/dump.py rename to bktest/export/dump.py index 9390275..ce2ac43 100644 --- a/backtest/export/dump.py +++ b/bktest/export/dump.py @@ -1,14 +1,14 @@ import abc +import datetime +import math import os import sys -import math import typing -import datetime - -from ..utils import signum import pandas +import readwrite +from ..utils import signum from .base import BaseExporter from .model import Snapshot @@ -24,7 +24,7 @@ def __init__( self.output_file = output_file self.auto_delete = auto_delete self.auto_override = auto_override - + self.all_dates = set() self.rows = [] @@ -53,7 +53,7 @@ def on_snapshot(self, snapshot: Snapshot) -> None: if snapshot.postponned is not None: date = snapshot.postponned - + self.rows.extend([ ( date, @@ -78,13 +78,13 @@ def finalize(self) -> None: self.rows, columns=["date", "symbol", "quantity", "price", "market_price", "equity"] ) - + if not len(self.dataframe): print( "[warning] cannot create dump: dataframe is empty", file=sys.stderr ) - + return def inverse_sign_if_shorting(row: pandas.Series): @@ -117,7 +117,7 @@ def compute_profit(group: pandas.DataFrame): if self.output_file is not None: if self.auto_override or not os.path.exists(self.output_file): - self.dataframe.to_csv(self.output_file) + readwrite.write(self.dataframe, self.output_file) else: print( f"[warning] {self.output_file} already exists", diff --git a/backtest/export/influx.py b/bktest/export/influx.py similarity index 100% rename from backtest/export/influx.py rename to bktest/export/influx.py diff --git a/backtest/export/model.py b/bktest/export/model.py similarity index 100% rename from backtest/export/model.py rename to bktest/export/model.py diff --git a/backtest/export/pdf.py b/bktest/export/pdf.py similarity index 100% rename from backtest/export/pdf.py rename to bktest/export/pdf.py diff --git a/backtest/export/quants.py b/bktest/export/quants.py similarity index 100% rename from backtest/export/quants.py rename to bktest/export/quants.py diff --git a/backtest/export/specific_return.py b/bktest/export/specific_return.py similarity index 100% rename from backtest/export/specific_return.py rename to bktest/export/specific_return.py diff --git a/backtest/fee.py b/bktest/fee.py similarity index 99% rename from backtest/fee.py rename to bktest/fee.py index 676c260..b20082d 100644 --- a/backtest/fee.py +++ b/bktest/fee.py @@ -1,8 +1,9 @@ import abc -from .order import Order import py_expression_eval +from .order import Order + class FeeModel(metaclass=abc.ABCMeta): diff --git a/backtest/holding.py b/bktest/holding.py similarity index 100% rename from backtest/holding.py rename to bktest/holding.py diff --git a/backtest/order.py b/bktest/order.py similarity index 100% rename from backtest/order.py rename to bktest/order.py diff --git a/backtest/price_provider.py b/bktest/price_provider.py similarity index 100% rename from backtest/price_provider.py rename to bktest/price_provider.py diff --git a/backtest/template/__init__.py b/bktest/template/__init__.py similarity index 100% rename from backtest/template/__init__.py rename to bktest/template/__init__.py diff --git a/backtest/template/models.py b/bktest/template/models.py similarity index 100% rename from backtest/template/models.py rename to bktest/template/models.py diff --git a/backtest/template/pdf.py b/bktest/template/pdf.py similarity index 100% rename from backtest/template/pdf.py rename to bktest/template/pdf.py diff --git a/backtest/template/sketch.py b/bktest/template/sketch.py similarity index 100% rename from backtest/template/sketch.py rename to bktest/template/sketch.py diff --git a/backtest/template/template.py b/bktest/template/template.py similarity index 100% rename from backtest/template/template.py rename to bktest/template/template.py diff --git a/backtest/utils.py b/bktest/utils.py similarity index 100% rename from backtest/utils.py rename to bktest/utils.py diff --git a/example/yahoo.py b/example/yahoo.py index f04ff3b..c20cfb1 100644 --- a/example/yahoo.py +++ b/example/yahoo.py @@ -1,16 +1,17 @@ import datetime -import backtest import pandas +import bktest + end = datetime.date.today() start = end - datetime.timedelta(days=38) initial_cash = 1_000_000 quantity_in_decimal = False -data_source = backtest.data.source.YahooDataSource() +data_source = bktest.data.source.YahooDataSource() -order_provider = backtest.order.provider.DataFrameOrderProvider(pandas.DataFrame([ +order_provider = bktest.order.provider.DataFrameOrderProvider(pandas.DataFrame([ {"symbol": "AAPL", "quantity": +50, "date": (start + datetime.timedelta(days=10)).isoformat()}, {"symbol": "TSLA", "quantity": -25, "date": (start + datetime.timedelta(days=10)).isoformat()}, {"symbol": "AAPL", "quantity": -40, "date": (start + datetime.timedelta(days=20)).isoformat()}, @@ -18,11 +19,11 @@ {"symbol": "AAPL", "quantity": +50, "date": (start + datetime.timedelta(days=30)).isoformat()}, ])) -fee_model = backtest.fee.ExpressionFeeModel( +fee_model = bktest.fee.ExpressionFeeModel( "abs(price * quantity) * 0.1" ) -backtest.Backtester( +bktest.Backtester( start=start, end=end, order_provider=order_provider, @@ -31,8 +32,8 @@ data_source=data_source, fee_model=fee_model, exporters=[ - backtest.export.ConsoleExporter(), - backtest.export.DumpExporter( + bktest.export.ConsoleExporter(), + bktest.export.DumpExporter( auto_override=True ), # backtest.export.QuantStatsExporter( diff --git a/main.py b/main.py index 13ea870..f618588 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,3 @@ -from backtest import cli +from bktest import cli cli.cli() diff --git a/setup.py b/setup.py index c1cf6c0..81c0287 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,48 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 +import os from setuptools import setup, find_packages +package = "bktest" + +about = {} +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, package, '__version__.py')) as f: + exec(f.read(), about) + with open('requirements.txt') as fd: requirements = fd.read().splitlines() +with open('README.md') as fd: + readme = fd.read() + setup( - name='crunchdao-backtest', - version='1.0.1', - description='CrunchDAO backtester', - author='Enzo Caceres, CrunchDAO', - author_email='enzo.caceres@crunchdao.com', + name=about['__title__'], + description=about['__description__'], + long_description=readme, + long_description_content_type='text/markdown', + version=about['__version__'], + author=about['__author__'], + author_email=about['__author_email__'], + url=about['__url__'], packages=find_packages(), + package_data={ + package: [ + "demo-project/*", + "demo-project/.*", + ] + }, + include_package_data=True, + python_requires=">=3", install_requires=requirements, -) \ No newline at end of file + zip_safe=False, + entry_points={ + 'console_scripts': ['bktest=bktest.main:cli'], + 'console_scripts': ['backtest=bktest.main:cli'], + }, + classifiers=[ + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3.7', + ], + keywords='package development template' +) diff --git a/tests/test_account.py b/tests/test_account.py index 88aafc8..7a87a01 100755 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,25 +1,26 @@ import typing import unittest -import backtest -aapl = backtest.Order("AAPL", 42, 1) -aapl_hold = backtest.Order("AAPL", 0, 1) -aapl_short = backtest.Order("AAPL", -42, 1) +import bktest + +aapl = bktest.Order("AAPL", 42, 1) +aapl_hold = bktest.Order("AAPL", 0, 1) +aapl_short = bktest.Order("AAPL", -42, 1) cash = 1000000 rfr = 4.17 class AccountTest(unittest.TestCase): def test_place_order(self): - account = backtest.Account() + account = bktest.Account() - order = backtest.Order(None, 1, 1) + order = bktest.Order(None, 1, 1) result = account.place_order(order) self.assertEqual(order, result.order) self.assertFalse(result.success) self.assertEqual(0, len(account.holdings)) - aapl = backtest.Order("AAPL", 15, 2) + aapl = bktest.Order("AAPL", 15, 2) result = account.place_order(aapl) self.assertTrue(result.success) self.assertEqual(1, len(account.holdings)) @@ -30,13 +31,13 @@ def test_place_order(self): self.assertEqual(1, len(account.holdings)) self.assertEqual(account.cash + aapl.value * 2, account.equity) - tsla = backtest.Order("TSLA", 15, 2) + tsla = bktest.Order("TSLA", 15, 2) result = account.place_order(tsla) self.assertTrue(result.success) self.assertEqual(2, len(account.holdings)) self.assertEqual(account.cash + tsla.value + aapl.value * 2, account.equity) - aapl_short = backtest.Order("AAPL", -15, 2) + aapl_short = bktest.Order("AAPL", -15, 2) result = account.place_order(aapl_short) self.assertEqual(2, len(account.holdings)) @@ -47,9 +48,9 @@ def test_place_order(self): self.assertEqual(account.cash + tsla.value, account.equity) def test_order_position(self): - account = backtest.Account() + account = bktest.Account() - result = account.order_position(backtest.Order(None, 1, 1)) + result = account.order_position(bktest.Order(None, 1, 1)) self.assertFalse(result.success) result = account.order_position(aapl) @@ -77,7 +78,7 @@ def test_order_position(self): self.assertEqual(aapl_short.price, holding.price) def test_close_position(self): - account = backtest.Account() + account = bktest.Account() result = account.close_position(None) self.assertFalse(result.success) @@ -92,7 +93,7 @@ def test_close_position(self): self.assertEqual(0, result.order.quantity) self.assertEqual(42.0, result.order.price) - aapl = backtest.Order("AAPL", 15, 2) + aapl = bktest.Order("AAPL", 15, 2) result = account.place_order(aapl) self.assertTrue(result.success) self.assertEqual(1, len(account.holdings)) @@ -137,7 +138,7 @@ def test_find_holding(self): def test_interest_on_cash(self): cash_after_interest = cash * (1 + max((rfr - 0.17), 0) / 100) - account = backtest.Account() + account = bktest.Account() day = 1 while day <= 365: account.interest_on_cash(rfr) @@ -146,7 +147,7 @@ def test_interest_on_cash(self): self.assertEqual(cash_after_interest, round(account.cash)) def test_to_relative_order(self): - account = backtest.Account() + account = bktest.Account() self.assertEqual(aapl, account.to_relative_order(aapl)) self.assertEqual(aapl_hold, account.to_relative_order(aapl_hold)) @@ -164,11 +165,11 @@ def test_to_relative_order(self): relative = account.to_relative_order(aapl_short) self.assertEqual(aapl_short.quantity * 2, relative.quantity) - order = backtest.Order("AAPL", 44, 1) + order = bktest.Order("AAPL", 44, 1) relative = account.to_relative_order(order) self.assertEqual(2, relative.quantity) - account = backtest.Account() + account = bktest.Account() result = account.place_order(aapl_short) relative = account.to_relative_order(aapl_short) @@ -180,16 +181,16 @@ def test_to_relative_order(self): relative = account.to_relative_order(aapl) self.assertEqual(aapl.quantity * 2, relative.quantity) - order = backtest.Order("AAPL", -44, 1) + order = bktest.Order("AAPL", -44, 1) relative = account.to_relative_order(order) self.assertEqual(-2, relative.quantity) @staticmethod - def _create_dummy(add=True) -> typing.Tuple[backtest.Account, backtest.Holding, backtest.Holding]: - account = backtest.Account() + def _create_dummy(add=True) -> typing.Tuple[bktest.Account, bktest.Holding, bktest.Holding]: + account = bktest.Account() - aapl = backtest.Holding("AAPL", 15, 2) - tsla = backtest.Holding("TSLA", 30, 4) + aapl = bktest.Holding("AAPL", 15, 2) + tsla = bktest.Holding("TSLA", 30, 4) for holding in [aapl, tsla]: account._holdings[holding.symbol] = holding diff --git a/tests/test_data_source.py b/tests/test_data_source.py index 7e89d97..a70d98f 100644 --- a/tests/test_data_source.py +++ b/tests/test_data_source.py @@ -1,6 +1,6 @@ import unittest -from backtest.data.source import DataSource +from bktest.data.source import DataSource class DataSourceTest(unittest.TestCase): diff --git a/tests/test_fee.py b/tests/test_fee.py index dc24374..7be597a 100644 --- a/tests/test_fee.py +++ b/tests/test_fee.py @@ -1,11 +1,12 @@ import unittest -import backtest + +import bktest class ConstantFeeModelTest(unittest.TestCase): def test_get_order_fee(self): - model = backtest.fee.ConstantFeeModel(5) + model = bktest.fee.ConstantFeeModel(5) self.assertEqual(5, model.get_order_fee(None)) @@ -13,16 +14,16 @@ def test_get_order_fee(self): class ExpressionFeeModelTest(unittest.TestCase): def test_get_order_fee(self): - model = backtest.fee.ExpressionFeeModel("5 * price * quantity") - order = backtest.Order("AAPL", 15, 2) + model = bktest.fee.ExpressionFeeModel("5 * price * quantity") + order = bktest.Order("AAPL", 15, 2) self.assertEqual(5 * order.price * order.quantity, model.get_order_fee(order)) def test_get_order_fee_interactive_broker(self): - model = backtest.fee.ExpressionFeeModel("max(abs(price * quantity) * 0.01, 1)") + model = bktest.fee.ExpressionFeeModel("max(abs(price * quantity) * 0.01, 1)") - order = backtest.Order("AAPL", 1, 20) + order = bktest.Order("AAPL", 1, 20) self.assertEqual(1, model.get_order_fee(order)) - order = backtest.Order("AAPL", 150, 20) + order = bktest.Order("AAPL", 150, 20) self.assertEqual(30, model.get_order_fee(order)) diff --git a/tests/test_holdings.py b/tests/test_holdings.py index f93bb5b..03e33be 100644 --- a/tests/test_holdings.py +++ b/tests/test_holdings.py @@ -1,16 +1,18 @@ import unittest -import backtest + +import bktest + class HoldingTest(unittest.TestCase): def test_market_price(self): - holding = backtest.Holding("AAPL", 15, 2) + holding = bktest.Holding("AAPL", 15, 2) self.assertEqual(15*2, holding.market_price) def test_merge(self): - holding = backtest.Holding("AAPL", 15, 2, False) - order = backtest.Order("AAPL", 30, 4) + holding = bktest.Holding("AAPL", 15, 2, False) + order = bktest.Order("AAPL", 30, 4) expected_quantity = holding.quantity + order.quantity @@ -21,18 +23,18 @@ def test_merge(self): self.assertTrue(holding.up_to_date) def test_str(self): - holding = backtest.Holding("AAPL", 15, 2) + holding = bktest.Holding("AAPL", 15, 2) self.assertEqual(str(holding), "AAPLx15@2") def test_repr(self): - holding = backtest.Holding("AAPL", 15, 2) + holding = bktest.Holding("AAPL", 15, 2) self.assertEqual(repr(holding), "AAPLx15") def from_order(self): - order = backtest.Order("AAPL", 15, 2) - holding = backtest.Holding.from_order(order) + order = bktest.Order("AAPL", 15, 2) + holding = bktest.Holding.from_order(order) self.assertEqual(order.symbol, holding.symbol) self.assertEqual(order.quantity, holding.quantity) diff --git a/tests/test_order.py b/tests/test_order.py index cbc57ba..c25935e 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -1,42 +1,43 @@ import unittest -import backtest + +import bktest class OrderTest(unittest.TestCase): def test_value(self): - order = backtest.Order("AAPL", 15, 2) + order = bktest.Order("AAPL", 15, 2) self.assertEqual(15*2, order.value) def test_direction(self): - order = backtest.Order("AAPL", 15, 2) - self.assertEqual(backtest.OrderDirection.BUY, order.direction) + order = bktest.Order("AAPL", 15, 2) + self.assertEqual(bktest.OrderDirection.BUY, order.direction) - order = backtest.Order("AAPL", 0, 2) - self.assertEqual(backtest.OrderDirection.HOLD, order.direction) + order = bktest.Order("AAPL", 0, 2) + self.assertEqual(bktest.OrderDirection.HOLD, order.direction) - order = backtest.Order("AAPL", -15, 2) - self.assertEqual(backtest.OrderDirection.SELL, order.direction) + order = bktest.Order("AAPL", -15, 2) + self.assertEqual(bktest.OrderDirection.SELL, order.direction) def test_valid(self): - order = backtest.Order(None, 15, 2) + order = bktest.Order(None, 15, 2) self.assertFalse(order.valid) - order = backtest.Order("", 15, 2) + order = bktest.Order("", 15, 2) self.assertFalse(order.valid) - order = backtest.Order(" ", 15, 2) + order = bktest.Order(" ", 15, 2) self.assertFalse(order.valid) - order = backtest.Order("AAPL", 15, 0) + order = bktest.Order("AAPL", 15, 0) self.assertFalse(order.valid) - order = backtest.Order("AAPL", 15, -5) + order = bktest.Order("AAPL", 15, -5) self.assertFalse(order.valid) - order = backtest.Order("AAPL", 15, 5) + order = bktest.Order("AAPL", 15, 5) self.assertTrue(order.valid) - order = backtest.Order("AAPL", -15, 5) + order = bktest.Order("AAPL", -15, 5) self.assertTrue(order.valid) diff --git a/tests/test_utils.py b/tests/test_utils.py index e512563..82091a8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,43 +1,44 @@ import unittest -import backtest.utils + +import bktest.utils class UtilsTest(unittest.TestCase): def test_signum(self): - self.assertEqual(backtest.utils.signum(1), 1) - self.assertEqual(backtest.utils.signum(0), 0) - self.assertEqual(backtest.utils.signum(-1), -1) + self.assertEqual(bktest.utils.signum(1), 1) + self.assertEqual(bktest.utils.signum(0), 0) + self.assertEqual(bktest.utils.signum(-1), -1) def test_is_int(self): - self.assertTrue(backtest.utils.is_int("1")) + self.assertTrue(bktest.utils.is_int("1")) - self.assertFalse(backtest.utils.is_int("1.5")) - self.assertFalse(backtest.utils.is_int("hello")) + self.assertFalse(bktest.utils.is_int("1.5")) + self.assertFalse(bktest.utils.is_int("hello")) def test_is_float(self): - self.assertTrue(backtest.utils.is_float("1.5")) - self.assertTrue(backtest.utils.is_float("1")) + self.assertTrue(bktest.utils.is_float("1.5")) + self.assertTrue(bktest.utils.is_float("1")) - self.assertFalse(backtest.utils.is_float("hello")) + self.assertFalse(bktest.utils.is_float("hello")) def test_is_number(self): - self.assertTrue(backtest.utils.is_number("1.5")) - self.assertTrue(backtest.utils.is_number("1")) + self.assertTrue(bktest.utils.is_number("1.5")) + self.assertTrue(bktest.utils.is_number("1")) - self.assertFalse(backtest.utils.is_number("hello")) + self.assertFalse(bktest.utils.is_number("hello")) def test_is_not_blank(self): - self.assertTrue(backtest.utils.is_blank(None)) - self.assertTrue(backtest.utils.is_blank("")) - self.assertTrue(backtest.utils.is_blank(" ")) + self.assertTrue(bktest.utils.is_blank(None)) + self.assertTrue(bktest.utils.is_blank("")) + self.assertTrue(bktest.utils.is_blank(" ")) - self.assertFalse(backtest.utils.is_blank("hello")) + self.assertFalse(bktest.utils.is_blank("hello")) def test_ensure_not_blank(self): def case(value: str, property: str, message: str): with self.assertRaises(ValueError) as context: - backtest.utils.ensure_not_blank(value, property) + bktest.utils.ensure_not_blank(value, property) exception = context.exception self.assertEquals(message, str(exception)) @@ -49,4 +50,4 @@ def case(value: str, property: str, message: str): case(" ", None, "must not be blank") case(" ", "dummy", "dummy must not be blank") - self.assertEqual("hello", backtest.utils.ensure_not_blank("hello")) + self.assertEqual("hello", bktest.utils.ensure_not_blank("hello")) From 0b9e9e9e8ff38b8aeb686409abfc6e8c7254eb2c Mon Sep 17 00:00:00 2001 From: Caceresenzo Date: Wed, 17 Jan 2024 23:54:34 +0400 Subject: [PATCH 2/4] feat(pypi): add workflow --- .github/workflows/pypi.yml | 36 ++++++++++++++++++++++++++++++++++++ .github/workflows/pytest.yml | 1 - 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pypi.yml diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..eb1ac22 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,36 @@ +name: PyPI Publish + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + + - name: Install tools + run: | + python -m pip install --upgrade pip + pip install --upgrade setuptools wheel + + - name: Build Package + run: make build + + - name: Publish Package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + verbose: true + print-hash: true diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d2f2ce5..4822876 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,4 +1,3 @@ -# .github/workflows/app.yaml name: PyTest on: push From eebe947a4a218a66449e82cd8254d4bc88b24fc5 Mon Sep 17 00:00:00 2001 From: Caceresenzo Date: Wed, 17 Jan 2024 23:55:44 +0400 Subject: [PATCH 3/4] feat(readme): update --- README.md | 73 ++++++++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 61690bb..88f6931 100755 --- a/README.md +++ b/README.md @@ -1,42 +1,39 @@ -# crunchdao-backtest - A small backtesting utility. ![image](https://user-images.githubusercontent.com/7386242/189368488-b0c30d48-9a1f-4362-9f78-10a451535682.png) [![PyTest](https://github.com/crunchdao/backtest/actions/workflows/pytest.yml/badge.svg)](https://github.com/crunchdao/backtest/actions/workflows/pytest.yml) -- [crunchdao-backtest](#crunchdao-backtest) - - [Install](#install) - - [Usage](#usage) - - [Options](#options) - - [Exporters](#exporters) - - [Console](#console) - - [Dump](#dump) - - [Influx](#influx) - - [QuantStats](#quantstats) - - [PDF](#pdf) - - [Specific Return](#specific-return) - - [Data Sources](#data-sources) - - [Yahoo](#yahoo) - - [CoinMarketCap](#coinmarketcap) - - [FactSet](#factset) - - [File](#file) - - [.parquet](#parquet) - -## Install +- [Install](#install) +- [Usage](#usage) + - [Options](#options) + - [Exporters](#exporters) + - [Console](#console) + - [Dump](#dump) + - [Influx](#influx) + - [QuantStats](#quantstats) + - [PDF](#pdf) + - [Specific Return](#specific-return) + - [Data Sources](#data-sources) + - [Yahoo](#yahoo) + - [CoinMarketCap](#coinmarketcap) + - [FactSet](#factset) + - [File](#file) + - [.parquet](#parquet) + +# Install ``` -python3 -m pip install --upgrade git+https://github.com/crunchdao/backtest +pip install --upgrade bktest ``` -## Usage +# Usage ```bash -python3 -m backtest [OPTIONS] +bktest [OPTIONS] ``` -### Options +## Options | Option | Value | Default | Format | Description | | --- | --- | --- | --- | --- | @@ -61,11 +58,11 @@ python3 -m backtest [OPTIONS] | `--rfr-file` | `` | | `path` | The directory of rfr file to use. The file must contain a column with date information and a column with the rfr information in %. | | `--rfr-file-column-date` | `` | `date` | `string` | Change the date column name to use. | -#### Exporters +### Exporters Multiple exporters can be enabled at one time. -##### Console +#### Console The console exporter allows a quick look at the backtest. @@ -77,7 +74,7 @@ The console exporter allows a quick look at the backtest. | `--console-hide-skips` | | `false` | | Do not the skipped days. | | `--console-text-no-color` | | `false` | | Disable colors in the output. (only if the format is `text`) | -##### Dump +#### Dump The dump exporter generate a dump of the portfolio at each day. @@ -87,7 +84,7 @@ The dump exporter generate a dump of the portfolio at each day. | `--dump-output-file` | `` | `dump.csv` | `path` | Specify the output file. | | `--dump-auto-delete` | | `false` | | Automatically delete the previous dump file if it is present. | -##### Influx +#### Influx Export the generated data to an Influx database.
Making it easier to plot the values using software like Grafana. @@ -101,7 +98,7 @@ Making it easier to plot the values using software like Grafana. | `--influx-measurement` | `` | `snapshots` |`string` | Specify the table name to use. | | `--influx-key` | `` | `test` | `string` | Specify the unique key to use. **Previous data with the same key will be deleted!** | -##### QuantStats +#### QuantStats Generate a tearsheet from the backtest data. @@ -113,7 +110,7 @@ Generate a tearsheet from the backtest data. | `--quantstats-benchmark-ticker` | `` | `SPY` | `symbol` | Specify the ticker to use as a benchmark in the tearsheet. | | `--quantstats-auto-delete` | | `false` | | Automatically delete the previous report files if they are present. | -##### PDF +#### PDF Generate a tearsheet from a custom template. @@ -127,7 +124,7 @@ Generate a tearsheet from a custom template. | `--pdf-variable` | `[ ]` | `[]` | `string` `string` | Add a custom variable. | | `--pdf-user-script` | `[]` | `[]` | `path` | Add a user script. | -##### Specific Return +#### Specific Return Generate a tearsheet from the specific return backtest data. @@ -141,17 +138,17 @@ Generate a tearsheet from the specific return backtest data. | `--specific-return-output-file-csv` | `` | `sr-report.csv` | `path` | Specify the output file containing raw returns. | | `--specific-return-auto-delete` | | `false` | | Automatically delete the previous report files if they are present. | -#### Data Sources +### Data Sources Only one data source can be used at once. -##### Yahoo +#### Yahoo | Option | Value | Default | Format | Description | | --- | --- | --- | --- | --- | | `--yahoo` | | `false` | | Enable yahoo as the data source. | -##### CoinMarketCap +#### CoinMarketCap | Option | Value | Default | Format | Description | | --- | --- | --- | --- | --- | @@ -159,7 +156,7 @@ Only one data source can be used at once. | `--coinmarketcap-force-mapping-refresh` | | `false` | | Force a mapping refresh. This is usually only done automatically the first time of using this data source. | | `--coinmarketcap-page-size` | `` | `10_000` | `number` | Specify the page size while building the mapping. | -##### FactSet +#### FactSet | Option | Value | Default | Format | Description | | --- | --- | --- | --- | --- | @@ -167,13 +164,13 @@ Only one data source can be used at once. | `--factset-username-serial` | | `$FACTSET_USERNAME_SERIAL` | | Specify the factset's username serial to use. | | `--factset-api-key` | | `$FACTSET_API_KEY` | | Specify the factset's api key to use. | -##### File +#### File Use a static file as a price data source. If another data source is specified, files sources will be used first in a delegated data source. Meaning that if the data are not available in the file, the next data source will be used. -###### .parquet +##### .parquet | Option | Value | Default | Format | Description | | --- | --- | --- | --- | --- | From 5f865e760a0c7bb0fd704c6611383c4bae902a71 Mon Sep 17 00:00:00 2001 From: Caceresenzo Date: Wed, 17 Jan 2024 23:55:52 +0400 Subject: [PATCH 4/4] fix(requirements): add `readwrite` --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 2f6bcc6..91ba0b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ fpdf2==2.7.4 contexttimer watchdog fastparquet +readwrite