├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── HISTORY.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── README.rst ├── coverage.cfg ├── docker ├── Dockerfile └── build.sh ├── docs ├── Makefile ├── _templates │ └── layout.html ├── barfeed.rst ├── bitcoin.rst ├── bitcoincharts.rst ├── bitcoincharts_example.rst ├── bitcoincharts_ref.rst ├── bitstamp.rst ├── bitstamp_example.rst ├── bitstamp_ref.rst ├── broker.rst ├── compinvpart1.rst ├── conf.py ├── eventprofiler.rst ├── images │ ├── execution_running_1.png │ └── queue_execution.png ├── index.rst ├── intro.rst ├── optimizer.rst ├── sample_bbands.rst ├── sample_market_timing.rst ├── sample_quandl.rst ├── sample_rsi2.rst ├── sample_sma_crossover.rst ├── sample_statarb_erniechan.rst ├── sample_vwap_momentum.rst ├── samples.rst ├── talib.rst ├── tools.rst └── tutorial.rst ├── histdata └── .gitkeep ├── mooquant ├── __init__.py ├── analyzer │ ├── __init__.py │ ├── drawdown.py │ ├── returns.py │ ├── sharpe.py │ └── trades.py ├── bar.py ├── barfeed │ ├── __init__.py │ ├── common.py │ ├── csvfeed.py │ ├── dbfeed.py │ ├── membf.py │ ├── mootdxfeed.py │ ├── ninjatraderfeed.py │ ├── quandlfeed.py │ ├── resampled.py │ ├── sqlitefeed.py │ └── tusharefeed.py ├── broker │ ├── __init__.py │ ├── backtesting.py │ ├── fillstrategy.py │ └── slippage.py ├── dataseries │ ├── __init__.py │ ├── aligned.py │ ├── bards.py │ └── resampled.py ├── dispatcher.py ├── dispatchprio.py ├── eventprofiler.py ├── feed │ ├── __init__.py │ ├── csvfeed.py │ └── memfeed.py ├── logger.py ├── marketsession.py ├── observer.py ├── optimizer │ ├── __init__.py │ ├── base.py │ ├── local.py │ ├── server.py │ ├── worker.py │ ├── xmlrpcserver.py │ └── zmqrpcserver.py ├── plotter.py ├── provider │ ├── __init__.py │ ├── bar.py │ ├── bitcoincharts │ │ ├── __init__.py │ │ └── barfeed.py │ ├── bitstamp │ │ ├── __init__.py │ │ ├── barfeed.py │ │ ├── broker.py │ │ ├── common.py │ │ ├── httpclient.py │ │ ├── livebroker.py │ │ ├── livefeed.py │ │ └── wsclient.py │ ├── csvfeed.py │ └── xignite │ │ ├── __init__.py │ │ ├── api.py │ │ └── barfeed.py ├── resamplebase.py ├── strategy │ ├── __init__.py │ └── position.py ├── talibext │ ├── __init__.py │ └── indicator.py ├── technical │ ├── __init__.py │ ├── atr.py │ ├── bollinger.py │ ├── cross.py │ ├── cumret.py │ ├── highlow.py │ ├── hurst.py │ ├── linebreak.py │ ├── linreg.py │ ├── ma.py │ ├── macd.py │ ├── ratio.py │ ├── roc.py │ ├── rsi.py │ ├── stats.py │ ├── stoch.py │ └── vwap.py ├── tools │ ├── __init__.py │ ├── mootdx.py │ ├── quandl.py │ ├── resample.py │ └── tushare.py ├── utils │ ├── __init__.py │ ├── collections.py │ ├── csvutils.py │ ├── dt.py │ └── stats.py ├── warninghelpers.py └── websocket │ ├── __init__.py │ ├── client.py │ └── pusher.py ├── requirements.in ├── requirements.txt ├── samples ├── bbands.py ├── bccharts_example_1.py ├── bccharts_example_2.py ├── compinv-1.py ├── compinv-3.py ├── csvfeed_1.py ├── eventstudy.py ├── market_timing.py ├── quandl_sample.py ├── rsi2.py ├── rsi2_sample.py ├── sample-strategy-analyzer.py ├── sample.py ├── sample_05.py ├── sample_golds.py ├── sample_mootdx.py ├── sample_quandl.py ├── sample_tushare.py ├── sma_crossover.py ├── sma_crossover_sample.py ├── statarb_erniechan.py ├── technical-1.py ├── tutorial-1.py ├── tutorial-2.py ├── tutorial-3.py ├── tutorial-4.py ├── tutorial-5.py ├── tutorial-optimizer-local.py ├── tutorial-optimizer-server.py ├── tutorial-optimizer-worker.py ├── tutorial-zerorpc-server.py ├── tutorial-zerorpc-worker.py ├── tutorial_bitstamp_1.py └── vwap_momentum.py ├── sandbox ├── bbands.py ├── bccharts_example_1.py ├── bccharts_example_2.py └── vwap_momentum.py ├── setup.cfg ├── setup.py ├── stratlib ├── sample_DMA.py ├── sample_FSMA.py ├── sample_SMA.py ├── sample_SMA_Day.py ├── sample_SMA_Live.py ├── sample_bollinger_bandit.py └── sample_orders.py ├── tests ├── __init__.py ├── bar_test.py ├── barfeed_test.py ├── bitstamp_test.py ├── broker_backtesting_test.py ├── broker_test.py ├── btcharts_test.py ├── common.py ├── csvfeed_test.py ├── data │ ├── 30min-bitstampUSD.csv │ ├── AA-2008-yahoofinance.csv │ ├── AA-2009-yahoofinance.csv │ ├── AES-2008-yahoofinance.csv │ ├── AES-2009-yahoofinance.csv │ ├── AIG-2008-yahoofinance.csv │ ├── AIG-2009-yahoofinance.csv │ ├── DBC-2007-yahoofinance.csv │ ├── DBC-2008-yahoofinance.csv │ ├── DBC-2009-yahoofinance.csv │ ├── DBC-2010-yahoofinance.csv │ ├── DBC-2011-yahoofinance.csv │ ├── DBC-2012-yahoofinance.csv │ ├── DBC-2013-yahoofinance.csv │ ├── DIA-2009-yahoofinance.csv │ ├── DIA-2010-yahoofinance.csv │ ├── DIA-2011-yahoofinance.csv │ ├── DIA-2012-yahoofinance.csv │ ├── IEF-2007-yahoofinance.csv │ ├── IEF-2008-yahoofinance.csv │ ├── IEF-2009-yahoofinance.csv │ ├── IEF-2010-yahoofinance.csv │ ├── IEF-2011-yahoofinance.csv │ ├── IEF-2012-yahoofinance.csv │ ├── IEF-2013-yahoofinance.csv │ ├── SPY-2007-yahoofinance.csv │ ├── SPY-2008-yahoofinance.csv │ ├── SPY-2009-yahoofinance.csv │ ├── SPY-2010-yahoofinance.csv │ ├── SPY-2011-yahoofinance.csv │ ├── SPY-2012-yahoofinance.csv │ ├── SPY-2013-yahoofinance.csv │ ├── VEU-2007-yahoofinance.csv │ ├── VEU-2008-yahoofinance.csv │ ├── VEU-2009-yahoofinance.csv │ ├── VEU-2010-yahoofinance.csv │ ├── VEU-2011-yahoofinance.csv │ ├── VEU-2012-yahoofinance.csv │ ├── VEU-2013-yahoofinance.csv │ ├── VNQ-2007-yahoofinance.csv │ ├── VNQ-2008-yahoofinance.csv │ ├── VNQ-2009-yahoofinance.csv │ ├── VNQ-2010-yahoofinance.csv │ ├── VNQ-2011-yahoofinance.csv │ ├── VNQ-2012-yahoofinance.csv │ ├── VNQ-2013-yahoofinance.csv │ ├── VTI-2007-yahoofinance.csv │ ├── VTI-2008-yahoofinance.csv │ ├── VTI-2009-yahoofinance.csv │ ├── VTI-2010-yahoofinance.csv │ ├── VTI-2011-yahoofinance.csv │ ├── VTI-2012-yahoofinance.csv │ ├── VTI-2013-yahoofinance.csv │ ├── WIKI-GORO-2006-quandl.csv │ ├── WIKI-GORO-2007-quandl.csv │ ├── WIKI-GORO-2008-quandl.csv │ ├── WIKI-GORO-2009-quandl.csv │ ├── WIKI-GORO-2010-quandl.csv │ ├── WIKI-GORO-2011-quandl.csv │ ├── WIKI-GORO-2012-quandl.csv │ ├── aapl-2011-yahoofinance.csv │ ├── aapl-2012-yahoofinance.csv │ ├── aeti-2011-yahoofinance.csv │ ├── bccharts_example_2.output │ ├── bitstampUSD-2.csv │ ├── bitstampUSD.csv │ ├── egan-2011-yahoofinance.csv │ ├── gdx-2006-yahoofinance.csv │ ├── gdx-2007-yahoofinance.csv │ ├── gdx-2008-yahoofinance.csv │ ├── gdx-2009-yahoofinance.csv │ ├── gdx-2010-yahoofinance.csv │ ├── gdx-2011-yahoofinance.csv │ ├── gdx-2012-yahoofinance.csv │ ├── gld-2006-yahoofinance.csv │ ├── gld-2007-yahoofinance.csv │ ├── gld-2008-yahoofinance.csv │ ├── gld-2009-yahoofinance.csv │ ├── gld-2010-yahoofinance.csv │ ├── gld-2011-yahoofinance.csv │ ├── gld-2012-yahoofinance.csv │ ├── glng-2011-yahoofinance.csv │ ├── goog-2011-yahoofinance.csv │ ├── multiinstrument.sqlite │ ├── nikkei-2010-yahoofinance.csv │ ├── nikkei-2011-yahoofinance.csv │ ├── nt-sma-15.csv │ ├── nt-spy-minute-2011-03.csv │ ├── nt-spy-minute-2011.csv │ ├── orcl-2000-yahoofinance.csv │ ├── orcl-2000.csv │ ├── orcl-2001-yahoofinance.csv │ ├── orders.csv │ ├── plotter_test.png │ ├── quandl_gold_2.csv │ ├── rsi-test.csv │ ├── sc-ema-10.csv │ ├── sc-sma-10.csv │ ├── sharpe-ratio-test-ige.csv │ ├── sharpe-ratio-test-spy.csv │ ├── simo-2011-yahoofinance.csv │ ├── trades-mgtox-usd-2013-01-01.csv │ ├── trades-mtgox-usd-2013-03.csv │ ├── yhoo-2011-yahoofinance.csv │ └── yhoo-2012-yahoofinance.csv ├── dataseries_test.py ├── dbfeed_test.py ├── drawdown_analyzer_test.py ├── eventprofiler_test.py ├── feed_test.py ├── fill_strategy_test.py ├── http_server.py ├── logger_test.py ├── logger_test_1.py ├── logger_test_2.py ├── logger_test_3.py ├── memfeed_test.py ├── multi_instrument_strategy_test.py ├── ninjatraderfeed_test.py ├── observer_test.py ├── optimizer_testcase.py ├── plotter_test.py ├── position_test.py ├── pusher_test.py ├── requirements.txt ├── resample_test.py ├── returns_analyzer_test.py ├── sharpe_analyzer_test.py ├── slippage_model_test.py ├── sma_crossover.py ├── smacrossover_strategy_test.py ├── strategy_test.py ├── talib_test.py ├── technical_atr_test.py ├── technical_bollinger_test.py ├── technical_cross_test.py ├── technical_cumret_test.py ├── technical_highlow_test.py ├── technical_hurst_test.py ├── technical_linebreak_test.py ├── technical_linreg_test.py ├── technical_ma_test.py ├── technical_macd_test.py ├── technical_ratio_test.py ├── technical_roc_test.py ├── technical_rsi_test.py ├── technical_stats_test.py ├── technical_stoch_test.py ├── technical_test.py ├── technical_trend_test.py ├── technical_vwap_test.py ├── test_strategy.py ├── trades_analyzer_test.py ├── utils_test.py └── websocket_server.py ├── tools └── ts2moo.py ├── tox.ini └── travis ├── Dockerfile └── run_tests.sh /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: hy9eCNWNRBQedT3iK0m1fDO4Gvpmrvmg3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *pyc 2 | MANIFEST 3 | /docs/_build 4 | /site 5 | .tox 6 | .idea 7 | .cache 8 | .coverage 9 | .ipynb_checkpoints 10 | .ropeproject 11 | 12 | /dist 13 | /build 14 | *.egg-info 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | env: 6 | global: 7 | # https://docs.travis-ci.com/user/environment-variables/#Defining-encrypted-variables-in-.travis.yml 8 | # travis encrypt QUANDL_API_KEY=somevalue 9 | - secure: "YgngWTEKD6bUVgExTN8vWYCq7mrMKsVVqZaWIsepRziU++K1e9iBuQrSspQbyrdsKMqKMyMnsM3ru7M6+D0F+p/BC4gyWg6N+wn3RoqrWidyutejibsobDJnS3EsSfo4cJUFcoAOzzuXK+Du0yUZPNBxjJdF/NA1C8vDg0YAmzs=" 10 | - secure: "GUuZEQSA9x96bIJiC/Ab/RxQ+Dv2eqwhRVaZYVlmuSDQXouDJdFAF9dEcUplwt3TCKg7CbOa3gr2c1rJ7bclk1ueUSXdwzlgEfEdcu7Xm+lf6LcfZESDtWUvPgRzvVqspp3hwx+SCpSl2TzXwgCRNOX8ybDC5ZvRZRQaagT6maA=" 11 | - secure: "oAUaWICTHks6N/RNFZbF/NJE+1mB45DRZu1uvzKN5EhEp+Qys6LGHm1yZLI1sMAfwYrLro1oxNGpxD26hoMDjy2bDyBNZj59i2M7Eeo9juY3wR2sz0tKuvEdMdbuhMGyDgvXV6xlqO10hHeUUwbJOyxcQxmcp0c33B0Jse3m+pI=" 12 | - secure: "OrR5+l3HzjKWQtVRUIHB1uLCFtRt1wNAQxJjoL6mufBMIigcpMnE2QzsoP9ywSgew8St4nFMGaL/xxR/RYTi59c6fVRh8zGg/JYMSYtr4UM8M2Gk0vSYmo3JO9OihOxn3JWbF4606vzEgqtjMT9qojo3Xx/d0Hp9br5s/wwe1oQ=" 13 | - secure: "beFEPV9JWgX4wFx3mBeBwE7hBAK2h9gW5qpNolgYrevDQxHDQdfcNN5Rcda2/VGQiEKM+X4dixNttogfNu4yS21c5/DPUhsKNwV/ClZgYsmkowGwkXsaF6J/cYcE9+WvziCfDV3z3wTKMmyPMJlbeFDB+zik2idzkG3txUVo27g=" 14 | 15 | sudo: required 16 | 17 | services: 18 | - docker 19 | 20 | before_install: 21 | - docker pull gbecedillas/pyalgotrade:0.20-py27 22 | - cp travis/Dockerfile . 23 | - docker build -t pyalgotrade_testcases . 24 | - sudo pip install coveralls 25 | 26 | script: 27 | - docker run --name testcases --env TWITTER_CONSUMER_KEY=$TWITTER_CONSUMER_KEY --env TWITTER_CONSUMER_SECRET=$TWITTER_CONSUMER_SECRET --env TWITTER_ACCESS_TOKEN=$TWITTER_ACCESS_TOKEN --env TWITTER_ACCESS_TOKEN_SECRET=$TWITTER_ACCESS_TOKEN_SECRET --env QUANDL_API_KEY=$QUANDL_API_KEY pyalgotrade_testcases /bin/bash -c "cd /tmp/pyalgotrade; ./run_tests.sh" 28 | 29 | after_success: 30 | - docker cp testcases:/tmp/pyalgotrade/.coverage . 31 | # .coverage file has absolute paths in it, so we need to put the source in the right path for coveralls to find it. 32 | - mkdir /tmp/pyalgotrade 33 | - docker cp testcases:/tmp/pyalgotrade/pyalgotrade /tmp/pyalgotrade/pyalgotrade 34 | - coveralls 35 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | 更新记录 3 | ================== 4 | 5 | 0.0.1 6 | ================== 7 | 8 | - 搭建基本的框架支持 py3, 增加基本的 unittest 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MooQuant 2 | 3 | Copyright 2017 Gabriel Martin Becedillas Ruiz 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude .DS_Store *.pyc *.pyo 2 | 3 | include HISTORY.rst 4 | include README.rst 5 | include README.md 6 | 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | 32 | clean-build: ## remove build artifacts 33 | rm -fr build/ 34 | rm -fr dist/ 35 | rm -fr .eggs/ 36 | rm -fr .pytest_cache/ 37 | rm -fr .python-version 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '*.bak' -exec rm -f {} + 46 | find . -name '__pycache__' -exec rm -fr {} + 47 | 48 | clean-test: ## remove test and coverage artifacts 49 | rm -fr .tox/ 50 | rm -f .coverage 51 | rm -fr htmlcov/ 52 | 53 | lint: ## check style with flake8 54 | flake8 mooquant tests 55 | 56 | test: ## run tests quickly with the default Python 57 | py.test 58 | 59 | 60 | test-all: ## run tests on every Python version with tox 61 | tox 62 | 63 | coverage: ## check code coverage quickly with the default Python 64 | coverage run --source mooquant -m pytest 65 | coverage report -m 66 | coverage html 67 | $(BROWSER) htmlcov/index.html 68 | 69 | docs: ## generate Sphinx HTML documentation, including API docs 70 | rm -f docs/mooquant.rst 71 | rm -f docs/modules.rst 72 | sphinx-apidoc -o docs/ mooquant 73 | $(MAKE) -C docs clean 74 | $(MAKE) -C docs html 75 | $(BROWSER) docs/_build/html/index.html 76 | 77 | servedocs: docs ## compile the docs watching for changes 78 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 79 | 80 | release: clean ## package and upload a release 81 | python setup.py sdist upload 82 | python setup.py bdist_wheel upload 83 | 84 | dist: clean ## builds source and wheel package 85 | python setup.py sdist 86 | python setup.py bdist_wheel 87 | ls -l dist 88 | 89 | develop: clean ## install the package to the active Python's site-packages 90 | python setup.py develop 91 | 92 | install: clean ## install the package to the active Python's site-packages 93 | python setup.py install 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MooQuant 量化交易框架 2 | =================== 3 | 4 | [![pypi](https://img.shields.io/pypi/v/mooquant.svg)](https://pypi.python.org/pypi/mooquant) [![travis](https://img.shields.io/travis/bopo/mooquant.svg)](https://travis-ci.org/bopo/mooquant) [![Documentation Status](https://readthedocs.org/projects/mooquant/badge/?version=latest)](https://mooquant.readthedocs.io/en/latest/?badge=latest) [![Updates](https://pyup.io/repos/github/bopo/mooquant/shield.svg)](https://pyup.io/repos/github/bopo/mooquant/) 5 | 6 | MooQuant 是一个基于 `pyalgotrade` 衍生而来的完全支持 `Python3.x` 版本的支持国内A股的量化交易框架。 7 | 8 | * 协议: MIT license 9 | * 文档: https://mooquant.readthedocs.io. 10 | 11 | 12 | 功能 13 | -------- 14 | 15 | * 添加 [tushare](https://github.com/waditu/tushare) 作为数据源 16 | * 添加 [mootdx](https://github.com/bopo/mootdx) 作为数据源 17 | 18 | 版权 19 | --------- 20 | 21 | * TODO 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | MooQuant 量化交易框架 2 | =================== 3 | 4 | 5 | .. image:: https://img.shields.io/pypi/v/mooquant.svg 6 | :target: https://pypi.python.org/pypi/mooquant 7 | 8 | .. image:: https://img.shields.io/travis/bopo/mooquant.svg 9 | :target: https://travis-ci.org/bopo/mooquant 10 | 11 | .. image:: https://readthedocs.org/projects/mooquant/badge/?version=latest 12 | :target: https://mooquant.readthedocs.io/en/latest/?badge=latest 13 | :alt: Documentation Status 14 | 15 | .. image:: https://pyup.io/repos/github/bopo/mooquant/shield.svg 16 | :target: https://pyup.io/repos/github/bopo/mooquant/ 17 | :alt: Updates 18 | 19 | 20 | MooQuant 是一个基于 pyalgotrade 衍生而来的完全支持 python3.x 版本 的支持国内A股的量化交易框架。 21 | 22 | * 协议: MIT license 23 | * 文档: https://mooquant.readthedocs.io. 24 | 25 | 26 | 功能 27 | -------- 28 | 29 | * 添加 `tushare `__ 做为数据源 30 | * 添加 `mootdx `__ 作为数据源 31 | 32 | 版权 33 | --------- 34 | 35 | * TODO 36 | -------------------------------------------------------------------------------- /coverage.cfg: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | raise NotImplementedError() 5 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION 2 | FROM python:${PYTHON_VERSION} 3 | MAINTAINER BoPo 4 | 5 | RUN apt-get update; apt-get upgrade -y 6 | RUN apt-get install -y build-essential 7 | RUN apt-get install -y python-setuptools python-dev 8 | RUN apt-get install -y python-pip 9 | 10 | RUN apt-get install -y gfortran libopenblas-dev liblapack-dev 11 | RUN apt-get install -y libfreetype6-dev 12 | RUN apt-get install -y pkg-config 13 | RUN apt-get install -y wget 14 | 15 | RUN pip install numpy 16 | RUN pip install scipy 17 | RUN pip install pandas 18 | RUN pip install patsy 19 | RUN pip install statsmodels 20 | RUN pip install matplotlib 21 | RUN pip install ws4py 22 | RUN pip install tornado 23 | RUN pip install tweepy 24 | RUN pip install cython 25 | RUN pip install retrying 26 | 27 | # TA-Lib 28 | RUN cd /tmp; \ 29 | wget http://sourceforge.net/projects/ta-lib/files/ta-lib/0.4.0/ta-lib-0.4.0-src.tar.gz; \ 30 | tar -xzf ta-lib-0.4.0-src.tar.gz; \ 31 | cd ta-lib; \ 32 | ./configure ; make; make install; \ 33 | cd ..; \ 34 | rm ta-lib-0.4.0-src.tar.gz; \ 35 | rm -rf ta-lib 36 | 37 | RUN pip install TA-Lib 38 | RUN pip install pyalgotrade==0.20 39 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build and tag Python 2.7 images 4 | docker build --build-arg PYTHON_VERSION=2.7 -t mooquant:0.20 . 5 | docker tag mooquant:0.20 mooquant:0.20-py27 6 | docker tag mooquant:0.20 gbecedillas/mooquant:0.20 7 | docker tag mooquant:0.20-py27 gbecedillas/mooquant:0.20-py27 8 | 9 | # Build and tag Python 3.7 images 10 | docker build --build-arg PYTHON_VERSION=3.7 -t mooquant:0.20-py37 . 11 | docker tag mooquant:0.20-py37 gbecedillas/mooquant:0.20-py37 12 | 13 | # Push images 14 | # docker login --username=gbecedillas 15 | # docker push gbecedillas/mooquant:0.20 16 | # docker push gbecedillas/mooquant:0.20-py27 17 | # docker push gbecedillas/mooquant:0.20-py37 18 | 19 | # docker rmi $(docker images --quiet --filter "dangling=true") 20 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block extrahead %} 4 | {{ super() }} 5 | 6 | 7 | 20 | 21 | 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /docs/barfeed.rst: -------------------------------------------------------------------------------- 1 | .. _barfeed-label: 2 | 3 | barfeed -- Bar providers 4 | ======================== 5 | 6 | .. automodule:: mooquant.barfeed 7 | :members: BaseBarFeed 8 | :member-order: bysource 9 | :special-members: 10 | :exclude-members: __weakref__ 11 | :show-inheritance: 12 | 13 | CSV 14 | --- 15 | .. automodule:: mooquant.barfeed.csvfeed 16 | :members: BarFeed, GenericBarFeed 17 | :show-inheritance: 18 | 19 | Yahoo! Finance 20 | -------------- 21 | .. automodule:: mooquant.barfeed.yahoofeed 22 | :members: Feed 23 | :show-inheritance: 24 | 25 | Google Finance 26 | -------------- 27 | .. automodule:: mooquant.barfeed.googlefeed 28 | :members: Feed 29 | :show-inheritance: 30 | 31 | Quandl 32 | ------ 33 | .. automodule:: mooquant.barfeed.quandlfeed 34 | :members: Feed 35 | :show-inheritance: 36 | 37 | Ninja Trader 38 | ------------ 39 | .. automodule:: mooquant.barfeed.ninjatraderfeed 40 | :members: Feed 41 | :show-inheritance: 42 | 43 | -------------------------------------------------------------------------------- /docs/bitcoin.rst: -------------------------------------------------------------------------------- 1 | Bitcoin 2 | ======= 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | bitstamp 10 | bitcoincharts 11 | -------------------------------------------------------------------------------- /docs/bitcoincharts.rst: -------------------------------------------------------------------------------- 1 | Bitcoin Charts support 2 | ====================== 3 | 4 | The bitcoincharts package adds support for integrating with historical trade data supplied by http://www.bitcoincharts.com/ 5 | for backtesting Bitcoin strategies. 6 | 7 | Historical trade data in CSV format is described in http://www.bitcoincharts.com/about/markets-api/, and files can be 8 | downloaded from http://api.bitcoincharts.com/v1/csv/. 9 | 10 | Contents: 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | bitcoincharts_ref 16 | bitcoincharts_example 17 | -------------------------------------------------------------------------------- /docs/bitcoincharts_example.rst: -------------------------------------------------------------------------------- 1 | Bitcoin Charts example 2 | ====================== 3 | 4 | Although it is absolutely possible to backtest a strategy with tick data as supplied by 5 | http://www.bitcoincharts.com/about/markets-api/ using :class:`mooquant.bitcoincharts.barfeed.CSVTradeFeed`, 6 | you may want to to backtest using summarized bars at a different frequency to make backtesting faster. 7 | 8 | As of 12-Aug-2014, http://api.bitcoincharts.com/v1/csv/bitstampUSD.csv.gz has 4588830 events so we'll transform a portion of 9 | it into 30 minute bars for backtesting purposes with the following script: 10 | 11 | .. literalinclude:: ../samples/bccharts_example_1.py 12 | 13 | It will take some time to execute, so be patient. The resampled file should look like this: :: 14 | 15 | Date Time,Open,High,Low,Close,Volume,Adj Close 16 | 2014-01-01 00:00:00,732.0,738.25,729.01,734.81,266.17955488, 17 | 2014-01-01 00:30:00,734.81,739.9,734.47,739.02,308.96802502, 18 | 2014-01-01 01:00:00,739.02,739.97,737.65,738.11,65.66924473, 19 | 2014-01-01 01:30:00,738.0,742.0,737.65,741.89,710.27165024, 20 | 2014-01-01 02:00:00,741.89,757.99,741.89,752.23,1085.13335011, 21 | 2014-01-01 02:30:00,752.23,755.0,747.0,747.2,272.03949342, 22 | 2014-01-01 04:00:00,744.98,748.02,744.98,747.19,104.65989075, 23 | . 24 | . 25 | 26 | We can now take advantage of :class:`mooquant.barfeed.csvfeed.GenericBarFeed` to load the resampled file and backtest a 27 | Bitcoin strategy. We'll be using a VWAP momentum strategy for illustration purposes: 28 | 29 | .. literalinclude:: ../samples/bccharts_example_2.py 30 | 31 | This is what the plot looks like: 32 | 33 | .. image:: ../samples/bccharts_example_2.png 34 | -------------------------------------------------------------------------------- /docs/bitcoincharts_ref.rst: -------------------------------------------------------------------------------- 1 | bitcoincharts -- Bitcoin Charts reference 2 | ========================================= 3 | 4 | Feeds 5 | ----- 6 | 7 | .. automodule:: mooquant.bitcoincharts.barfeed 8 | :members: 9 | :show-inheritance: 10 | -------------------------------------------------------------------------------- /docs/bitstamp.rst: -------------------------------------------------------------------------------- 1 | Bitstamp support 2 | ================ 3 | 4 | The bitstamp package adds support for paper trading and live trading Bitcoin strategies through Bitstamp 5 | (https://www.bitstamp.net/). 6 | 7 | Bitstamp support depends on: 8 | * **ws4py** (https://github.com/Lawouach/WebSocket-for-Python) 9 | * **tornado** (http://www.tornadoweb.org/en/stable/) 10 | 11 | so be sure to have those installed before moving forward. 12 | 13 | Contents: 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | bitstamp_ref 19 | bitstamp_example 20 | -------------------------------------------------------------------------------- /docs/bitstamp_ref.rst: -------------------------------------------------------------------------------- 1 | bitstamp -- Bitstamp reference 2 | ============================== 3 | 4 | WebSocket 5 | --------- 6 | 7 | This package has classes for the events emitted by Bitstamp's streaming service. 8 | Check https://www.bitstamp.net/websocket/ for more information. 9 | 10 | .. automodule:: mooquant.bitstamp.wsclient 11 | :members: 12 | :show-inheritance: 13 | 14 | Feeds 15 | ----- 16 | 17 | .. automodule:: mooquant.bitstamp.barfeed 18 | :members: LiveTradeFeed 19 | :show-inheritance: 20 | 21 | Brokers 22 | ------- 23 | 24 | .. automodule:: mooquant.bitstamp.broker 25 | :members: BacktestingBroker, PaperTradingBroker, LiveBroker 26 | :show-inheritance: 27 | 28 | -------------------------------------------------------------------------------- /docs/broker.rst: -------------------------------------------------------------------------------- 1 | broker -- Order management classes 2 | ================================== 3 | 4 | Base module and classes 5 | ------------------------ 6 | 7 | .. automodule:: mooquant.broker 8 | :members: Order, MarketOrder, LimitOrder, StopOrder, StopLimitOrder, OrderExecutionInfo, Broker 9 | :member-order: bysource 10 | :show-inheritance: 11 | 12 | Backtesting module and classes 13 | ------------------------------ 14 | 15 | .. automodule:: mooquant.broker.backtesting 16 | :members: Commission, NoCommission, FixedPerTrade, TradePercentage, Broker 17 | :show-inheritance: 18 | 19 | .. automodule:: mooquant.broker.slippage 20 | :members: SlippageModel, NoSlippage, VolumeShareSlippage 21 | :show-inheritance: 22 | 23 | .. automodule:: mooquant.broker.fillstrategy 24 | :members: FillStrategy, DefaultStrategy 25 | :show-inheritance: 26 | -------------------------------------------------------------------------------- /docs/compinvpart1.rst: -------------------------------------------------------------------------------- 1 | Computational Investing Part I 2 | ============================== 3 | 4 | As I was taking the `Computational Investing Part I `_ course in 2012 5 | I had to work on a set of assignments and for some of them I used MooQuant. 6 | 7 | Homework 1 8 | ---------- 9 | 10 | For this assignment I had to pick 4 stocks, invest a total of $100000 during 2011, and calculate: 11 | 12 | * Final portfolio value 13 | * Anual return 14 | * Average daily return 15 | * Std. dev. of daily returns 16 | * Sharpe ratio 17 | 18 | Download the data with the following commands: :: 19 | 20 | python -c "from mooquant.tools import yahoofinance; yahoofinance.download_daily_bars('aeti', 2011, 'aeti-2011-yahoofinance.csv')" 21 | python -c "from mooquant.tools import yahoofinance; yahoofinance.download_daily_bars('egan', 2011, 'egan-2011-yahoofinance.csv')" 22 | python -c "from mooquant.tools import yahoofinance; yahoofinance.download_daily_bars('glng', 2011, 'glng-2011-yahoofinance.csv')" 23 | python -c "from mooquant.tools import yahoofinance; yahoofinance.download_daily_bars('simo', 2011, 'simo-2011-yahoofinance.csv')" 24 | 25 | Although the deliverable was an Excel spreadsheet, I validated the results using this piece of code: 26 | 27 | .. literalinclude:: ../samples/compinv-1.py 28 | 29 | The results were: 30 | 31 | .. literalinclude:: ../samples/compinv-1.output 32 | 33 | Homework 3 and 4 34 | ---------------- 35 | 36 | For these assignments I had to build a market simulation tool that loads orders from a file, executes those, 37 | and prints the results for each day. 38 | 39 | The orders file for homework 3 look like this: :: 40 | 41 | 2011,1,10,AAPL,Buy,1500, 42 | 2011,1,13,AAPL,Sell,1500, 43 | 2011,1,13,IBM,Buy,4000, 44 | 2011,1,26,GOOG,Buy,1000, 45 | 2011,2,2,XOM,Sell,4000, 46 | 2011,2,10,XOM,Buy,4000, 47 | 2011,3,3,GOOG,Sell,1000, 48 | 2011,3,3,IBM,Sell,2200, 49 | 2011,6,3,IBM,Sell,3300, 50 | 2011,5,3,IBM,Buy,1500, 51 | 2011,6,10,AAPL,Buy,1200, 52 | 2011,8,1,GOOG,Buy,55, 53 | 2011,8,1,GOOG,Sell,55, 54 | 2011,12,20,AAPL,Sell,1200, 55 | 56 | This is the market simulation tool that I built: 57 | 58 | .. literalinclude:: ../samples/compinv-3.py 59 | 60 | The output for homework 3 looks like this: :: 61 | 62 | First date 2011-01-10 00:00:00 63 | Last date 2011-12-20 00:00:00 64 | Symbols ['AAPL', 'IBM', 'GOOG', 'XOM'] 65 | 2011-01-10 00:00:00: Portfolio value: $1000000.00 66 | 2011-01-11 00:00:00: Portfolio value: $998785.00 67 | 2011-01-12 00:00:00: Portfolio value: $1002940.00 68 | 2011-01-13 00:00:00: Portfolio value: $1004815.00 69 | . 70 | . 71 | . 72 | 2011-12-15 00:00:00: Portfolio value: $1113532.00 73 | 2011-12-16 00:00:00: Portfolio value: $1116016.00 74 | 2011-12-19 00:00:00: Portfolio value: $1117444.00 75 | 2011-12-20 00:00:00: Portfolio value: $1133860.00 76 | Final portfolio value: $1133860.00 77 | Anual return: 13.39 % 78 | Average daily return: 0.05 % 79 | Std. dev. daily return: 0.0072 80 | Sharpe ratio: 1.21 81 | 82 | -------------------------------------------------------------------------------- /docs/eventprofiler.rst: -------------------------------------------------------------------------------- 1 | Event profiler 2 | ============== 3 | 4 | Inspired in QSTK (http://wiki.quantsoftware.org/index.php?title=QSTK_Tutorial_9), the **eventprofiler** module is a tool to analyze, 5 | statistically, how events affect future equity prices. 6 | The event profiler scans over historical data for a specified event and then calculates the impact of that event on the equity prices in the past 7 | and the future over a certain lookback period. 8 | 9 | **The goal of this tool is to help you quickly validate an idea, before moving forward with the backtesting process.** 10 | 11 | .. automodule:: mooquant.eventprofiler 12 | :members: 13 | :member-order: bysource 14 | :show-inheritance: 15 | 16 | Example 17 | ------- 18 | 19 | The following example is inspired on the 'Buy-on-Gap Model' from Ernie Chan's book: 20 | 'Algorithmic Trading: Winning Strategies and Their Rationale': 21 | 22 | * The idea is to select a stock near the market open whose returns from their previous day's lows 23 | to today's open are lower that one standard deviation. The standard deviation is computed using 24 | the daily close-to-close returns of the last 90 days. These are the stocks that "gapped down". 25 | * This is narrowed down by requiring the open price to be higher than the 20-day moving average 26 | of the closing price. 27 | 28 | .. literalinclude:: ../samples/eventstudy.py 29 | 30 | The code is doing 4 things: 31 | 32 | 1. Declaring a :class:`Predicate` that implements the 'Buy-on-Gap Model' event identification. 33 | 2. Loading bars for some stocks. 34 | 3. Running the analysis. 35 | 4. Plotting the results. 36 | 37 | This is what the output should look like: 38 | 39 | .. image:: ../samples/eventstudy.png 40 | 41 | .. literalinclude:: ../samples/eventstudy.output 42 | 43 | Note that **Cummulative returns are normalized to the time of the event**. 44 | -------------------------------------------------------------------------------- /docs/images/execution_running_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bopo/mooquant/244a87d4cd8b4d918eec4f16905e0921c3b39f50/docs/images/execution_running_1.png -------------------------------------------------------------------------------- /docs/images/queue_execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bopo/mooquant/244a87d4cd8b4d918eec4f16905e0921c3b39f50/docs/images/queue_execution.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. MooQuant documentation master file, created by 2 | sphinx-quickstart on Fri Nov 4 21:48:31 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | MooQuant 文档 7 | ========================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | 14 | intro 15 | tutorial 16 | tools 17 | eventprofiler 18 | bitcoin 19 | talib 20 | compinvpart1 21 | samples 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | 项目简介 2 | ============ 3 | 4 | MooQuant 是一个基于事件驱动的 Python 算法交易库,它支持: 5 | * 基于CSV文件里的历史数据进行回测. 6 | * 基于 :ref:`Bitstamp ` 的实时数据进行模拟交易. 7 | * 基于Bitstamp平台进行真实交易. 8 | 9 | It should also make it easy to optimize a strategy using multiple computers. 10 | 11 | MooQuant is developed using Python 2.7 and depends on: 12 | * NumPy and SciPy (http://numpy.scipy.org/). 13 | * pytz (http://pytz.sourceforge.net/). 14 | * matplotlib (http://matplotlib.sourceforge.net/) for plotting support. 15 | * ws4py (https://github.com/Lawouach/WebSocket-for-Python) for Bitstamp support. 16 | * tornado (http://www.tornadoweb.org/en/stable/) for Bitstamp support. 17 | * tweepy (https://github.com/tweepy/tweepy) for Twitter support. 18 | 19 | so you need to have those installed in order to use this library. 20 | 21 | You can install MooQuant using pip like this: :: 22 | 23 | pip install mooquant 24 | 25 | -------------------------------------------------------------------------------- /docs/optimizer.rst: -------------------------------------------------------------------------------- 1 | optimizer -- Parallel optimizers 2 | ================================ 3 | 4 | .. automodule:: mooquant.optimizer.server 5 | :members: 6 | :member-order: bysource 7 | :show-inheritance: 8 | 9 | .. automodule:: mooquant.optimizer.worker 10 | :members: 11 | :member-order: bysource 12 | :show-inheritance: 13 | 14 | .. automodule:: mooquant.optimizer.local 15 | :members: 16 | :member-order: bysource 17 | :show-inheritance: 18 | 19 | .. note:: 20 | * The server component will split strategy executions in chunks which are distributed among the different workers. **mooquant.optimizer.server.Server.defaultBatchSize** controls the chunk size. 21 | * The :meth:`mooquant.strategy.BaseStrategy.getResult` method is used to select the best strategy execution. You can override that method to rank executions using a different criteria. 22 | 23 | -------------------------------------------------------------------------------- /docs/sample_bbands.rst: -------------------------------------------------------------------------------- 1 | Bollinger Bands 2 | =============== 3 | 4 | This example is based on: 5 | * http://www.investopedia.com/articles/trading/07/bollinger.asp 6 | 7 | .. literalinclude:: ../samples/bbands.py 8 | 9 | this is what the output should look like: 10 | 11 | .. literalinclude:: ../samples/bbands.output 12 | 13 | and this is what the plot should look like: 14 | 15 | .. image:: ../samples/bbands.png 16 | 17 | You can get better returns by tunning the Bollinger Bands period as well as the entry and exit points. 18 | -------------------------------------------------------------------------------- /docs/sample_market_timing.rst: -------------------------------------------------------------------------------- 1 | Market Timing Using Moving-Average Crossovers 2 | ============================================= 3 | 4 | 5 | This example is inspired on the Market Timing / GTAA model described in: 6 | * http://mebfaber.com/timing-model/ 7 | * http://papers.ssrn.com/sol3/papers.cfm?abstract_id=962461 8 | 9 | The stragegy supports analyzing more than one instrument per asset class, and selects the one that has highest 10 | returns in the last month. 11 | 12 | .. literalinclude:: ../samples/market_timing.py 13 | 14 | This is what the output should look like: 15 | 16 | .. literalinclude:: ../samples/market_timing.output 17 | 18 | This is what the plot should look like: 19 | 20 | .. image:: ../samples/market_timing.png 21 | -------------------------------------------------------------------------------- /docs/sample_quandl.rst: -------------------------------------------------------------------------------- 1 | Quandl integration 2 | ================== 3 | 4 | The purpose of this example is to show how to integrate price data along with any 5 | time-series data in CSV format from Quandl into a strategy. 6 | 7 | We'll use the following CSV data from Quandl: 8 | http://www.quandl.com/OFDP-Open-Financial-Data-Project/GOLD_2-LBMA-Gold-Price-London-Fixings-P-M 9 | 10 | .. literalinclude:: ../samples/quandl_sample.py 11 | 12 | This is what the output should look like: 13 | 14 | .. literalinclude:: ../samples/quandl_sample.output 15 | 16 | and this is what the plot should look like: 17 | 18 | .. image:: ../samples/quandl_sample.png 19 | -------------------------------------------------------------------------------- /docs/sample_rsi2.rst: -------------------------------------------------------------------------------- 1 | RSI2 2 | ==== 3 | 4 | This example is based on a strategy known as RSI2 (http://stockcharts.com/school/doku.php?id=chart_school:trading_strategies:rsi2) 5 | which requires the following parameters: 6 | 7 | * An SMA period for trend identification. We’ll call this entrySMA. 8 | * A smaller SMA period for the exit point. We’ll call this exitSMA. 9 | * An RSI period for entering both short/long positions. We’ll call this rsiPeriod. 10 | * An RSI oversold threshold for long position entry. We’ll call this overSoldThreshold. 11 | * An RSI overbought threshold for short position entry. We’ll call this overBoughtThreshold. 12 | 13 | Save this code as rsi2.py: 14 | 15 | .. literalinclude:: ../samples/rsi2.py 16 | 17 | and use the following code to execute the strategy: 18 | 19 | .. literalinclude:: ../samples/rsi2_sample.py 20 | 21 | This is what the output should look like: 22 | 23 | .. literalinclude:: ../samples/rsi2_sample.output 24 | 25 | and this is what the plot should look like: 26 | 27 | .. image:: ../samples/rsi2_sample.png 28 | 29 | You can get better returns by tunning the different parameters. 30 | -------------------------------------------------------------------------------- /docs/sample_sma_crossover.rst: -------------------------------------------------------------------------------- 1 | SMA Crossover 2 | ============= 3 | 4 | Save this code as sma_crossover.py: 5 | 6 | .. literalinclude:: ../samples/sma_crossover.py 7 | 8 | and use the following code to execute the strategy: 9 | 10 | .. literalinclude:: ../samples/sma_crossover_sample.py 11 | 12 | This is what the output should look like: 13 | 14 | .. literalinclude:: ../samples/sma_crossover.output 15 | 16 | and this is what the plot should look like: 17 | 18 | .. image:: ../samples/sma_crossover.png 19 | 20 | You can get better returns by tunning the sma period. 21 | -------------------------------------------------------------------------------- /docs/sample_statarb_erniechan.rst: -------------------------------------------------------------------------------- 1 | Ernie Chan's Gold vs. Gold Miners 2 | ================================= 3 | 4 | This example is based on: 5 | * http://epchan.blogspot.com.ar/2006/11/gold-vs-gold-miners-another-arbitrage.html 6 | * https://www.quantopian.com/posts/ernie-chans-gold-vs-gold-miners-stat-arb 7 | 8 | .. literalinclude:: ../samples/statarb_erniechan.py 9 | 10 | this is what the output should look like: 11 | 12 | .. literalinclude:: ../samples/statarb_erniechan.output 13 | 14 | and this is what the plot should look like: 15 | 16 | .. image:: ../samples/statarb_erniechan.png 17 | 18 | You can get better returns by tunning the window size as well as the entry and exit values for the z-score. 19 | 20 | -------------------------------------------------------------------------------- /docs/sample_vwap_momentum.rst: -------------------------------------------------------------------------------- 1 | VWAP Momentum Trade 2 | =================== 3 | 4 | This example is based on: 5 | * https://www.quantopian.com/posts/momentum-trade 6 | 7 | .. literalinclude:: ../samples/vwap_momentum.py 8 | 9 | this is what the output should look like: 10 | 11 | .. literalinclude:: ../samples/vwap_momentum.output 12 | 13 | and this is what the plot should look like: 14 | 15 | .. image:: ../samples/vwap_momentum.png 16 | 17 | You can get better returns by tunning the VWAP and threshold parameters. 18 | 19 | -------------------------------------------------------------------------------- /docs/samples.rst: -------------------------------------------------------------------------------- 1 | .. _samples-label: 2 | 3 | Sample strategies 4 | ================= 5 | 6 | Momentum 7 | -------- 8 | .. toctree:: 9 | sample_vwap_momentum 10 | sample_sma_crossover 11 | sample_market_timing 12 | 13 | Mean Reversion 14 | -------------- 15 | .. toctree:: 16 | sample_statarb_erniechan 17 | sample_bbands 18 | sample_rsi2 19 | 20 | Others 21 | ------ 22 | .. toctree:: 23 | sample_quandl 24 | -------------------------------------------------------------------------------- /docs/talib.rst: -------------------------------------------------------------------------------- 1 | TA-Lib integration 2 | ================== 3 | 4 | The **mooquant.talibext.indicator** module provides integration with Python wrapper for TA-Lib (http://mrjbq7.github.com/ta-lib/) 5 | to enable calling TA-Lib functions directly with :class:`mooquant.dataseries.DataSeries` or :class:`mooquant.dataseries.bards.BarDataSeries` 6 | instances instead of numpy arrays. 7 | 8 | If you're familiar with the **talib** module, then using the **mooquant.talibext.indicator** module should be straightforward. 9 | When using **talib** standalone you do something like this: :: 10 | 11 | import numpy 12 | import talib 13 | 14 | data = numpy.random.random(100) 15 | upper, middle, lower = talib.BBANDS(data, matype=talib.MA_T3) 16 | 17 | To use the **mooquant.talibext.indicator** module in your strategies you should do something like this: :: 18 | 19 | def onBars(self, bars): 20 | closeDs = self.getFeed().getDataSeries("orcl").getCloseDataSeries() 21 | upper, middle, lower = mooquant.talibext.indicator.BBANDS(closeDs, 100, matype=talib.MA_T3) 22 | if upper != None: 23 | print "%s" % upper[-1] 24 | 25 | Every function in the **mooquant.talibext.indicator** module receives one or more dataseries (most receive just one) and the 26 | number of values to use from the dataseries. In the example above, we're calculating Bollinger Bands over the last 100 closing prices. 27 | 28 | If the parameter name is **ds**, then you should pass a regular :class:`mooquant.dataseries.DataSeries` instance, like the one 29 | shown in the example above. 30 | 31 | If the parameter name is **barDs**, then you should pass a :class:`mooquant.dataseries.bards.BarDataSeries` instance, like in the next 32 | example: :: 33 | 34 | def onBars(self, bars): 35 | barDs = self.getFeed().getDataSeries("orcl") 36 | sar = indicator.SAR(barDs, 100) 37 | if sar != None: 38 | print "%s" % sar[-1] 39 | 40 | The following TA-Lib functions are available through the **mooquant.talibext.indicator** module: 41 | 42 | .. automodule:: mooquant.talibext.indicator 43 | :members: 44 | :member-order: bysource 45 | :show-inheritance: 46 | 47 | -------------------------------------------------------------------------------- /docs/tools.rst: -------------------------------------------------------------------------------- 1 | Tools 2 | ===== 3 | 4 | Yahoo! Finance 5 | -------------- 6 | 7 | .. automodule:: mooquant.tools.yahoofinance 8 | :members: 9 | :show-inheritance: 10 | 11 | Quandl 12 | ------ 13 | 14 | .. automodule:: mooquant.tools.quandl 15 | :members: 16 | :show-inheritance: 17 | 18 | 19 | BarFeed resampling 20 | ------------------ 21 | 22 | .. automodule:: mooquant.tools.resample 23 | :members: 24 | :show-inheritance: 25 | 26 | -------------------------------------------------------------------------------- /histdata/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bopo/mooquant/244a87d4cd8b4d918eec4f16905e0921c3b39f50/histdata/.gitkeep -------------------------------------------------------------------------------- /mooquant/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | __version__ = "0.3.8" 23 | -------------------------------------------------------------------------------- /mooquant/analyzer/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | 23 | class StrategyAnalyzer(object): 24 | """Base class for strategy analyzers. 25 | 策略分析基类 26 | 27 | .. note:: 28 | 29 | This is a base class and should not be used directly. 30 | """ 31 | 32 | def beforeAttach(self, strat): 33 | pass 34 | 35 | def attached(self, strat): 36 | pass 37 | 38 | def beforeOnBars(self, strat, bars): 39 | pass 40 | -------------------------------------------------------------------------------- /mooquant/barfeed/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | 23 | # 数据序列化 ohlc (开高低收四价格) 24 | def sanitize_ohlc(open_, high, low, close): 25 | if low > open_: 26 | low = open_ 27 | 28 | if low > close: 29 | low = close 30 | 31 | if high < open_: 32 | high = open_ 33 | 34 | if high < close: 35 | high = close 36 | 37 | return open_, high, low, close 38 | -------------------------------------------------------------------------------- /mooquant/barfeed/dbfeed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | 23 | # 数据库数据操作 增删改查 24 | class Database(object): 25 | """ 数据库数据操作 增删改查 """ 26 | 27 | def addBars(self, bars, frequency): 28 | for instrument in bars.getInstruments(): 29 | bar = bars.getBar(instrument) 30 | self.addBar(instrument, bar, frequency) 31 | 32 | def addBarsFromFeed(self, feed): 33 | for dateTime, bars in feed: 34 | if bars: 35 | self.addBars(bars, feed.getFrequency()) 36 | 37 | def addBar(self, instrument, bar, frequency): 38 | raise NotImplementedError() 39 | 40 | def getBars(self, instrument, frequency, timezone=None, fromDateTime=None, toDateTime=None): 41 | raise NotImplementedError() 42 | -------------------------------------------------------------------------------- /mooquant/barfeed/quandlfeed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import bar 23 | from mooquant.barfeed import csvfeed 24 | 25 | 26 | class Feed(csvfeed.GenericBarFeed): 27 | """A :class:`mooquant.barfeed.csvfeed.BarFeed` that loads bars from CSV files downloaded from Quandl. 28 | 29 | :param frequency: The frequency of the bars. Only **mooquant.bar.Frequency.DAY** or **mooquant.bar.Frequency.WEEK** 30 | are supported. 31 | :param timezone: The default timezone to use to localize bars. Check :mod:`mooquant.marketsession`. 32 | :type timezone: A pytz timezone. 33 | :param maxLen: The maximum number of values that the :class:`mooquant.dataseries.bards.BarDataSeries` will hold. 34 | Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the 35 | opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. 36 | :type maxLen: int. 37 | 38 | .. note:: 39 | When working with multiple instruments: 40 | 41 | * If all the instruments loaded are in the same timezone, then the timezone parameter may not be specified. 42 | * If any of the instruments loaded are in different timezones, then the timezone parameter must be set. 43 | """ 44 | 45 | def __init__(self, frequency=bar.Frequency.DAY, timezone=None, maxLen=None): 46 | if frequency not in [bar.Frequency.DAY, bar.Frequency.WEEK]: 47 | raise Exception("Invalid frequency.") 48 | 49 | super().__init__(frequency, timezone, maxLen) 50 | 51 | self.setDateTimeFormat("%Y-%m-%d") 52 | self.setColumnName("datetime", "Date") 53 | self.setColumnName("adj_close", "Adj. Close") 54 | -------------------------------------------------------------------------------- /mooquant/broker/slippage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import abc 23 | 24 | 25 | # 滑点模型 26 | class SlippageModel(object, metaclass=abc.ABCMeta): 27 | """Base class for slippage models. 28 | 29 | .. note:: 30 | This is a base class and should not be used directly. 31 | """ 32 | 33 | @abc.abstractmethod 34 | def calculatePrice(self, order, price, quantity, bar, volumeUsed): 35 | """ 36 | # 计算价格 37 | Returns the slipped price per share for an order. 38 | 39 | :param order: The order being filled. 40 | :type order: :class:`mooquant.broker.Order`. 41 | :param price: The price for each share before slippage. 42 | :type price: float. 43 | :param quantity: The amount of shares that will get filled at this time for this order. 44 | :type quantity: float. 45 | :param bar: The current bar. 46 | :type bar: :class:`mooquant.bar.Bar`. 47 | :param volumeUsed: The volume size that was taken so far from the current bar. 48 | :type volumeUsed: float. 49 | :rtype: float. 50 | """ 51 | raise NotImplementedError() 52 | 53 | 54 | class NoSlippage(SlippageModel): 55 | """无滑点模型""" 56 | 57 | def calculatePrice(self, order, price, quantity, bar, volumeUsed): 58 | return price 59 | 60 | 61 | class VolumeShareSlippage(SlippageModel): 62 | """ 63 | A volume share slippage model as defined in Zipline's VolumeShareSlippage model. 64 | The slippage is calculated by multiplying the price impact constant by the square of the ratio of the order 65 | to the total volume. 66 | 67 | Check https://www.quantopian.com/help#ide-slippage for more details. 68 | 69 | :param priceImpact: Defines how large of an impact your order will have on the backtester's price calculation. 70 | :type priceImpact: float. 71 | """ 72 | 73 | def __init__(self, priceImpact=0.1): 74 | super(VolumeShareSlippage, self).__init__() 75 | self.__priceImpact = priceImpact 76 | 77 | def calculatePrice(self, order, price, quantity, bar, volumeUsed): 78 | assert bar.getVolume(), "Can't use 0 volume bars with VolumeShareSlippage" 79 | 80 | totalVolume = volumeUsed + quantity 81 | volumeShare = totalVolume / float(bar.getVolume()) 82 | impactPct = volumeShare ** 2 * self.__priceImpact 83 | 84 | if order.isBuy(): 85 | ret = price * (1 + impactPct) 86 | else: 87 | ret = price * (1 - impactPct) 88 | 89 | return ret 90 | -------------------------------------------------------------------------------- /mooquant/dispatchprio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | FIRST = 0 23 | LAST = None 24 | 25 | # We want to process order events before bar feed events. 26 | BROKER = 1000 27 | BAR_FEED = 2000 28 | -------------------------------------------------------------------------------- /mooquant/feed/memfeed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import sys 23 | 24 | from mooquant import dataseries, feed 25 | 26 | 27 | class MemFeed(feed.BaseFeed): 28 | def __init__(self, maxLen=None): 29 | super(MemFeed, self).__init__(maxLen) 30 | 31 | self.__values = [] 32 | self.__nextIdx = 0 33 | 34 | def reset(self): 35 | self.__nextIdx = 0 36 | feed.BaseFeed.reset(self) 37 | 38 | def start(self): 39 | super().start() 40 | # Now that all the data is in place, sort it to dispatch it in order. 41 | self.__values.sort(key=lambda x: x[0]) 42 | 43 | def stop(self): 44 | pass 45 | 46 | def join(self): 47 | pass 48 | 49 | def eof(self): 50 | if self.__nextIdx < len(self.__values): 51 | return False 52 | else: 53 | return True 54 | 55 | def peekDateTime(self): 56 | ret = None 57 | 58 | if self.__nextIdx < len(self.__values): 59 | ret = self.__values[self.__nextIdx][0] 60 | 61 | return ret 62 | 63 | def createDataSeries(self, key, maxLen): 64 | return dataseries.SequenceDataSeries(maxLen) 65 | 66 | def getNextValues(self): 67 | ret = (None, None) 68 | 69 | if self.__nextIdx < len(self.__values): 70 | ret = self.__values[self.__nextIdx] 71 | self.__nextIdx += 1 72 | 73 | return ret 74 | 75 | # Add values to the feed. values should be a sequence of tuples. The tuples should have two elements: 76 | # 1: datetime.datetime. 77 | # 2: dictionary or dict-like object. 78 | def addValues(self, values): 79 | # Register a dataseries for each item. 80 | for key in values[0][1].keys(): 81 | self.registerDataSeries(key) 82 | 83 | self.__values.extend(values) 84 | -------------------------------------------------------------------------------- /mooquant/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import logging 23 | import threading 24 | 25 | initLock = threading.Lock() 26 | rootLoggerInitialized = False 27 | 28 | level = logging.INFO 29 | file_log = None # File name 30 | console_log = True 31 | log_format = "%(asctime)s %(name)s [%(levelname)s] %(message)s" 32 | 33 | 34 | def init_handler(handler): 35 | handler.setFormatter(Formatter(log_format)) 36 | 37 | 38 | def init_logger(logger): 39 | logger.setLevel(level) 40 | 41 | if file_log is not None: 42 | fileHandler = logging.FileHandler(file_log) 43 | init_handler(fileHandler) 44 | logger.addHandler(fileHandler) 45 | 46 | if console_log: 47 | consoleHandler = logging.StreamHandler() 48 | init_handler(consoleHandler) 49 | logger.addHandler(consoleHandler) 50 | 51 | 52 | def initialize(): 53 | global rootLoggerInitialized 54 | 55 | with initLock: 56 | if not rootLoggerInitialized: 57 | init_logger(logging.getLogger()) 58 | rootLoggerInitialized = True 59 | 60 | 61 | def getLogger(name=None): 62 | initialize() 63 | return logging.getLogger(name) 64 | 65 | 66 | # This formatter provides a way to hook in formatTime. 67 | class Formatter(logging.Formatter): 68 | DATETIME_HOOK = None 69 | 70 | def formatTime(self, record, datefmt=None): 71 | newDateTime = None 72 | 73 | if Formatter.DATETIME_HOOK is not None: 74 | newDateTime = Formatter.DATETIME_HOOK() 75 | 76 | if newDateTime is None: 77 | ret = super(Formatter, self).formatTime(record, datefmt) 78 | else: 79 | ret = str(newDateTime) 80 | 81 | return ret 82 | -------------------------------------------------------------------------------- /mooquant/marketsession.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import pytz 23 | 24 | 25 | # http://en.wikipedia.org/wiki/List_of_market_opening_times 26 | class MarketSession(object): 27 | """Base class for market sessions. 28 | 29 | .. note:: 30 | This is a base class and should not be used directly. 31 | """ 32 | 33 | @classmethod 34 | def getTimezone(cls): 35 | """Returns the pytz timezone for the market session.""" 36 | return cls.timezone 37 | 38 | 39 | ###################################################################### 40 | # US 41 | 42 | class NASDAQ(MarketSession): 43 | """NASDAQ market session.""" 44 | timezone = pytz.timezone("US/Eastern") 45 | 46 | 47 | class NYSE(MarketSession): 48 | """New York Stock Exchange market session.""" 49 | timezone = pytz.timezone("US/Eastern") 50 | 51 | 52 | class USEquities(MarketSession): 53 | """US Equities market session.""" 54 | timezone = pytz.timezone("US/Eastern") 55 | 56 | 57 | ###################################################################### 58 | # South America 59 | 60 | class MERVAL(MarketSession): 61 | """Buenos Aires (Argentina) market session.""" 62 | timezone = pytz.timezone("America/Argentina/Buenos_Aires") 63 | 64 | 65 | class BOVESPA(MarketSession): 66 | """BOVESPA (Brazil) market session.""" 67 | timezone = pytz.timezone("America/Sao_Paulo") 68 | 69 | 70 | ###################################################################### 71 | # Europe 72 | 73 | class FTSE(MarketSession): 74 | """ London Stock Exchange market session.""" 75 | timezone = pytz.timezone("Europe/London") 76 | 77 | 78 | ###################################################################### 79 | # Asia 80 | 81 | class TSE(MarketSession): 82 | """Tokyo Stock Exchange market session.""" 83 | timezone = pytz.timezone("Asia/Tokyo") 84 | -------------------------------------------------------------------------------- /mooquant/optimizer/__init__.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: bopo.wang 19 | """ 20 | -------------------------------------------------------------------------------- /mooquant/optimizer/server.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: bopo.wang 19 | """ 20 | 21 | import mooquant.logger 22 | from mooquant.optimizer import base 23 | 24 | logger = mooquant.logger.getLogger('optimizer.server') 25 | 26 | 27 | class Results(object): 28 | """The results of the strategy executions.""" 29 | 30 | def __init__(self, parameters, result): 31 | self.__parameters = parameters 32 | self.__result = result 33 | 34 | def getParameters(self): 35 | """Returns a sequence of parameter values.""" 36 | return self.__parameters 37 | 38 | def getResult(self): 39 | """Returns the result for a given set of parameters.""" 40 | return self.__result 41 | 42 | 43 | def serve(barFeed, strategyParameters, address, port, drivce='xml'): 44 | """Executes a server that will provide bars and strategy parameters for workers to use. 45 | 46 | :param drivce: backend server drivce. 47 | :param barFeed: The bar feed that each worker will use to backtest the strategy. 48 | :type barFeed: :class:`mooquant.barfeed.BarFeed`. 49 | :param strategyParameters: The set of parameters to use for backtesting. An iterable object where **each element is a tuple that holds parameter values**. 50 | :param address: The address to listen for incoming worker connections. 51 | :type address: string. 52 | :param port: The port to listen for incoming worker connections. 53 | :type port: int. 54 | :rtype: A :class:`Results` instance with the best results found or None if no results were obtained. 55 | """ 56 | 57 | paramSource = base.ParameterSource(strategyParameters) 58 | resultSinc = base.ResultSinc() 59 | 60 | if drivce not in ('xml', 'zmq'): 61 | logger.error('drivce not found') 62 | raise Execute('drivce not found') 63 | 64 | if drivce == 'xml': 65 | from mooquant.optimizer import xmlrpcserver as server 66 | elif drivce == 'zmq': 67 | from mooquant.optimizer import zmqrpcserver as server 68 | 69 | s = server.Server(paramSource, resultSinc, barFeed, address, port) 70 | logger.info("Starting server") 71 | 72 | s.serve() 73 | logger.info("Server finished") 74 | 75 | ret = None 76 | bestResult, bestParameters = resultSinc.getBest() 77 | 78 | if bestResult is not None: 79 | logger.info("Best final result {} with parameters {}".format(bestResult, bestParameters.args)) 80 | ret = Results(bestParameters.args, bestResult) 81 | else: 82 | logger.error("No results. All jobs failed or no jobs were processed.") 83 | 84 | return ret 85 | -------------------------------------------------------------------------------- /mooquant/provider/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | -------------------------------------------------------------------------------- /mooquant/provider/bitcoincharts/__init__.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: bopo.wang 19 | """ 20 | -------------------------------------------------------------------------------- /mooquant/provider/bitstamp/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | -------------------------------------------------------------------------------- /mooquant/provider/bitstamp/barfeed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant.provider.bitstamp import livefeed 23 | 24 | LiveTradeFeed = livefeed.LiveTradeFeed 25 | -------------------------------------------------------------------------------- /mooquant/provider/bitstamp/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import broker 23 | from mooquant import logger as logging 24 | 25 | logger = logging.getLogger("bitstamp") 26 | btc_symbol = "BTC" 27 | 28 | 29 | class BTCTraits(broker.InstrumentTraits): 30 | def roundQuantity(self, quantity): 31 | return round(quantity, 8) 32 | -------------------------------------------------------------------------------- /mooquant/provider/csvfeed.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | from datetime import datetime 22 | 23 | import pandas as pd 24 | from mooquant import dataseries 25 | from mooquant.barfeed import membf 26 | from mooquant.barfeed.pandasfeed import dataframeToBar, dataframeToTick 27 | 28 | 29 | class Feed(membf.BarFeed): 30 | def __init__(self, frequency, maxLen=dataseries.DEFAULT_MAX_LEN): 31 | membf.BarFeed.__init__(self, frequency, maxLen) 32 | self.__frequency = frequency 33 | 34 | def barsHaveAdjClose(self): 35 | return False 36 | 37 | def setDateTimeFormat(self, iformat): 38 | self._datetime_format = iformat 39 | 40 | def loadBars(self, instrument, exchange_id, fromdate, todate, path): 41 | try: 42 | fromdate = datetime.strptime(fromdate, '%Y%m%d') 43 | todate = datetime.strptime(todate, '%Y%m%d') 44 | except Exception: 45 | raise Exception('invalid date format, e.g. 20160206') 46 | 47 | idataframe = pd.read_csv(path) 48 | idataframe.ix[:, 'datetime'] = idataframe.ix[:, 'datetime'].apply( 49 | lambda x: datetime.strptime(x, self._datetime_format)) 50 | idataframe = idataframe[(idataframe.datetime >= fromdate) & (idataframe.datetime <= todate)] 51 | 52 | bars = dataframeToBar(idataframe, self.__frequency) 53 | mooquant_id = instrument + '.' + exchange_id 54 | 55 | self.addBarsFromSequence(mooquant_id, bars) 56 | 57 | def loadTicks(self, instrument, exchange_id, fromdate, todate, path): 58 | try: 59 | fromdate = datetime.strptime(fromdate, '%Y%m%d') 60 | todate = datetime.strptime(todate, '%Y%m%d') 61 | except Exception: 62 | raise Exception('invalid date format, e.g. 20160206') 63 | 64 | idataframe = pd.read_csv(path) 65 | idataframe = idataframe.sort_values(by='datetime') 66 | idataframe.ix[:, 'datetime'] = idataframe.ix[:, 'datetime'].apply( 67 | lambda x: datetime.strptime(x, self._datetime_format)) 68 | idataframe = idataframe[(idataframe.datetime >= fromdate) & (idataframe.datetime <= todate)] 69 | bars = dataframeToTick(idataframe, self.__frequency) 70 | mooquant_id = instrument + '.' + exchange_id 71 | 72 | self.addBarsFromSequence(mooquant_id, bars) 73 | 74 | def closeDB(self): 75 | self.__db.closeDB() 76 | -------------------------------------------------------------------------------- /mooquant/provider/xignite/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | -------------------------------------------------------------------------------- /mooquant/talibext/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | -------------------------------------------------------------------------------- /mooquant/technical/bollinger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import dataseries 23 | from mooquant.technical import ma, stats 24 | 25 | 26 | # 布林带指标 27 | class BollingerBands(object): 28 | """Bollinger Bands filter as described in 29 | http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:bollinger_bands. 30 | 31 | :param dataSeries: The DataSeries instance being filtered. 32 | :type dataSeries: :class:`mooquant.dataseries.DataSeries`. 33 | :param period: The number of values to use in the calculation. Must be > 1. 34 | :type period: int. 35 | :param numStdDev: The number of standard deviations to use for the upper and lower bands. 36 | :type numStdDev: int. 37 | :param maxLen: The maximum number of values to hold. 38 | Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the 39 | opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. 40 | :type maxLen: int. 41 | """ 42 | 43 | def __init__(self, dataSeries, period, numStdDev, maxLen=None): 44 | self.__sma = ma.SMA(dataSeries, period, maxLen=maxLen) 45 | self.__stdDev = stats.StdDev(dataSeries, period, maxLen=maxLen) 46 | self.__upperBand = dataseries.SequenceDataSeries(maxLen) 47 | self.__lowerBand = dataseries.SequenceDataSeries(maxLen) 48 | self.__numStdDev = numStdDev 49 | # It is important to subscribe after sma and stddev since we'll use those values. 50 | dataSeries.getNewValueEvent().subscribe(self.__onNewValue) 51 | 52 | def __onNewValue(self, dataSeries, dateTime, value): 53 | upperValue = None 54 | lowerValue = None 55 | 56 | if value is not None: 57 | sma = self.__sma[-1] 58 | if sma is not None: 59 | stdDev = self.__stdDev[-1] 60 | upperValue = sma + stdDev * self.__numStdDev 61 | lowerValue = sma + stdDev * self.__numStdDev * -1 62 | 63 | self.__upperBand.appendWithDateTime(dateTime, upperValue) 64 | self.__lowerBand.appendWithDateTime(dateTime, lowerValue) 65 | 66 | def getUpperBand(self): 67 | """ 68 | Returns the upper band as a :class:`mooquant.dataseries.DataSeries`. 69 | """ 70 | return self.__upperBand 71 | 72 | def getMiddleBand(self): 73 | """ 74 | Returns the middle band as a :class:`mooquant.dataseries.DataSeries`. 75 | """ 76 | return self.__sma 77 | 78 | def getLowerBand(self): 79 | """ 80 | Returns the lower band as a :class:`mooquant.dataseries.DataSeries`. 81 | """ 82 | return self.__lowerBand 83 | -------------------------------------------------------------------------------- /mooquant/technical/cumret.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import technical 23 | 24 | 25 | class CumRetEventWindow(technical.EventWindow): 26 | def __init__(self): 27 | super().__init__(2) 28 | self.__prevCumRet = 0 29 | 30 | def getValue(self): 31 | ret = None 32 | 33 | if self.windowFull(): 34 | values = self.getValues() 35 | prev = values[0] 36 | actual = values[1] 37 | netReturn = (actual - prev) / float(prev) 38 | ret = (1 + self.__prevCumRet) * (1 + netReturn) - 1 39 | self.__prevCumRet = ret 40 | 41 | return ret 42 | 43 | 44 | class CumulativeReturn(technical.EventBasedFilter): 45 | """This filter calculates cumulative returns over another dataseries. 46 | 47 | :param dataSeries: The DataSeries instance being filtered. 48 | :type dataSeries: :class:`mooquant.dataseries.DataSeries`. 49 | :param maxLen: The maximum number of values to hold. 50 | Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the 51 | opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. 52 | :type maxLen: int. 53 | """ 54 | 55 | def __init__(self, dataSeries, maxLen=None): 56 | super(CumulativeReturn, self).__init__(dataSeries, CumRetEventWindow(), maxLen) 57 | -------------------------------------------------------------------------------- /mooquant/technical/highlow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import technical 23 | 24 | 25 | # 高低价指标 26 | class HighLowEventWindow(technical.EventWindow): 27 | def __init__(self, windowSize, useMin): 28 | super().__init__(windowSize) 29 | self.__useMin = useMin 30 | 31 | def getValue(self): 32 | ret = None 33 | 34 | if self.windowFull(): 35 | values = self.getValues() 36 | 37 | if self.__useMin: 38 | ret = values.min() 39 | else: 40 | ret = values.max() 41 | 42 | return ret 43 | 44 | 45 | class High(technical.EventBasedFilter): 46 | """This filter calculates the highest value. 47 | 48 | :param dataSeries: The DataSeries instance being filtered. 49 | :type dataSeries: :class:`mooquant.dataseries.DataSeries`. 50 | :param period: The number of values to use to calculate the highest value. 51 | :type period: int. 52 | :param maxLen: The maximum number of values to hold. 53 | Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the 54 | opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. 55 | :type maxLen: int. 56 | """ 57 | 58 | def __init__(self, dataSeries, period, maxLen=None): 59 | super().__init__(dataSeries, HighLowEventWindow(period, False), maxLen) 60 | 61 | 62 | class Low(technical.EventBasedFilter): 63 | """This filter calculates the lowest value. 64 | 65 | :param dataSeries: The DataSeries instance being filtered. 66 | :type dataSeries: :class:`mooquant.dataseries.DataSeries`. 67 | :param period: The number of values to use to calculate the lowest value. 68 | :type period: int. 69 | :param maxLen: The maximum number of values to hold. 70 | Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the 71 | opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. 72 | :type maxLen: int. 73 | """ 74 | 75 | def __init__(self, dataSeries, period, maxLen=None): 76 | super().__init__(dataSeries, HighLowEventWindow(period, True), maxLen) 77 | -------------------------------------------------------------------------------- /mooquant/technical/ratio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import technical, utils 23 | 24 | 25 | class RatioEventWindow(technical.EventWindow): 26 | def __init__(self): 27 | super().__init__(2) 28 | 29 | def getValue(self): 30 | ret = None 31 | 32 | if self.windowFull(): 33 | prev = self.getValues()[0] 34 | actual = self.getValues()[-1] 35 | ret = utils.get_change_percentage(actual, prev) 36 | 37 | return ret 38 | 39 | 40 | # Calculates the ratio between a value and the previous one. 41 | # The ratio can't be calculated if a previous value is 0. 42 | class Ratio(technical.EventBasedFilter): 43 | def __init__(self, dataSeries, maxLen=None): 44 | super().__init__(dataSeries, RatioEventWindow(), maxLen) 45 | -------------------------------------------------------------------------------- /mooquant/technical/roc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import technical 23 | 24 | 25 | class ROCEventWindow(technical.EventWindow): 26 | def __init__(self, windowSize): 27 | super().__init__(windowSize) 28 | 29 | def getValue(self): 30 | ret = None 31 | 32 | if self.windowFull(): 33 | prev = self.getValues()[0] 34 | actual = self.getValues()[-1] 35 | 36 | if actual is not None and prev is not None: 37 | diff = float(actual - prev) 38 | 39 | if diff == 0: 40 | ret = float(0) 41 | elif prev != 0: 42 | ret = diff / prev 43 | 44 | return ret 45 | 46 | 47 | class RateOfChange(technical.EventBasedFilter): 48 | """Rate of change filter as described in http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:rate_of_change_roc_and_momentum. 49 | 50 | :param dataSeries: The DataSeries instance being filtered. 51 | :type dataSeries: :class:`mooquant.dataseries.DataSeries`. 52 | :param valuesAgo: The number of values back that a given value will compare to. Must be > 0. 53 | :type valuesAgo: int. 54 | :param maxLen: The maximum number of values to hold. 55 | Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the 56 | opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. 57 | :type maxLen: int. 58 | """ 59 | 60 | def __init__(self, dataSeries, valuesAgo, maxLen=None): 61 | assert (valuesAgo > 0) 62 | super().__init__(dataSeries, ROCEventWindow(valuesAgo + 1), maxLen) 63 | -------------------------------------------------------------------------------- /mooquant/technical/vwap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | from mooquant import technical 23 | from mooquant.dataseries import bards 24 | 25 | 26 | class VWAPEventWindow(technical.EventWindow): 27 | def __init__(self, windowSize, useTypicalPrice): 28 | super().__init__(windowSize, dtype=object) 29 | self.__useTypicalPrice = useTypicalPrice 30 | 31 | def getValue(self): 32 | ret = None 33 | 34 | if self.windowFull(): 35 | cumTotal = 0 36 | cumVolume = 0 37 | 38 | for bar in self.getValues(): 39 | if self.__useTypicalPrice: 40 | cumTotal += bar.getTypicalPrice() * bar.getVolume() 41 | else: 42 | cumTotal += bar.getPrice() * bar.getVolume() 43 | 44 | cumVolume += bar.getVolume() 45 | 46 | ret = cumTotal / float(cumVolume) 47 | 48 | return ret 49 | 50 | 51 | class VWAP(technical.EventBasedFilter): 52 | """Volume Weighted Average Price filter. 53 | 54 | :param dataSeries: The DataSeries instance being filtered. 55 | :type dataSeries: :class:`mooquant.dataseries.bards.BarDataSeries`. 56 | :param period: The number of values to use to calculate the VWAP. 57 | :type period: int. 58 | :param useTypicalPrice: True if the typical price should be used instead of the closing price. 59 | :type useTypicalPrice: boolean. 60 | :param maxLen: The maximum number of values to hold. 61 | Once a bounded length is full, when new items are added, a corresponding number of items are discarded from the 62 | opposite end. If None then dataseries.DEFAULT_MAX_LEN is used. 63 | :type maxLen: int. 64 | """ 65 | 66 | def __init__(self, dataSeries, period, useTypicalPrice=False, maxLen=None): 67 | assert isinstance(dataSeries, bards.BarDataSeries), \ 68 | "dataSeries must be a dataseries.bards.BarDataSeries instance" 69 | 70 | super().__init__(dataSeries, VWAPEventWindow(period, useTypicalPrice), maxLen) 71 | 72 | def getPeriod(self): 73 | return self.getEventWindow().getWindowSize() 74 | -------------------------------------------------------------------------------- /mooquant/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | -------------------------------------------------------------------------------- /mooquant/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | 23 | def get_change_percentage(actual, prev): 24 | if actual is None or prev is None or prev == 0: 25 | raise Exception("Invalid values") 26 | 27 | diff = actual - prev 28 | ret = diff / float(abs(prev)) 29 | 30 | return ret 31 | 32 | 33 | def safe_min(left, right): 34 | if left is None: 35 | return right 36 | elif right is None: 37 | return left 38 | 39 | return min(left, right) 40 | 41 | 42 | def safe_max(left, right): 43 | if left is None: 44 | return right 45 | elif right is None: 46 | return left 47 | 48 | return max(left, right) 49 | -------------------------------------------------------------------------------- /mooquant/utils/csvutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import csv 23 | import logging 24 | 25 | import requests 26 | 27 | logging.getLogger("requests").setLevel(logging.ERROR) 28 | 29 | 30 | # A faster (but limited) version of csv.DictReader 31 | class FastDictReader(object): 32 | def __init__(self, f, fieldnames=None, dialect="excel", *args, **kwargs): 33 | self.__fieldNames = fieldnames 34 | self.reader = csv.reader(f, dialect, *args, **kwargs) 35 | 36 | if self.__fieldNames is None: 37 | self.__fieldNames = next(self.reader) 38 | 39 | self.__dict = {} 40 | 41 | def __iter__(self): 42 | return self 43 | 44 | def __next__(self): 45 | # Skip empty rows. 46 | row = next(self.reader) 47 | 48 | while row == []: 49 | row = next(self.reader) 50 | 51 | # Check that the row has the right number of columns. 52 | assert (len(self.__fieldNames) == len(row)) 53 | 54 | # Copy the row values into the dict. 55 | for i in range(len(self.__fieldNames)): 56 | self.__dict[self.__fieldNames[i]] = row[i] 57 | 58 | return self.__dict 59 | 60 | def next(self): 61 | # Skip empty rows. 62 | row = next(self.reader) 63 | 64 | while row == []: 65 | row = next(self.reader) 66 | 67 | # Check that the row has the right number of columns. 68 | assert (len(self.__fieldNames) == len(row)) 69 | 70 | # Copy the row values into the dict. 71 | for i in range(len(self.__fieldNames)): 72 | self.__dict[self.__fieldNames[i]] = row[i] 73 | 74 | return self.__dict 75 | 76 | 77 | def download_csv(url, url_params=None, content_type="text/csv"): 78 | response = requests.get(url, params=url_params) 79 | response.raise_for_status() 80 | response_content_type = response.headers['content-type'] 81 | 82 | if response_content_type != content_type: 83 | raise Exception("Invalid content-type: %s" % response_content_type) 84 | 85 | ret = response.text 86 | 87 | # Remove the BOM 88 | # 移除 UTF8 的 BOM 头字符 89 | while not ret[0].isalnum(): 90 | ret = ret[1:] 91 | 92 | return ret 93 | 94 | 95 | def float_or_string(value): 96 | try: 97 | ret = float(value) 98 | except Exception: 99 | ret = value 100 | 101 | return ret 102 | -------------------------------------------------------------------------------- /mooquant/utils/dt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import datetime 23 | 24 | import pytz 25 | 26 | 27 | # 时间转原生格式 28 | def datetime_is_naive(dateTime): 29 | """ Returns True if dateTime is naive.""" 30 | return dateTime.tzinfo is None or dateTime.tzinfo.utcoffset(dateTime) is None 31 | 32 | 33 | # 移除时区信息 34 | def unlocalize(dateTime): 35 | return dateTime.replace(tzinfo=None) 36 | 37 | 38 | # 本地化时间转换 39 | def localize(dateTime, timeZone): 40 | """Returns a datetime adjusted to a timezone: 41 | 返回将一个日期时间调整到一个时区 42 | 43 | * If dateTime is a naive datetime (datetime with no timezone information), timezone information is added but date 44 | and time remains the same. 45 | * If dateTime is not a naive datetime, a datetime object with new tzinfo attribute is returned, adjusting the date 46 | and time data so the result is the same UTC time. 47 | """ 48 | 49 | if datetime_is_naive(dateTime): 50 | ret = timeZone.localize(dateTime) 51 | else: 52 | ret = dateTime.astimezone(timeZone) 53 | 54 | return ret 55 | 56 | 57 | def as_utc(dateTime): 58 | return localize(dateTime, pytz.utc) 59 | 60 | 61 | # 时间转时间戳 62 | def datetime_to_timestamp(dateTime): 63 | """ Converts a datetime.datetime to a UTC timestamp.""" 64 | diff = as_utc(dateTime) - epoch_utc 65 | return diff.total_seconds() 66 | 67 | 68 | # 时间戳转时间 69 | def timestamp_to_datetime(timeStamp, localized=True): 70 | """ Converts a UTC timestamp to a datetime.datetime.""" 71 | ret = datetime.datetime.utcfromtimestamp(timeStamp) 72 | 73 | if localized: 74 | ret = localize(ret, pytz.utc) 75 | 76 | return ret 77 | 78 | 79 | # 获取第一个星期一 80 | def get_first_monday(year): 81 | ret = datetime.date(year, 1, 1) 82 | 83 | if ret.weekday() != 0: 84 | diff = 7 - ret.weekday() 85 | ret = ret + datetime.timedelta(days=diff) 86 | 87 | return ret 88 | 89 | 90 | def get_last_monday(year): 91 | ret = datetime.date(year, 12, 31) 92 | 93 | if ret.weekday() != 0: 94 | diff = ret.weekday() * -1 95 | ret = ret + datetime.timedelta(days=diff) 96 | 97 | return ret 98 | 99 | 100 | epoch_utc = as_utc(datetime.datetime(1970, 1, 1)) 101 | -------------------------------------------------------------------------------- /mooquant/utils/stats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import numpy as np 23 | 24 | 25 | # 取平均值 26 | def mean(values): 27 | ret = None 28 | 29 | if len(values): 30 | ret = np.array(values).mean() 31 | 32 | return ret 33 | 34 | 35 | # 取方差值 36 | def stddev(values, ddof=1): 37 | ret = None 38 | 39 | if len(values): 40 | ret = np.array(values).std(ddof=ddof) 41 | 42 | return ret 43 | -------------------------------------------------------------------------------- /mooquant/warninghelpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2017 bopo.wang 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: bopo.wang 20 | """ 21 | 22 | import warnings 23 | 24 | 25 | class MooQuantDeprecationWarning(DeprecationWarning): 26 | pass 27 | 28 | 29 | warnings.simplefilter("default", MooQuantDeprecationWarning) 30 | 31 | 32 | # Deprecation warnings are disabled by default in Python 2.7, so this helper function enables them back. 33 | def deprecation_warning(msg, stacklevel=0): 34 | warnings.warn(msg, category=MooQuantDeprecationWarning, stacklevel=stacklevel + 1) 35 | -------------------------------------------------------------------------------- /mooquant/websocket/__init__.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: bopo.wang 19 | """ 20 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements.txt requirements.in 6 | # 7 | --index-url http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 8 | coloredlogs 9 | statsmodels 10 | matplotlib 11 | requests 12 | tornado<5 13 | tushare 14 | cython 15 | mootdx 16 | ta-lib 17 | pytest 18 | pandas 19 | scipy 20 | ws4py 21 | pytz 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements.txt requirements.in 6 | # 7 | --index-url http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com 8 | 9 | asn1crypto==0.24.0 # via cryptography 10 | attrs==18.2.0 # via pytest 11 | certifi==2018.8.24 # via requests 12 | cffi==1.11.5 # via cryptography 13 | chardet==3.0.4 # via requests 14 | click==6.7 # via mootdx, pytdx 15 | coloredlogs==10.0 16 | cryptography==2.3.1 # via pytdx 17 | cycler==0.10.0 # via matplotlib 18 | cython==0.28.5 19 | humanfriendly==4.16.1 # via coloredlogs 20 | idna==2.7 # via cryptography, requests 21 | kiwisolver==1.0.1 # via matplotlib 22 | matplotlib==3.0.0 23 | mootdx==0.2.1 24 | more-itertools==4.3.0 # via pytest 25 | numpy==1.15.2 # via matplotlib, pandas, patsy, scipy, ta-lib 26 | pandas==0.23.4 27 | patsy==0.5.0 # via statsmodels 28 | pluggy==0.7.1 # via pytest 29 | prettytable==7 # via mootdx 30 | py==1.6.0 # via pytest 31 | pycparser==2.19 # via cffi 32 | pyparsing==2.2.1 # via matplotlib 33 | pytdx==1.67 # via mootdx 34 | pytest==3.8.1 35 | python-dateutil==2.7.3 # via matplotlib, pandas 36 | pytz==2018.5 37 | requests==2.19.1 38 | scipy==1.1.0 39 | six==1.11.0 # via cryptography, cycler, matplotlib, more-itertools, patsy, pytdx, pytest, python-dateutil 40 | statsmodels==0.9.0 41 | ta-lib==0.4.17 42 | tornado==5.1.1 43 | tushare==1.2.12 44 | urllib3==1.22 # via requests 45 | ws4py==0.5.1 46 | -------------------------------------------------------------------------------- /samples/bbands.py: -------------------------------------------------------------------------------- 1 | from mooquant import plotter, strategy 2 | from mooquant.analyzer import sharpe 3 | from mooquant.technical import bollinger 4 | from mooquant.tools import mootdx 5 | 6 | 7 | class BBands(strategy.BacktestingStrategy): 8 | def __init__(self, feed, instrument, bBandsPeriod): 9 | super().__init__(feed) 10 | self.__instrument = instrument 11 | self.__bbands = bollinger.BollingerBands(feed[instrument].getCloseDataSeries(), bBandsPeriod, 2) 12 | 13 | def getBollingerBands(self): 14 | return self.__bbands 15 | 16 | def onBars(self, bars): 17 | lower = self.__bbands.getLowerBand()[-1] 18 | upper = self.__bbands.getUpperBand()[-1] 19 | 20 | if lower is None: 21 | return 22 | 23 | shares = self.getBroker().getShares(self.__instrument) 24 | bar = bars[self.__instrument] 25 | 26 | if shares == 0 and bar.getClose() < lower: 27 | sharesToBuy = int(self.getBroker().getCash(False) / bar.getClose()) 28 | self.marketOrder(self.__instrument, sharesToBuy) 29 | elif shares > 0 and bar.getClose() > upper: 30 | self.marketOrder(self.__instrument, -1 * shares) 31 | 32 | 33 | def main(plot): 34 | import coloredlogs 35 | coloredlogs.install(level='DEBUG', fmt='[%(asctime)s] %(levelname)s %(message)s') 36 | 37 | instrument = "600036" 38 | bBandsPeriod = 40 39 | 40 | feeds = mootdx.build_feed([instrument], 2003, 2018, "histdata/mootdx") 41 | strat = BBands(feeds, instrument, bBandsPeriod) 42 | 43 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 44 | strat.attachAnalyzer(sharpeRatioAnalyzer) 45 | 46 | if plot: 47 | plt = plotter.StrategyPlotter(strat, True, True, True) 48 | plt.getInstrumentSubplot(instrument).addDataSeries("upper", strat.getBollingerBands().getUpperBand()) 49 | plt.getInstrumentSubplot(instrument).addDataSeries("middle", strat.getBollingerBands().getMiddleBand()) 50 | plt.getInstrumentSubplot(instrument).addDataSeries("lower", strat.getBollingerBands().getLowerBand()) 51 | 52 | strat.run() 53 | 54 | print("Sharpe ratio: %.2f" % sharpeRatioAnalyzer.getSharpeRatio(0.05)) 55 | 56 | if plot: 57 | plt.plot() 58 | 59 | 60 | if __name__ == "__main__": 61 | main(True) 62 | -------------------------------------------------------------------------------- /samples/bccharts_example_1.py: -------------------------------------------------------------------------------- 1 | from mooquant import bar 2 | from mooquant.tools import mootdx, resample 3 | 4 | 5 | def main(): 6 | import coloredlogs 7 | coloredlogs.install(level='DEBUG', fmt='[%(asctime)s] %(levelname)s %(message)s') 8 | 9 | instrument = "600036" 10 | feeds = mootdx.build_feed([instrument], 2003, 2018, "histdata/mootdx") 11 | resample.resample_to_csv(feeds, bar.Frequency.MINUTE * 30, "30min-bitstampUSD.csv") 12 | 13 | 14 | if __name__ == "__main__": 15 | main() 16 | -------------------------------------------------------------------------------- /samples/compinv-1.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.analyzer import returns, sharpe 3 | from mooquant.barfeed import yahoofeed 4 | from mooquant.utils import stats 5 | 6 | 7 | class MyStrategy(strategy.BacktestingStrategy): 8 | def __init__(self, feed): 9 | super().__init__(feed, 1000000) 10 | 11 | # We wan't to use adjusted close prices instead of close. 12 | self.setUseAdjustedValues(True) 13 | 14 | # Place the orders to get them processed on the first bar. 15 | orders = { 16 | "aeti": 297810, 17 | "egan": 81266, 18 | "glng": 11095, 19 | "simo": 17293, 20 | } 21 | for instrument, quantity in list(orders.items()): 22 | self.marketOrder(instrument, quantity, onClose=True, allOrNone=True) 23 | 24 | def onBars(self, bars): 25 | pass 26 | 27 | 28 | def main(): 29 | import coloredlogs 30 | coloredlogs.install(level='DEBUG', fmt='[%(asctime)s] %(levelname)s %(message)s') 31 | 32 | # Load the yahoo feed from CSV files. 33 | feed = yahoofeed.Feed() 34 | feed.addBarsFromCSV("aeti", "./tests/data/aeti-2011-yahoofinance.csv") 35 | feed.addBarsFromCSV("egan", "./tests/data/egan-2011-yahoofinance.csv") 36 | feed.addBarsFromCSV("glng", "./tests/data/glng-2011-yahoofinance.csv") 37 | feed.addBarsFromCSV("simo", "./tests/data/simo-2011-yahoofinance.csv") 38 | 39 | # Evaluate the strategy with the feed's bars. 40 | myStrategy = MyStrategy(feed) 41 | 42 | # Attach returns and sharpe ratio analyzers. 43 | retAnalyzer = returns.Returns() 44 | myStrategy.attachAnalyzer(retAnalyzer) 45 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 46 | myStrategy.attachAnalyzer(sharpeRatioAnalyzer) 47 | 48 | # Run the strategy 49 | myStrategy.run() 50 | 51 | # Print the results. 52 | print("Final portfolio value: $%.2f" % myStrategy.getResult()) 53 | print("Anual return: %.2f %%" % (retAnalyzer.getCumulativeReturns()[-1] * 100)) 54 | print("Average daily return: %.2f %%" % (stats.mean(retAnalyzer.getReturns()) * 100)) 55 | print("Std. dev. daily return: %.4f" % (stats.stddev(retAnalyzer.getReturns()))) 56 | print("Sharpe ratio: %.2f" % (sharpeRatioAnalyzer.getSharpeRatio(0))) 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /samples/csvfeed_1.py: -------------------------------------------------------------------------------- 1 | from mooquant.feed import csvfeed 2 | 3 | feed = csvfeed.Feed("Date", "%Y-%m-%d") 4 | feed.addValuesFromCSV("./tests/data/quandl_gold_2.csv") 5 | 6 | for dateTime, value in feed: 7 | print(dateTime, value) 8 | -------------------------------------------------------------------------------- /samples/eventstudy.py: -------------------------------------------------------------------------------- 1 | from mooquant import eventprofiler 2 | from mooquant.barfeed import yahoofeed 3 | from mooquant.technical import ma, roc, stats 4 | 5 | 6 | class BuyOnGap(eventprofiler.Predicate): 7 | def __init__(self, feed): 8 | super().__init__() 9 | 10 | stdDevPeriod = 90 11 | smaPeriod = 20 12 | 13 | self.__returns = {} 14 | self.__stdDev = {} 15 | self.__ma = {} 16 | 17 | for instrument in feed.getRegisteredInstruments(): 18 | priceDS = feed[instrument].getAdjCloseDataSeries() 19 | # Returns over the adjusted close values. 20 | self.__returns[instrument] = roc.RateOfChange(priceDS, 1) 21 | # StdDev over those returns. 22 | self.__stdDev[instrument] = stats.StdDev(self.__returns[instrument], stdDevPeriod) 23 | # MA over the adjusted close values. 24 | self.__ma[instrument] = ma.SMA(priceDS, smaPeriod) 25 | 26 | def __gappedDown(self, instrument, bards): 27 | ret = False 28 | 29 | if self.__stdDev[instrument][-1] is not None: 30 | prevBar = bards[-2] 31 | currBar = bards[-1] 32 | low2OpenRet = (currBar.getOpen(True) - prevBar.getLow(True)) / float(prevBar.getLow(True)) 33 | 34 | if low2OpenRet < (self.__returns[instrument][-1] - self.__stdDev[instrument][-1]): 35 | ret = True 36 | 37 | return ret 38 | 39 | def __aboveSMA(self, instrument, bards): 40 | ret = False 41 | 42 | if self.__ma[instrument][-1] is not None and bards[-1].getOpen(True) > self.__ma[instrument][-1]: 43 | ret = True 44 | 45 | return ret 46 | 47 | def eventOccurred(self, instrument, bards): 48 | ret = False 49 | 50 | if self.__gappedDown(instrument, bards) and self.__aboveSMA(instrument, bards): 51 | ret = True 52 | 53 | return ret 54 | 55 | 56 | def main(plot): 57 | instruments = ["orcl", ] 58 | 59 | feed = yahoofeed.Feed() 60 | feed.addBarsFromCSV(instruments[0], "./tests/data/orcl-2000.csv") 61 | 62 | predicate = BuyOnGap(feed) 63 | eventProfiler = eventprofiler.Profiler(predicate, 5, 5) 64 | eventProfiler.run(feed, True) 65 | 66 | results = eventProfiler.getResults() 67 | print("%d events found" % (results.getEventCount())) 68 | 69 | if plot: 70 | eventprofiler.plot(results) 71 | 72 | 73 | if __name__ == "__main__": 74 | main(True) 75 | -------------------------------------------------------------------------------- /samples/quandl_sample.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from mooquant import plotter, strategy 4 | from mooquant.feed import csvfeed 5 | from mooquant.tools import quandl 6 | 7 | 8 | class MyStrategy(strategy.BacktestingStrategy): 9 | def __init__(self, feed, quandlFeed, instrument): 10 | super().__init__(feed) 11 | self.setUseAdjustedValues(True) 12 | self.__instrument = instrument 13 | 14 | # It is VERY important to add the the extra feed to the event dispatch loop before 15 | # running the strategy. 16 | self.getDispatcher().addSubject(quandlFeed) 17 | 18 | # Subscribe to events from the Quandl feed. 19 | quandlFeed.getNewValuesEvent().subscribe(self.onQuandlData) 20 | 21 | def onQuandlData(self, dateTime, values): 22 | self.info(values) 23 | 24 | def onBars(self, bars): 25 | self.info(bars[self.__instrument].getAdjClose()) 26 | 27 | 28 | def main(plot): 29 | instruments = ["GORO"] 30 | 31 | # Download GORO bars using WIKI source code. 32 | feed = quandl.build_feed("WIKI", instruments, 2006, 2012, "./data") 33 | 34 | quandlFeed = csvfeed.Feed("Date", "%Y-%m-%d") 35 | quandlFeed.setDateRange(datetime.datetime(2006, 1, 1), datetime.datetime(2012, 12, 31)) 36 | quandlFeed.addValuesFromCSV("./tests/data/quandl_gold_2.csv") 37 | myStrategy = MyStrategy(feed, quandlFeed, instruments[0]) 38 | 39 | if plot: 40 | plt = plotter.StrategyPlotter(myStrategy, True, False, False) 41 | plt.getOrCreateSubplot("quandl").addDataSeries("USD", quandlFeed["USD"]) 42 | plt.getOrCreateSubplot("quandl").addDataSeries("EUR", quandlFeed["EUR"]) 43 | plt.getOrCreateSubplot("quandl").addDataSeries("GBP", quandlFeed["GBP"]) 44 | 45 | myStrategy.run() 46 | 47 | if plot: 48 | plt.plot() 49 | 50 | 51 | if __name__ == "__main__": 52 | main(True) 53 | -------------------------------------------------------------------------------- /samples/rsi2.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.technical import cross, ma, rsi 3 | 4 | 5 | class RSI2(strategy.BacktestingStrategy): 6 | ''' 7 | RSI2 策略 8 | ''' 9 | 10 | def __init__(self, feed, instrument, entrySMA, exitSMA, rsiPeriod, overBoughtThreshold, overSoldThreshold): 11 | super().__init__(feed) 12 | self.__instrument = instrument 13 | 14 | # We'll use adjusted close values, if available, instead of regular close values. 15 | if feed.barsHaveAdjClose(): 16 | self.setUseAdjustedValues(True) 17 | 18 | self.__priceDS = feed[instrument].getPriceDataSeries() 19 | self.__entrySMA = ma.SMA(self.__priceDS, entrySMA) 20 | self.__exitSMA = ma.SMA(self.__priceDS, exitSMA) 21 | self.__rsi = rsi.RSI(self.__priceDS, rsiPeriod) 22 | self.__overBoughtThreshold = overBoughtThreshold 23 | self.__overSoldThreshold = overSoldThreshold 24 | self.__longPos = None 25 | self.__shortPos = None 26 | 27 | def getEntrySMA(self): 28 | return self.__entrySMA 29 | 30 | def getExitSMA(self): 31 | return self.__exitSMA 32 | 33 | def getRSI(self): 34 | return self.__rsi 35 | 36 | def onEnterCanceled(self, position): 37 | if self.__longPos == position: 38 | self.__longPos = None 39 | elif self.__shortPos == position: 40 | self.__shortPos = None 41 | else: 42 | assert False 43 | 44 | def onExitOk(self, position): 45 | if self.__longPos == position: 46 | self.__longPos = None 47 | elif self.__shortPos == position: 48 | self.__shortPos = None 49 | else: 50 | assert False 51 | 52 | def onExitCanceled(self, position): 53 | # If the exit was canceled, re-submit it. 54 | position.exitMarket() 55 | 56 | def onBars(self, bars): 57 | # Wait for enough bars to be available to calculate SMA and RSI. 58 | if self.__exitSMA[-1] is None or self.__entrySMA[-1] is None or self.__rsi[-1] is None: 59 | return 60 | 61 | bar = bars[self.__instrument] 62 | 63 | if self.__longPos is not None: 64 | if self.exitLongSignal(): 65 | self.__longPos.exitMarket() 66 | elif self.__shortPos is not None: 67 | if self.exitShortSignal(): 68 | self.__shortPos.exitMarket() 69 | else: 70 | if self.enterLongSignal(bar): 71 | shares = int(self.getBroker().getCash() * 0.9 / bars[self.__instrument].getPrice()) 72 | self.__longPos = self.enterLong(self.__instrument, shares, True) 73 | elif self.enterShortSignal(bar): 74 | shares = int(self.getBroker().getCash() * 0.9 / bars[self.__instrument].getPrice()) 75 | self.__shortPos = self.enterShort(self.__instrument, shares, True) 76 | 77 | def enterLongSignal(self, bar): 78 | return bar.getPrice() > self.__entrySMA[-1] and self.__rsi[-1] <= self.__overSoldThreshold 79 | 80 | def exitLongSignal(self): 81 | return cross.cross_above(self.__priceDS, self.__exitSMA) and not self.__longPos.exitActive() 82 | 83 | def enterShortSignal(self, bar): 84 | return bar.getPrice() < self.__entrySMA[-1] and self.__rsi[-1] >= self.__overBoughtThreshold 85 | 86 | def exitShortSignal(self): 87 | return cross.cross_below(self.__priceDS, self.__exitSMA) and not self.__shortPos.exitActive() 88 | -------------------------------------------------------------------------------- /samples/rsi2_sample.py: -------------------------------------------------------------------------------- 1 | from mooquant import plotter 2 | from mooquant.analyzer import sharpe 3 | from mooquant.barfeed import yahoofeed 4 | from mooquant.tools import tushare 5 | 6 | from rsi2 import RSI2 7 | 8 | 9 | # ('600016', 150, 5, 3, 93, 25) 10 | def main(plot): 11 | instrument = "600016" 12 | entrySMA = 150 13 | exitSMA = 5 14 | rsiPeriod = 3 15 | overBoughtThreshold = 93 16 | overSoldThreshold = 25 17 | 18 | # Download the bars. 19 | # feed = quandl.build_feed("WIKI", [instrument], 2009, 2012, "./data") 20 | # feeds = yahoofeed.Feed() 21 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2009-yahoofinance.csv") 22 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2010-yahoofinance.csv") 23 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2011-yahoofinance.csv") 24 | 25 | # instruments = ["600036"] 26 | 27 | feeds = tushare.build_feed([instrument], 2009, 2011, "histdata/mootdx") 28 | 29 | strat = RSI2(feeds, instrument, entrySMA, exitSMA, rsiPeriod, overBoughtThreshold, overSoldThreshold) 30 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 31 | strat.attachAnalyzer(sharpeRatioAnalyzer) 32 | 33 | if plot: 34 | plt = plotter.StrategyPlotter(strat, True, False, True) 35 | plt.getInstrumentSubplot(instrument).addDataSeries("Entry SMA", strat.getEntrySMA()) 36 | plt.getInstrumentSubplot(instrument).addDataSeries("Exit SMA", strat.getExitSMA()) 37 | plt.getOrCreateSubplot("rsi").addDataSeries("RSI", strat.getRSI()) 38 | plt.getOrCreateSubplot("rsi").addLine("Overbought", overBoughtThreshold) 39 | plt.getOrCreateSubplot("rsi").addLine("Oversold", overSoldThreshold) 40 | 41 | strat.run() 42 | print("夏普比率: %.2f" % sharpeRatioAnalyzer.getSharpeRatio(0.05)) 43 | 44 | if plot: 45 | plt.plot() 46 | 47 | 48 | if __name__ == "__main__": 49 | main(True) 50 | -------------------------------------------------------------------------------- /samples/sample.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | import coloredlogs 5 | from mooquant import plotter, strategy 6 | from mooquant.analyzer import drawdown, sharpe, trades 7 | from mooquant.technical import ma 8 | from mooquant.tools import mootdx 9 | 10 | coloredlogs.install(level='DEBUG', fmt='[%(asctime)s] %(levelname)s %(message)s') 11 | 12 | 13 | class MyStrategy(strategy.BacktestingStrategy): 14 | ''' 15 | # 1.构建一个策略 16 | ''' 17 | 18 | def __init__(self, feed, instrument): 19 | super(MyStrategy, self).__init__(feed) 20 | 21 | self.__position = None 22 | self.__sma = ma.SMA(feed[instrument].getCloseDataSeries(), 150) 23 | self.__instrument = instrument 24 | 25 | self.getBroker() 26 | 27 | def onEnterOk(self, position): 28 | execInfo = position.getEntryOrder().getExecutionInfo() 29 | self.info("买入 %.2f" % (execInfo.getPrice())) 30 | 31 | def onEnterCanceled(self, position): 32 | self.__position = None 33 | 34 | def onExitOk(self, position): 35 | execInfo = position.getExitOrder().getExecutionInfo() 36 | self.info("卖出 %.2f" % (execInfo.getPrice())) 37 | self.__position = None 38 | 39 | def onExitCanceled(self, position): 40 | # If the exit was canceled, re-submit it. 41 | self.__position.exitMarket() 42 | 43 | def getSMA(self): 44 | return self.__sma 45 | 46 | def onBars(self, bars): 47 | # 每一个数据都会抵达这里,就像becktest中的next 48 | # Wait for enough bars to be available to calculate a SMA. 49 | if self.__sma[-1] is None: 50 | return 51 | 52 | # bar.getTyoicalPrice = (bar.getHigh() + bar.getLow() + bar.getClose())/ 3.0 53 | bar = bars[self.__instrument] 54 | 55 | # If a position was not opened, check if we should enter a long position. 56 | if self.__position is None: 57 | if bar.getPrice() > self.__sma[-1]: 58 | # 开多头. 59 | self.__position = self.enterLong(self.__instrument, 100, True) 60 | 61 | # 平掉多头头寸. 62 | elif bar.getPrice() < self.__sma[-1] and not self.__position.exitActive(): 63 | self.__position.exitMarket() 64 | 65 | 66 | def main(): 67 | ''' 68 | 策略执行 69 | :return: 70 | ''' 71 | instrument = "600036" 72 | feeds = mootdx.build_feed([instrument], 2008, 2018, "histdata/mootdx") 73 | 74 | # 3.实例化策略 75 | strat = MyStrategy(feeds, instrument) 76 | 77 | # 4.设置指标和绘图 78 | ratio = sharpe.SharpeRatio() 79 | strat.attachAnalyzer(ratio) 80 | # plter = plotter.StrategyPlotter(strat) 81 | 82 | # 4.设置指标和绘图 83 | draws = drawdown.DrawDown() 84 | strat.attachAnalyzer(draws) 85 | 86 | tradeAnalyzer = trades.Trades() 87 | strat.attachAnalyzer(tradeAnalyzer) 88 | 89 | plter = plotter.StrategyPlotter(strat) 90 | 91 | # 5.运行策略 92 | strat.run() 93 | strat.info("最终收益: {}".format(strat.getResult())) 94 | 95 | # 6.输出夏普率、绘图 96 | strat.info("夏普比率: {}".format(ratio.getSharpeRatio(0))) 97 | strat.info("最大回撤: {}".format(draws.getMaxDrawDown())) 98 | strat.info("回撤时间: {}".format(draws.getLongestDrawDownDuration())) 99 | plter.plot() 100 | 101 | 102 | if __name__ == '__main__': 103 | main() 104 | -------------------------------------------------------------------------------- /samples/sample_05.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from mooquant import strategy 3 | from mooquant.analyzer import returns, sharpe 4 | from mooquant.bar import Frequency 5 | from mooquant.barfeed.csvfeed import GenericBarFeed 6 | from mooquant.utils import stats 7 | 8 | 9 | class MyStrategy(strategy.BacktestingStrategy): 10 | def __init__(self, feed): 11 | super().__init__(feed, 10000000) 12 | self.__position = None 13 | 14 | def onEnterOk(self, position): 15 | execInfo = position.getEntryOrder().getExecutionInfo() 16 | self.info("BUY at %.2f" % (execInfo.getPrice())) 17 | 18 | def onEnterCanceled(self, position): 19 | self.__position = None 20 | 21 | def onExitOk(self, position): 22 | execInfo = position.getExitOrder().getExecutionInfo() 23 | self.info("SELL at $%.2f" % (execInfo.getPrice())) 24 | self.__position = None 25 | 26 | def onExitCanceled(self, position): 27 | self.__position.exitMarket() 28 | 29 | def onBars(self, bars): 30 | # 1.我们弄一个简单的策略来假装一下 31 | day = bars.getDateTime().date().day 32 | if day == 5: 33 | self.__position = self.enterLong('a', 1, True) 34 | elif day == 10: 35 | self.__position = self.enterLong('b', 1, True) 36 | elif day == 15: 37 | self.__position = self.enterLong('c', 1, True) 38 | elif day == 20: 39 | self.__position = self.enterLong('d', 1, True) 40 | 41 | 42 | def main(): 43 | # 2.读取csv文件. 44 | feed = GenericBarFeed(Frequency.DAY, None, None) 45 | feed.addBarsFromCSV("a", "a.csv") 46 | feed.addBarsFromCSV("b", "b.csv") 47 | feed.addBarsFromCSV("c", "c.csv") 48 | feed.addBarsFromCSV("d", "d.csv") 49 | 50 | strat = MyStrategy(feed) 51 | 52 | # 3.加入分析器 53 | retAnalyzer = returns.Returns() 54 | strat.attachAnalyzer(retAnalyzer) 55 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 56 | strat.attachAnalyzer(sharpeRatioAnalyzer) 57 | 58 | # 4.运行策略 59 | strat.run() 60 | 61 | # 5.输出结果 62 | 63 | # 最终的投资组合价值 64 | print("Final portfolio value: $%.2f" % strat.getResult()) 65 | 66 | # 年化平均收益率 67 | print("Anual return: %.2f %%" % (retAnalyzer.getCumulativeReturns()[-1] * 100)) 68 | 69 | # 平均日收益 70 | print("Average daily return: %.2f %%" % (stats.mean(retAnalyzer.getReturns()) * 100)) 71 | 72 | # 每日收益标准开发 73 | print("Std. dev. daily return: %.4f" % (stats.stddev(retAnalyzer.getReturns()))) 74 | 75 | # 夏普比率 76 | print("Sharpe ratio: %.2f" % (sharpeRatioAnalyzer.getSharpeRatio(0))) 77 | 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /samples/sample_mootdx.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from mooquant import plotter, strategy 4 | from mooquant.analyzer import sharpe 5 | from mooquant.bar import Frequency 6 | from mooquant.barfeed.csvfeed import GenericBarFeed 7 | from mooquant.technical import ma 8 | from mooquant.tools import mootdx 9 | 10 | 11 | class Strategy(strategy.BacktestingStrategy): 12 | def __init__(self, feed, instrument): 13 | super(Strategy, self).__init__(feed) 14 | 15 | self.__position = None 16 | self.__sma = ma.SMA(feed[instrument].getCloseDataSeries(), 150) 17 | self.__instrument = instrument 18 | self.getBroker() 19 | 20 | def onEnterOk(self, position): 21 | execInfo = position.getEntryOrder().getExecutionInfo() 22 | self.info("买入 %.2f" % (execInfo.getPrice())) 23 | 24 | def onEnterCanceled(self, position): 25 | self.__position = None 26 | 27 | def onExitOk(self, position): 28 | execInfo = position.getExitOrder().getExecutionInfo() 29 | self.info("卖出 %.2f" % (execInfo.getPrice())) 30 | self.__position = None 31 | 32 | def onExitCanceled(self, position): 33 | # If the exit was canceled, re-submit it. 34 | self.__position.exitMarket() 35 | 36 | def getSMA(self): 37 | return self.__sma 38 | 39 | def onBars(self, bars): 40 | # 每一个数据都会抵达这里,就像becktest中的next 41 | # Wait for enough bars to be available to calculate a SMA. 42 | if self.__sma[-1] is None: 43 | return 44 | 45 | # bar.getTyoicalPrice = (bar.getHigh() + bar.getLow() + bar.getClose())/ 3.0 46 | bar = bars[self.__instrument] 47 | 48 | # If a position was not opened, check if we should enter a long position. 49 | if self.__position is None: 50 | if bar.getPrice() > self.__sma[-1]: 51 | # 开多头. 52 | self.__position = self.enterLong(self.__instrument, 100, True) 53 | 54 | # 平掉多头头寸. 55 | elif bar.getPrice() < self.__sma[-1] and not self.__position.exitActive(): 56 | self.__position.exitMarket() 57 | 58 | 59 | def main(): 60 | instruments = ["600036"] 61 | feeds = mootdx.build_feed(instruments, 2003, 2018, "histdata/mootdx") 62 | 63 | # 3.实例化策略 64 | strat = Strategy(feeds, instruments[0]) 65 | 66 | # 4.设置指标和绘图 67 | ratio = sharpe.SharpeRatio() 68 | strat.attachAnalyzer(ratio) 69 | plter = plotter.StrategyPlotter(strat) 70 | 71 | # 5.运行策略 72 | strat.run() 73 | strat.info("最终收益: %.2f" % strat.getResult()) 74 | 75 | # 6.输出夏普率、绘图 76 | strat.info("夏普比率: " + str(ratio.getSharpeRatio(0))) 77 | plter.plot() 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /samples/sample_quandl.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from mooquant import plotter, strategy 4 | from mooquant.feed import csvfeed 5 | from mooquant.tools import quandl 6 | 7 | 8 | class MyStrategy(strategy.BacktestingStrategy): 9 | def __init__(self, feed, quandlFeed, instrument): 10 | strategy.BacktestingStrategy.__init__(self, feed) 11 | self.setUseAdjustedValues(True) 12 | self.__instrument = instrument 13 | 14 | # It is VERY important to add the the extra feed to the event dispatch loop before 15 | # running the strategy. 16 | self.getDispatcher().addSubject(quandlFeed) 17 | 18 | # Subscribe to events from the Quandl feed. 19 | quandlFeed.getNewValuesEvent().subscribe(self.onQuandlData) 20 | 21 | def onQuandlData(self, dateTime, values): 22 | self.info(values) 23 | 24 | def onBars(self, bars): 25 | self.info(bars[self.__instrument].getAdjClose()) 26 | 27 | 28 | def main(): 29 | instruments = ["GORO"] 30 | 31 | # Download GORO bars using WIKI source code. 32 | feed = quandl.build_feed("WIKI", instruments, 2006, 2012, "./histdata") 33 | quandlFeed = csvfeed.Feed("Date", "%Y-%m-%d") 34 | quandlFeed.setDateRange(datetime.datetime(2006, 1, 1), datetime.datetime(2012, 12, 31)) 35 | quandlFeed.addValuesFromCSV("histdata/quandl_gold_2.csv") 36 | 37 | strat = MyStrategy(feed, quandlFeed, instruments[0]) 38 | plter = plotter.StrategyPlotter(strat, True, False, False) 39 | 40 | plter.getOrCreateSubplot("quandl").addDataSeries("USD", quandlFeed["USD"]) 41 | plter.getOrCreateSubplot("quandl").addDataSeries("EUR", quandlFeed["EUR"]) 42 | plter.getOrCreateSubplot("quandl").addDataSeries("GBP", quandlFeed["GBP"]) 43 | 44 | strat.run() 45 | plter.plot() 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /samples/sample_tushare.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from mooquant import plotter, strategy 4 | from mooquant.analyzer import sharpe 5 | from mooquant.technical import ma 6 | from mooquant.tools import tushare 7 | 8 | 9 | class Strategy(strategy.BacktestingStrategy): 10 | def __init__(self, feed, instrument): 11 | super(Strategy, self).__init__(feed) 12 | 13 | self.__position = None 14 | self.__sma = ma.SMA(feed[instrument].getCloseDataSeries(), 150) 15 | self.__instrument = instrument 16 | self.getBroker() 17 | 18 | def onEnterOk(self, position): 19 | execInfo = position.getEntryOrder().getExecutionInfo() 20 | self.info("买入 %.2f" % (execInfo.getPrice())) 21 | 22 | def onEnterCanceled(self, position): 23 | self.__position = None 24 | 25 | def onExitOk(self, position): 26 | execInfo = position.getExitOrder().getExecutionInfo() 27 | self.info("卖出 %.2f" % (execInfo.getPrice())) 28 | self.__position = None 29 | 30 | def onExitCanceled(self, position): 31 | # If the exit was canceled, re-submit it. 32 | self.__position.exitMarket() 33 | 34 | def getSMA(self): 35 | return self.__sma 36 | 37 | def onBars(self, bars): 38 | # 每一个数据都会抵达这里,就像becktest中的next 39 | # Wait for enough bars to be available to calculate a SMA. 40 | if self.__sma[-1] is None: 41 | return 42 | 43 | # bar.getTyoicalPrice = (bar.getHigh() + bar.getLow() + bar.getClose())/ 3.0 44 | bar = bars[self.__instrument] 45 | 46 | # If a position was not opened, check if we should enter a long position. 47 | if self.__position is None: 48 | if bar.getPrice() > self.__sma[-1]: 49 | # 开多头. 50 | self.__position = self.enterLong(self.__instrument, 100, True) 51 | 52 | # 平掉多头头寸. 53 | elif bar.getPrice() < self.__sma[-1] and not self.__position.exitActive(): 54 | self.__position.exitMarket() 55 | 56 | 57 | def main(): 58 | instruments = ["600036"] 59 | 60 | feeds = tushare.build_feed(instruments, 2003, 2018, "histdata/tushare") 61 | 62 | # 3.实例化策略 63 | strat = Strategy(feeds, instruments[0]) 64 | 65 | # 4.设置指标和绘图 66 | ratio = sharpe.SharpeRatio() 67 | strat.attachAnalyzer(ratio) 68 | plter = plotter.StrategyPlotter(strat) 69 | 70 | # 5.运行策略 71 | strat.run() 72 | strat.info("最终收益: %.2f" % strat.getResult()) 73 | 74 | # 6.输出夏普率、绘图 75 | strat.info("夏普比率: " + str(ratio.getSharpeRatio(0))) 76 | plter.plot() 77 | 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /samples/sma_crossover.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.technical import cross, ma 3 | 4 | 5 | class SMACrossOver(strategy.BacktestingStrategy): 6 | def __init__(self, feed, instrument, smaPeriod): 7 | super().__init__(feed) 8 | 9 | self.__instrument = instrument 10 | self.__position = None 11 | 12 | # We'll use adjusted close values instead of regular close values. 13 | self.setUseAdjustedValues(True) 14 | self.__prices = feed[instrument].getPriceDataSeries() 15 | self.__sma = ma.SMA(self.__prices, smaPeriod) 16 | 17 | def getSMA(self): 18 | return self.__sma 19 | 20 | def onEnterCanceled(self, position): 21 | self.__position = None 22 | 23 | def onExitOk(self, position): 24 | self.__position = None 25 | 26 | def onExitCanceled(self, position): 27 | # If the exit was canceled, re-submit it. 28 | self.__position.exitMarket() 29 | 30 | def onBars(self, bars): 31 | # If a position was not opened, check if we should enter a long position. 32 | if self.__position is None: 33 | if cross.cross_above(self.__prices, self.__sma) > 0: 34 | shares = int(self.getBroker().getCash() * 0.9 / bars[self.__instrument].getPrice()) 35 | # Enter a buy market order. The order is good till canceled. 36 | self.__position = self.enterLong(self.__instrument, shares, True) 37 | # Check if we have to exit the position. 38 | elif not self.__position.exitActive() and cross.cross_below(self.__prices, self.__sma) > 0: 39 | self.__position.exitMarket() 40 | -------------------------------------------------------------------------------- /samples/sma_crossover_sample.py: -------------------------------------------------------------------------------- 1 | from mooquant import plotter 2 | from mooquant.analyzer import sharpe 3 | from mooquant.tools import tushare 4 | 5 | from samples import sma_crossover 6 | 7 | 8 | def main(plot): 9 | instrument = "600016" 10 | smaPeriod = 163 11 | 12 | feed = tushare.build_feed([instrument], 2011, 2012, "./histdata/tushare") 13 | strat = sma_crossover.SMACrossOver(feed, instrument, smaPeriod) 14 | 15 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 16 | strat.attachAnalyzer(sharpeRatioAnalyzer) 17 | 18 | if plot: 19 | plt = plotter.StrategyPlotter(strat, True, False, True) 20 | plt.getInstrumentSubplot(instrument).addDataSeries("sma", strat.getSMA()) 21 | 22 | strat.run() 23 | print("Sharpe ratio: %.2f" % sharpeRatioAnalyzer.getSharpeRatio(0.05)) 24 | 25 | if plot: 26 | plt.plot() 27 | 28 | 29 | if __name__ == "__main__": 30 | main(True) 31 | -------------------------------------------------------------------------------- /samples/technical-1.py: -------------------------------------------------------------------------------- 1 | from mooquant import dataseries, technical 2 | 3 | 4 | # An EventWindow is responsible for making calculations using a window of values. 5 | class Accumulator(technical.EventWindow): 6 | def getValue(self): 7 | ret = None 8 | 9 | if self.windowFull(): 10 | ret = self.getValues().sum() 11 | 12 | return ret 13 | 14 | 15 | if __name__ == '__main__': 16 | # Build a sequence based DataSeries. 17 | seqDS = dataseries.SequenceDataSeries() 18 | # Wrap it with a filter that will get fed as new values get added to the underlying DataSeries. 19 | accum = technical.EventBasedFilter(seqDS, Accumulator(3)) 20 | 21 | # Put in some values. 22 | for i in range(0, 50): 23 | seqDS.append(i) 24 | 25 | # Get some values. 26 | print(accum[0]) # Not enough values yet. 27 | print(accum[1]) # Not enough values yet. 28 | print(accum[2]) # Ok, now we should have at least 3 values. 29 | print(accum[3]) 30 | 31 | # Get the last value, which should be equal to 49 + 48 + 47. 32 | print(accum[-1]) 33 | -------------------------------------------------------------------------------- /samples/tutorial-1.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.tools import tushare 3 | 4 | 5 | class MyStrategy(strategy.BacktestingStrategy): 6 | def __init__(self, feed, instrument): 7 | super().__init__(feed) 8 | self.__instrument = instrument 9 | 10 | def onBars(self, bars): 11 | bar = bars[self.__instrument] 12 | self.info(bar.getClose()) 13 | 14 | 15 | if __name__ == '__main__': 16 | instrument = '600036' 17 | feeds = tushare.build_feed([instrument], 2016, 2018, './histdata/tushare') 18 | 19 | # Evaluate the strategy with the feed's bars. 20 | strat = MyStrategy(feeds, instrument) 21 | strat.run() 22 | -------------------------------------------------------------------------------- /samples/tutorial-2.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.technical import ma 3 | from mooquant.tools import tushare 4 | 5 | 6 | class MyStrategy(strategy.BacktestingStrategy): 7 | def __init__(self, feed, instrument): 8 | super().__init__(feed) 9 | # We want a 15 period SMA over the closing prices. 10 | self.__sma = ma.SMA(feed[instrument].getCloseDataSeries(), 15) 11 | self.__instrument = instrument 12 | 13 | def onBars(self, bars): 14 | bar = bars[self.__instrument] 15 | self.info("%s %s" % (bar.getClose(), self.__sma[-1])) 16 | 17 | 18 | if __name__ == '__main__': 19 | 20 | instrument = '600016' 21 | feeds = tushare.build_feed([instrument], 2016, 2018, './histdata/tushare') 22 | 23 | # Evaluate the strategy with the feed's bars. 24 | strat = MyStrategy(feeds, instrument) 25 | strat.run() 26 | -------------------------------------------------------------------------------- /samples/tutorial-3.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.technical import ma, rsi 3 | from mooquant.tools import tushare 4 | 5 | 6 | class MyStrategy(strategy.BacktestingStrategy): 7 | def __init__(self, feed, instrument): 8 | super(MyStrategy, self).__init__(feed) 9 | self.__rsi = rsi.RSI(feed[instrument].getCloseDataSeries(), 14) 10 | self.__sma = ma.SMA(self.__rsi, 15) 11 | self.__instrument = instrument 12 | 13 | def onBars(self, bars): 14 | bar = bars[self.__instrument] 15 | self.info("%s %s %s" % (bar.getClose(), self.__rsi[-1], self.__sma[-1])) 16 | 17 | 18 | if __name__ == '__main__': 19 | 20 | instrument = '600016' 21 | feeds = tushare.build_feed([instrument], 2016, 2018, './histdata/tushare') 22 | 23 | # Evaluate the strategy with the feed's bars. 24 | strat = MyStrategy(feeds, instrument) 25 | strat.run() 26 | -------------------------------------------------------------------------------- /samples/tutorial-4.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.technical import ma 3 | from mooquant.tools import tushare 4 | 5 | 6 | class MyStrategy(strategy.BacktestingStrategy): 7 | def __init__(self, feed, instrument, smaPeriod): 8 | super(MyStrategy, self).__init__(feed, 1000) 9 | self.__position = None 10 | self.__instrument = instrument 11 | # We'll use adjusted close values instead of regular close values. 12 | self.setUseAdjustedValues(True) 13 | self.__sma = ma.SMA(feed[instrument].getPriceDataSeries(), smaPeriod) 14 | 15 | def onEnterOk(self, position): 16 | execInfo = position.getEntryOrder().getExecutionInfo() 17 | self.info("BUY at $%.2f" % (execInfo.getPrice())) 18 | 19 | def onEnterCanceled(self, position): 20 | self.__position = None 21 | 22 | def onExitOk(self, position): 23 | execInfo = position.getExitOrder().getExecutionInfo() 24 | self.info("SELL at $%.2f" % (execInfo.getPrice())) 25 | self.__position = None 26 | 27 | def onExitCanceled(self, position): 28 | # If the exit was canceled, re-submit it. 29 | self.__position.exitMarket() 30 | 31 | def onBars(self, bars): 32 | # Wait for enough bars to be available to calculate a SMA. 33 | if self.__sma[-1] is None: 34 | return 35 | 36 | bar = bars[self.__instrument] 37 | # If a position was not opened, check if we should enter a long position. 38 | if self.__position is None: 39 | if bar.getPrice() > self.__sma[-1]: 40 | # Enter a buy market order for 10 shares. The order is good till canceled. 41 | self.__position = self.enterLong(self.__instrument, 10, True) 42 | # Check if we have to exit the position. 43 | elif bar.getPrice() < self.__sma[-1] and not self.__position.exitActive(): 44 | self.__position.exitMarket() 45 | 46 | 47 | def run_strategy(smaPeriod): 48 | instrument = '600016' 49 | feeds = tushare.build_feed([instrument], 2016, 2018, './histdata/tushare') 50 | 51 | # Evaluate the strategy with the feed. 52 | strat = MyStrategy(feeds, instrument, smaPeriod) 53 | strat.run() 54 | strat.info("Final portfolio value: $%.2f" % strat.getBroker().getEquity()) 55 | 56 | 57 | if __name__ == '__main__': 58 | run_strategy(15) 59 | -------------------------------------------------------------------------------- /samples/tutorial-5.py: -------------------------------------------------------------------------------- 1 | from mooquant import plotter 2 | from mooquant.analyzer import returns 3 | from mooquant.tools import tushare 4 | 5 | from samples import sma_crossover 6 | 7 | if __name__ == '__main__': 8 | instrument = '600016' 9 | 10 | feeds = tushare.build_feed([instrument], 2016, 2018, './histdata/tushare') 11 | strat = sma_crossover.SMACrossOver(feeds, instrument, 20) 12 | 13 | # Attach a returns analyzers to the strategy. 14 | returnsAnalyzer = returns.Returns() 15 | strat.attachAnalyzer(returnsAnalyzer) 16 | 17 | # Attach the plotter to the strategy. 18 | plt = plotter.StrategyPlotter(strat) 19 | # Include the SMA in the instrument's subplot to get it displayed along with the closing prices. 20 | plt.getInstrumentSubplot("orcl").addDataSeries("SMA", strat.getSMA()) 21 | # Plot the simple returns on each bar. 22 | plt.getOrCreateSubplot("returns").addDataSeries("Simple returns", returnsAnalyzer.getReturns()) 23 | 24 | # Run the strategy. 25 | strat.run() 26 | strat.info("Final portfolio value: $%.2f" % strat.getResult()) 27 | 28 | # Plot the strategy. 29 | plt.plot() 30 | -------------------------------------------------------------------------------- /samples/tutorial-optimizer-local.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from mooquant.optimizer import local 4 | from mooquant.tools import tushare 5 | 6 | from rsi2 import RSI2 7 | 8 | 9 | def parameters_generator(instrument): 10 | entrySMA = list(range(150, 251)) 11 | exitSMA = list(range(5, 16)) 12 | rsiPeriod = list(range(2, 11)) 13 | 14 | overBoughtThreshold = list(range(75, 96)) 15 | overSoldThreshold = list(range(5, 26)) 16 | 17 | return itertools.product(instrument, entrySMA, exitSMA, rsiPeriod, overBoughtThreshold, overSoldThreshold) 18 | 19 | 20 | if __name__ == '__main__': 21 | instruments = ['000848'] 22 | 23 | feeds = tushare.build_feed(instruments, 2016, 2018, 'histdata/tushare') 24 | local.run(RSI2, feeds, parameters_generator(instruments)) 25 | -------------------------------------------------------------------------------- /samples/tutorial-optimizer-server.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from mooquant.optimizer import server 4 | from mooquant.tools import tushare 5 | 6 | 7 | def parameters_generator(instrument): 8 | instrument = [instrument] 9 | entrySMA = list(range(150, 251)) 10 | exitSMA = list(range(5, 16)) 11 | rsiPeriod = list(range(2, 11)) 12 | overBoughtThreshold = list(range(75, 96)) 13 | overSoldThreshold = list(range(5, 26)) 14 | 15 | return itertools.product(instrument, entrySMA, exitSMA, rsiPeriod, overBoughtThreshold, overSoldThreshold) 16 | 17 | 18 | # The if __name__ == '__main__' part is necessary if running on Windows. 19 | if __name__ == '__main__': 20 | # Load the feed from the CSV files. 21 | # feeds = yahoofeed.Feed() 22 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2009-yahoofinance.csv") 23 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2010-yahoofinance.csv") 24 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2011-yahoofinance.csv") 25 | 26 | instrument = '600016' 27 | feeds = tushare.build_feed([instrument], 2016, 2018, './histdata/tushare') 28 | 29 | # Run the server. 30 | server.serve( 31 | strategyParameters=parameters_generator(instrument), 32 | address="0.0.0.0", 33 | barFeed=feeds, 34 | drivce='xml', 35 | port=5000 36 | ) 37 | -------------------------------------------------------------------------------- /samples/tutorial-optimizer-worker.py: -------------------------------------------------------------------------------- 1 | from mooquant.optimizer import worker 2 | 3 | from rsi2 import RSI2 4 | 5 | # The if __name__ == '__main__' part is necessary if running on Windows. 6 | if __name__ == '__main__': 7 | worker.run( 8 | strategyClass=RSI2, 9 | workerName="worker", 10 | address="localhost", 11 | drivce='xml', 12 | port=5000 13 | ) 14 | -------------------------------------------------------------------------------- /samples/tutorial-zerorpc-server.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from mooquant.optimizer import server 4 | from mooquant.tools import mootdx 5 | 6 | 7 | def parameters_generator(instrument): 8 | instrument = [instrument] 9 | entrySMA = list(range(150, 251)) 10 | exitSMA = list(range(5, 16)) 11 | rsiPeriod = list(range(2, 11)) 12 | overBoughtThreshold = list(range(75, 96)) 13 | overSoldThreshold = list(range(5, 26)) 14 | return itertools.product(instrument, entrySMA, exitSMA, rsiPeriod, overBoughtThreshold, overSoldThreshold) 15 | 16 | 17 | # The if __name__ == '__main__' part is necessary if running on Windows. 18 | if __name__ == '__main__': 19 | # Load the feed from the CSV files. 20 | # feeds = yahoofeed.Feed() 21 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2009-yahoofinance.csv") 22 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2010-yahoofinance.csv") 23 | # feeds.addBarsFromCSV("dia", "./tests/data/DIA-2011-yahoofinance.csv") 24 | 25 | instrument = '600036' 26 | feeds = mootdx.build_feed([instrument], 2003, 2018, './histdata/mootdx') 27 | 28 | # Run the server. 29 | server.serve( 30 | strategyParameters=parameters_generator(instrument), 31 | address="0.0.0.0", 32 | barFeed=feeds, 33 | drivce='zmq', 34 | port=5000 35 | ) 36 | -------------------------------------------------------------------------------- /samples/tutorial-zerorpc-worker.py: -------------------------------------------------------------------------------- 1 | from mooquant.optimizer import worker 2 | 3 | from rsi2 import RSI2 4 | 5 | if __name__ == '__main__': 6 | worker.run( 7 | strategyClass=RSI2, 8 | workerName="worker", 9 | address="127.0.0.1", 10 | drivce='zmq', 11 | port=5000 12 | ) 13 | -------------------------------------------------------------------------------- /samples/tutorial_bitstamp_1.py: -------------------------------------------------------------------------------- 1 | from mooquant import strategy 2 | from mooquant.provider.bitstamp import barfeed, broker 3 | from mooquant.technical import cross, ma 4 | 5 | 6 | class MyStrategy(strategy.BaseStrategy): 7 | def __init__(self, feed, brk, smaPeriod=20): 8 | super().__init__(feed, brk) 9 | 10 | self.__instrument = "BTC" 11 | self.__prices = feed[self.__instrument].getCloseDataSeries() 12 | self.__sma = ma.SMA(self.__prices, smaPeriod) 13 | 14 | self.__bid = None 15 | self.__ask = None 16 | 17 | self.__position = None 18 | self.__posSize = 0.05 19 | 20 | # Subscribe to order book update events to get bid/ask prices to trade. 21 | feed.getOrderBookUpdateEvent().subscribe(self.__onOrderBookUpdate) 22 | 23 | def __onOrderBookUpdate(self, orderBookUpdate): 24 | bid = orderBookUpdate.getBidPrices()[0] 25 | ask = orderBookUpdate.getAskPrices()[0] 26 | 27 | if bid != self.__bid or ask != self.__ask: 28 | self.__bid = bid 29 | self.__ask = ask 30 | self.info("Order book updated. Best bid: %s. Best ask: %s" % (self.__bid, self.__ask)) 31 | 32 | def onEnterOk(self, position): 33 | self.info("Position opened at %s" % (position.getEntryOrder().getExecutionInfo().getPrice())) 34 | 35 | def onEnterCanceled(self, position): 36 | self.info("Position entry canceled") 37 | self.__position = None 38 | 39 | def onExitOk(self, position): 40 | self.__position = None 41 | self.info("Position closed at %s" % (position.getExitOrder().getExecutionInfo().getPrice())) 42 | 43 | def onExitCanceled(self, position): 44 | # If the exit was canceled, re-submit it. 45 | self.__position.exitLimit(self.__bid) 46 | 47 | def onBars(self, bars): 48 | bar = bars[self.__instrument] 49 | self.info("Price: %s. Volume: %s." % (bar.getClose(), bar.getVolume())) 50 | 51 | # Wait until we get the current bid/ask prices. 52 | if self.__ask is None: 53 | return 54 | 55 | # If a position was not opened, check if we should enter a long position. 56 | if self.__position is None: 57 | if cross.cross_above(self.__prices, self.__sma) > 0: 58 | self.info("Entry signal. Buy at %s" % (self.__ask)) 59 | self.__position = self.enterLongLimit(self.__instrument, self.__ask, self.__posSize, True) 60 | # Check if we have to close the position. 61 | elif not self.__position.exitActive() and cross.cross_below(self.__prices, self.__sma) > 0: 62 | self.info("Exit signal. Sell at %s" % (self.__bid)) 63 | self.__position.exitLimit(self.__bid) 64 | 65 | 66 | def main(): 67 | feeds = barfeed.LiveTradeFeed() 68 | brk = broker.PaperTradingBroker(1000, feeds) 69 | 70 | strat = MyStrategy(feeds, brk, smaPeriod=20) 71 | strat.run() 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /samples/vwap_momentum.py: -------------------------------------------------------------------------------- 1 | from mooquant import plotter, strategy 2 | from mooquant.analyzer import sharpe 3 | from mooquant.technical import vwap 4 | from mooquant.tools import tushare 5 | 6 | 7 | class VWAPMomentum(strategy.BacktestingStrategy): 8 | def __init__(self, feed, instrument, vwapWindowSize, threshold): 9 | super().__init__(feed) 10 | 11 | self.__instrument = instrument 12 | self.__vwap = vwap.VWAP(feed[instrument], vwapWindowSize) 13 | self.__threshold = threshold 14 | 15 | def getVWAP(self): 16 | return self.__vwap 17 | 18 | def onBars(self, bars): 19 | vwap = self.__vwap[-1] 20 | 21 | if vwap is None: 22 | return 23 | 24 | shares = self.getBroker().getShares(self.__instrument) 25 | price = bars[self.__instrument].getClose() 26 | notional = shares * price 27 | 28 | if price > vwap * (1 + self.__threshold) and notional < 1000000: 29 | self.marketOrder(self.__instrument, 100) 30 | elif price < vwap * (1 - self.__threshold) and notional > 0: 31 | self.marketOrder(self.__instrument, -100) 32 | 33 | 34 | def main(plot): 35 | instrument = '600016' 36 | vwapWindowSize = 5 37 | threshold = 0.01 38 | 39 | feeds = tushare.build_feed([instrument], 2010, 2018, './histdata/tushare') 40 | strat = VWAPMomentum(feeds, instrument, vwapWindowSize, threshold) 41 | 42 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 43 | strat.attachAnalyzer(sharpeRatioAnalyzer) 44 | 45 | plter = plotter.StrategyPlotter(strat, True, False, True) 46 | plter.getInstrumentSubplot(instrument).addDataSeries("vwap", strat.getVWAP()) 47 | 48 | strat.run() 49 | 50 | strat.info("Sharpe ratio: %.2f" % sharpeRatioAnalyzer.getSharpeRatio(0.05)) 51 | plter.plot() 52 | 53 | 54 | if __name__ == "__main__": 55 | main(True) 56 | -------------------------------------------------------------------------------- /sandbox/bbands.py: -------------------------------------------------------------------------------- 1 | from mooquant import plotter, strategy 2 | from mooquant.analyzer import sharpe 3 | from mooquant.technical import bollinger 4 | from mooquant.tools import tushare 5 | 6 | 7 | class BBandsStrategy(strategy.BacktestingStrategy): 8 | def __init__(self, feed, instrument, bBandsPeriod): 9 | super().__init__(feed) 10 | 11 | self.__instrument = instrument 12 | self.__bbands = bollinger.BollingerBands(feed[instrument].getCloseDataSeries(), bBandsPeriod, 2) 13 | 14 | def getBollingerBands(self): 15 | return self.__bbands 16 | 17 | def onBars(self, bars): 18 | lower = self.__bbands.getLowerBand()[-1] 19 | upper = self.__bbands.getUpperBand()[-1] 20 | 21 | if lower is None: 22 | return 23 | 24 | shares = self.getBroker().getShares(self.__instrument) 25 | bar = bars[self.__instrument] 26 | 27 | if shares == 0 and bar.getClose() < lower: 28 | sharesToBuy = int(self.getBroker().getCash(False) / bar.getClose()) 29 | self.marketOrder(self.__instrument, sharesToBuy) 30 | elif shares > 0 and bar.getClose() > upper: 31 | self.marketOrder(self.__instrument, -1 * shares) 32 | 33 | 34 | def main(plot=False): 35 | instruments = ["600036"] 36 | bBandsPeriod = 20 37 | 38 | feeds = tushare.build_feed(instruments, 2017, 2018, "./tests/data") 39 | strat = BBandsStrategy(feeds, instruments[0], bBandsPeriod) 40 | sharp = sharpe.SharpeRatio() 41 | strat.attachAnalyzer(sharp) 42 | 43 | if plot: 44 | plt = plotter.StrategyPlotter(strat, True, True, True) 45 | plt.getInstrumentSubplot(instruments[0]).addDataSeries("upper", strat.getBollingerBands().getUpperBand()) 46 | plt.getInstrumentSubplot(instruments[0]).addDataSeries("middle", strat.getBollingerBands().getMiddleBand()) 47 | plt.getInstrumentSubplot(instruments[0]).addDataSeries("lower", strat.getBollingerBands().getLowerBand()) 48 | 49 | strat.run() 50 | 51 | print("Sharpe ratio: %.2f" % sharp.getSharpeRatio(0.05)) 52 | 53 | if plot: 54 | plt.plot() 55 | 56 | 57 | if __name__ == "__main__": 58 | main(True) 59 | -------------------------------------------------------------------------------- /sandbox/bccharts_example_1.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from mooquant import bar 4 | from mooquant.provider.bitcoincharts import barfeed 5 | from mooquant.tools import resample 6 | 7 | 8 | def main(): 9 | barFeed = barfeed.CSVTradeFeed() 10 | barFeed.addBarsFromCSV("./tests/data/bitstampUSD.csv", fromDateTime=datetime.datetime(2014, 1, 1)) 11 | resample.resample_to_csv(barFeed, bar.Frequency.MINUTE * 30, "30min-bitstampUSD.csv") 12 | 13 | 14 | if __name__ == "__main__": 15 | main() 16 | -------------------------------------------------------------------------------- /sandbox/vwap_momentum.py: -------------------------------------------------------------------------------- 1 | from mooquant import plotter, strategy 2 | from mooquant.analyzer import sharpe 3 | from mooquant.technical import vwap 4 | from mooquant.tools import tushare 5 | 6 | 7 | class VWAPMomentum(strategy.BacktestingStrategy): 8 | def __init__(self, feed, instrument, vwapWindowSize, threshold): 9 | super(VWAPMomentum, self).__init__(feed) 10 | self.__instrument = instrument 11 | self.__vwap = vwap.VWAP(feed[instrument], vwapWindowSize) 12 | self.__threshold = threshold 13 | 14 | def getVWAP(self): 15 | return self.__vwap 16 | 17 | def onBars(self, bars): 18 | vwap = self.__vwap[-1] 19 | if vwap is None: 20 | return 21 | 22 | shares = self.getBroker().getShares(self.__instrument) 23 | price = bars[self.__instrument].getClose() 24 | notional = shares * price 25 | 26 | if price > vwap * (1 + self.__threshold) and notional < 1000000: 27 | self.marketOrder(self.__instrument, 100) 28 | elif price < vwap * (1 - self.__threshold) and notional > 0: 29 | self.marketOrder(self.__instrument, -100) 30 | 31 | 32 | def main(plot): 33 | instrument = "600036" 34 | vwapWindowSize = 5 35 | threshold = 0.01 36 | 37 | # Download the bars. 38 | feed = tushare.build_feed([instrument], 2011, 2012, "./tests/data") 39 | 40 | strat = VWAPMomentum(feed, instrument, vwapWindowSize, threshold) 41 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 42 | strat.attachAnalyzer(sharpeRatioAnalyzer) 43 | 44 | if plot: 45 | plt = plotter.StrategyPlotter(strat, True, False, True) 46 | plt.getInstrumentSubplot(instrument).addDataSeries("vwap", strat.getVWAP()) 47 | 48 | strat.run() 49 | print("Sharpe ratio: %.2f" % sharpeRatioAnalyzer.getSharpeRatio(0.05)) 50 | 51 | if plot: 52 | plt.plot() 53 | 54 | 55 | if __name__ == "__main__": 56 | main(True) 57 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=2 3 | detailed-errors=1 4 | nocapture=1 5 | 6 | # Code coverage settings using nose-cov instead of the builtin plugin because: 7 | # 1: It tracks down subprocesses 8 | # 2: It supports coverage config files 9 | 10 | # cov=mooquant 11 | # with-cov=1 12 | # cov-report=html 13 | # cov-config=coverage.cfg 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # MooQuant 4 | # 5 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | try: 20 | from setuptools import setup, find_packages 21 | except ImportError: 22 | from distutils.core import setup, find_packages 23 | 24 | with open('README.rst', encoding='utf-8') as f: 25 | description = f.read() 26 | 27 | requirements = [ 28 | 'click>=6.0', 29 | "python-dateutil", 30 | "requests", 31 | "numpy", 32 | "pytz", 33 | ] 34 | 35 | setup_requirements = [ 36 | 'pytest-runner', 37 | ] 38 | 39 | test_requirements = [ 40 | 'pytest', 41 | ] 42 | 43 | from mooquant import __version__ 44 | 45 | setup( 46 | name='MooQuant', 47 | version=__version__, 48 | description='MooQuant', 49 | long_description=description, 50 | author='bopowang', 51 | author_email='ibopo@126.com', 52 | url='http://www.mooquant.com/', 53 | download_url='https://github.com/bopo/mooquant/archive/master.zip', 54 | packages=find_packages(include=['mooquant', 'mooquant.*']), 55 | extras_require={ 56 | 'Scipy': ["scipy"], 57 | 'TALib': ["Cython", "TA-Lib"], 58 | 'Plotting': ["matplotlib"], 59 | 'Bitstamp': ["ws4py>=0.3.4", "tornado==4.5.3"], 60 | }, 61 | install_requires=requirements, 62 | license="MIT license", 63 | zip_safe=False, 64 | keywords='mooquant', 65 | classifiers=[ 66 | 'Development Status :: 2 - Pre-Alpha', 67 | 'Intended Audience :: Developers', 68 | 'License :: OSI Approved :: MIT License', 69 | 'Natural Language :: English', 70 | 'Programming Language :: Python :: 3', 71 | 'Programming Language :: Python :: 3.3', 72 | 'Programming Language :: Python :: 3.4', 73 | 'Programming Language :: Python :: 3.5', 74 | 'Programming Language :: Python :: 3.6', 75 | 'Programming Language :: Python :: 3.7', 76 | ], 77 | test_suite='tests', 78 | tests_require=test_requirements, 79 | setup_requires=setup_requirements, 80 | ) 81 | -------------------------------------------------------------------------------- /stratlib/sample_orders.py: -------------------------------------------------------------------------------- 1 | from mooquant import bar, plotter, strategy 2 | from mooquant.analyzer import drawdown, returns, sharpe, trades 3 | from mooquant.broker.backtesting import TradePercentage 4 | from mooquant.broker.fillstrategy import DefaultStrategy 5 | from mooquant.technical import ma 6 | 7 | 8 | class OrderBook(strategy.BacktestingStrategy): 9 | def __init__(self, feed, instrument, n, m): 10 | strategy.BacktestingStrategy.__init__(self, feed) 11 | self.__instrument = instrument 12 | 13 | self.getBroker().setFillStrategy(DefaultStrategy(None)) 14 | self.getBroker().setCommission(TradePercentage(0.001)) 15 | 16 | self.__position = None 17 | self.__prices = feed[instrument].getPriceDataSeries() 18 | self.__malength1 = int(n) 19 | self.__malength2 = int(m) 20 | 21 | self.__ma1 = ma.SMA(self.__prices, self.__malength1) 22 | self.__ma2 = ma.SMA(self.__prices, self.__malength2) 23 | 24 | def getPrice(self): 25 | return self.__prices 26 | 27 | def getSMA(self): 28 | return self.__ma1, self.__ma2 29 | 30 | def onEnterCanceled(self, position): 31 | self.__position = None 32 | 33 | def onEnterOK(self): 34 | pass 35 | 36 | def onExitOk(self, position): 37 | self.__position = None 38 | # self.info("long close") 39 | 40 | def onExitCanceled(self, position): 41 | self.__position.exitMarket() 42 | 43 | def onBars(self, bars): 44 | bar = bars[self.__instrument] 45 | print(bar.getDateTime(), bar.getBp(), bar.getAp()) 46 | 47 | 48 | def testStrategy(): 49 | instrument = '600288' 50 | frequency = bar.Frequency.TRADE 51 | fromDate = '20160815' 52 | toDate = '20160820' 53 | strat = OrderBook 54 | 55 | paras = [5, 20] 56 | plot = True 57 | 58 | from mooquant.tools import tushare 59 | feeds = tushare.build_feed([instrument], 2016, 2017, "histdata/tushare") 60 | strat = strat(feeds, instrument, *paras) 61 | 62 | retAnalyzer = returns.Returns() 63 | strat.attachAnalyzer(retAnalyzer) 64 | 65 | sharpeRatioAnalyzer = sharpe.SharpeRatio() 66 | strat.attachAnalyzer(sharpeRatioAnalyzer) 67 | 68 | drawDownAnalyzer = drawdown.DrawDown() 69 | strat.attachAnalyzer(drawDownAnalyzer) 70 | 71 | tradesAnalyzer = trades.Trades() 72 | strat.attachAnalyzer(tradesAnalyzer) 73 | 74 | if plot: 75 | plt = plotter.StrategyPlotter(strat, True, True, True) 76 | 77 | strat.run() 78 | 79 | if plot: 80 | plt.plot() 81 | 82 | # 夏普率 83 | sharp = sharpeRatioAnalyzer.getSharpeRatio(0.05) 84 | 85 | # 最大回撤 86 | maxdd = drawDownAnalyzer.getMaxDrawDown() 87 | 88 | # 收益率 89 | return_ = retAnalyzer.getCumulativeReturns()[-1] 90 | 91 | # 收益曲线 92 | return_list = [] 93 | 94 | for item in retAnalyzer.getCumulativeReturns(): 95 | return_list.append(item) 96 | 97 | 98 | if __name__ == "__main__": 99 | testStrategy() 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | -------------------------------------------------------------------------------- /tests/data/bccharts_example_2.output: -------------------------------------------------------------------------------- 1 | 2014-06-26 23:00:00 strategy [INFO] Buy 1.69 at 580.0 2 | 2014-06-26 23:00:00 strategy [INFO] Buy order 1 updated - Status: SUBMITTED - None 3 | 2014-06-26 23:30:00 strategy [INFO] Buy order 1 updated - Status: ACCEPTED - None 4 | 2014-06-26 23:30:00 strategy [INFO] Buy order 1 updated - Status: FILLED - 2014-06-26 23:30:00 - Price: 580.0 - Amount: 1.69 - Fee: 2.4505 5 | 2014-06-26 23:30:00 strategy [INFO] Buy 0.029 at 579.99 6 | 2014-06-26 23:30:00 strategy [INFO] Buy order 2 updated - Status: SUBMITTED - None 7 | 2014-06-27 00:00:00 strategy [INFO] Buy order 2 updated - Status: ACCEPTED - None 8 | 2014-06-27 00:00:00 strategy [INFO] Buy order 2 updated - Status: FILLED - 2014-06-27 00:00:00 - Price: 579.99 - Amount: 0.029 - Fee: 0.042049275 9 | 2014-07-03 18:30:00 strategy [INFO] Sell 1.719 at 639.0 10 | 2014-07-03 18:30:00 strategy [INFO] Sell order 3 updated - Status: SUBMITTED - None 11 | 2014-07-03 19:00:00 strategy [INFO] Sell order 3 updated - Status: ACCEPTED - None 12 | 2014-07-03 19:00:00 strategy [INFO] Sell order 3 updated - Status: FILLED - 2014-07-03 19:00:00 - Price: 639.0 - Amount: 1.719 - Fee: 2.7461025 13 | 2014-07-11 19:30:00 strategy [INFO] Buy 1.692 at 634.91 14 | 2014-07-11 19:30:00 strategy [INFO] Buy order 4 updated - Status: SUBMITTED - None 15 | 2014-07-11 20:00:00 strategy [INFO] Buy order 4 updated - Status: ACCEPTED - None 16 | 2014-07-11 20:00:00 strategy [INFO] Buy order 4 updated - Status: FILLED - 2014-07-11 20:00:00 - Price: 634.91 - Amount: 1.692 - Fee: 2.6856693 17 | 2014-07-11 20:30:00 strategy [INFO] Buy 0.03 at 634.99 18 | 2014-07-11 20:30:00 strategy [INFO] Buy order 5 updated - Status: SUBMITTED - None 19 | 2014-07-11 21:00:00 strategy [INFO] Buy order 5 updated - Status: ACCEPTED - None 20 | 2014-07-11 21:00:00 strategy [INFO] Buy order 5 updated - Status: FILLED - 2014-07-11 21:00:00 - Price: 634.99 - Amount: 0.03 - Fee: 0.04762425 21 | 2014-07-14 01:30:00 strategy [INFO] Sell 1.722 at 626.79 22 | 2014-07-14 01:30:00 strategy [INFO] Sell order 6 updated - Status: SUBMITTED - None 23 | 2014-07-14 02:00:00 strategy [INFO] Sell order 6 updated - Status: ACCEPTED - None 24 | 2014-07-14 02:00:00 strategy [INFO] Sell order 6 updated - Status: FILLED - 2014-07-14 02:00:00 - Price: 627.9 - Amount: 1.722 - Fee: 2.7031095 25 | 2014-07-31 14:00:00 strategy [INFO] Buy 1.81 at 584.13 26 | 2014-07-31 14:00:00 strategy [INFO] Buy order 7 updated - Status: SUBMITTED - None 27 | 2014-07-31 14:30:00 strategy [INFO] Buy order 7 updated - Status: ACCEPTED - None 28 | 2014-07-31 14:30:00 strategy [INFO] Buy order 7 updated - Status: FILLED - 2014-07-31 14:30:00 - Price: 584.1 - Amount: 1.81 - Fee: 2.6430525 29 | 2014-07-31 14:30:00 strategy [INFO] Buy 0.031 at 588.5 30 | 2014-07-31 14:30:00 strategy [INFO] Buy order 8 updated - Status: SUBMITTED - None 31 | 2014-07-31 15:00:00 strategy [INFO] Buy order 8 updated - Status: ACCEPTED - None 32 | 2014-07-31 15:00:00 strategy [INFO] Buy order 8 updated - Status: FILLED - 2014-07-31 15:00:00 - Price: 588.5 - Amount: 0.031 - Fee: 0.04560875 33 | 2014-08-03 00:00:00 strategy [INFO] Sell 1.841 at 587.64 34 | 2014-08-03 00:00:00 strategy [INFO] Sell order 9 updated - Status: SUBMITTED - None 35 | 2014-08-03 00:30:00 strategy [INFO] Sell order 9 updated - Status: ACCEPTED - None 36 | 2014-08-03 00:30:00 strategy [INFO] Sell order 9 updated - Status: FILLED - 2014-08-03 00:30:00 - Price: 589.59 - Amount: 1.841 - Fee: 2.713587975 37 | -------------------------------------------------------------------------------- /tests/data/multiinstrument.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bopo/mooquant/244a87d4cd8b4d918eec4f16905e0921c3b39f50/tests/data/multiinstrument.sqlite -------------------------------------------------------------------------------- /tests/data/orders.csv: -------------------------------------------------------------------------------- 1 | 2011,1,10,AAPL,Buy,1500, 2 | 2011,1,13,AAPL,Sell,1500, 3 | 2011,1,13,IBM,Buy,4000, 4 | 2011,1,26,GOOG,Buy,1000, 5 | 2011,2,2,XOM,Sell,4000, 6 | 2011,2,10,XOM,Buy,4000, 7 | 2011,3,3,GOOG,Sell,1000, 8 | 2011,3,3,IBM,Sell,2200, 9 | 2011,6,3,IBM,Sell,3300, 10 | 2011,5,3,IBM,Buy,1500, 11 | 2011,6,10,AAPL,Buy,1200, 12 | 2011,8,1,GOOG,Buy,55, 13 | 2011,8,1,GOOG,Sell,55, 14 | 2011,12,20,AAPL,Sell,1200, 15 | -------------------------------------------------------------------------------- /tests/data/plotter_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bopo/mooquant/244a87d4cd8b4d918eec4f16905e0921c3b39f50/tests/data/plotter_test.png -------------------------------------------------------------------------------- /tests/data/rsi-test.csv: -------------------------------------------------------------------------------- 1 | Input,Expected 2 | 44.3389, 3 | 44.0902, 4 | 44.1497, 5 | 43.6124, 6 | 44.3278, 7 | 44.8264, 8 | 45.0955, 9 | 45.4245, 10 | 45.8433, 11 | 46.0826, 12 | 45.8931, 13 | 46.0328, 14 | 45.6140, 15 | 46.2820, 16 | 46.2820,70.53278948 17 | 46.0028,66.31856181 18 | 46.0328,66.54982994 19 | 46.4116,69.40630534 20 | 46.2222,66.35516906 21 | 45.6439,57.97485571 22 | 46.2122,62.92960675 23 | 46.2521,63.25714756 24 | 45.7137,56.05929872 25 | 46.4515,62.37707144 26 | 45.7835,54.70757308 27 | 45.3548,50.42277441 28 | 44.0288,39.98982315 29 | 44.1783,41.46048198 30 | 44.2181,41.86891609 31 | 44.5672,45.46321245 32 | 43.4205,37.30404209 33 | 42.6628,33.07952299 34 | 43.1314,37.77295211 35 | -------------------------------------------------------------------------------- /tests/data/sc-ema-10.csv: -------------------------------------------------------------------------------- 1 | Input,Expected 2 | 22.2734, 3 | 22.1940, 4 | 22.0847, 5 | 22.1741, 6 | 22.1840, 7 | 22.1344, 8 | 22.2337, 9 | 22.4323, 10 | 22.2436, 11 | 22.2933,22.22475 12 | 22.1542,22.21192 13 | 22.3926,22.24477 14 | 22.3816,22.26965 15 | 22.6109,22.33170 16 | 23.3558,22.51790 17 | 24.0519,22.79681 18 | 23.7530,22.97066 19 | 23.8324,23.12734 20 | 23.9516,23.27721 21 | 23.6338,23.34204 22 | 23.8225,23.42940 23 | 23.8722,23.50991 24 | 23.6537,23.53605 25 | 23.1870,23.47259 26 | 23.0976,23.40441 27 | 23.3260,23.39015 28 | 22.6805,23.26112 29 | 23.0976,23.23139 30 | 22.4025,23.08068 31 | 22.1725,22.91556 32 | -------------------------------------------------------------------------------- /tests/data/sc-sma-10.csv: -------------------------------------------------------------------------------- 1 | Input,Expected 2 | 22.2734, 3 | 22.1940, 4 | 22.0847, 5 | 22.1741, 6 | 22.1840, 7 | 22.1344, 8 | 22.2337, 9 | 22.4323, 10 | 22.2436, 11 | 22.2933,22.22 12 | 22.1542,22.21 13 | 22.3926,22.23 14 | 22.3816,22.26 15 | 22.6109,22.31 16 | 23.3558,22.42 17 | 24.0519,22.61 18 | 23.7530,22.77 19 | 23.8324,22.91 20 | 23.9516,23.08 21 | 23.6338,23.21 22 | 23.8225,23.38 23 | 23.8722,23.53 24 | 23.6537,23.65 25 | 23.1870,23.71 26 | 23.0976,23.69 27 | 23.3260,23.61 28 | 22.6805,23.51 29 | 23.0976,23.43 30 | 22.4025,23.28 31 | 22.1725,23.13 32 | -------------------------------------------------------------------------------- /tests/eventprofiler_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import datetime 23 | 24 | from mooquant import eventprofiler 25 | from mooquant.barfeed import yahoofeed 26 | 27 | from . import common 28 | 29 | 30 | class Predicate(eventprofiler.Predicate): 31 | def __init__(self, eventDates): 32 | self.__dates = eventDates 33 | 34 | def eventOccurred(self, instrument, bards): 35 | ret = False 36 | if bards[-1].getDateTime().date() in self.__dates: 37 | ret = True 38 | return ret 39 | 40 | 41 | class EventProfilerTestCase(common.TestCase): 42 | def testNoEvents(self): 43 | feed = yahoofeed.Feed() 44 | feed.addBarsFromCSV("orcl", common.get_data_file_path("orcl-2000-yahoofinance.csv")) 45 | 46 | predicate = Predicate([]) 47 | eventProfiler = eventprofiler.Profiler(predicate, 5, 5) 48 | eventProfiler.run(feed, True) 49 | self.assertEqual(eventProfiler.getResults().getEventCount(), 0) 50 | 51 | def testEventsOnBoundary(self): 52 | feed = yahoofeed.Feed() 53 | feed.addBarsFromCSV("orcl", common.get_data_file_path("orcl-2000-yahoofinance.csv")) 54 | 55 | dates = [] 56 | dates.append(datetime.date(2000, 1, 3)) 57 | dates.append(datetime.date(2000, 1, 4)) 58 | dates.append(datetime.date(2000, 1, 5)) 59 | dates.append(datetime.date(2000, 1, 6)) 60 | dates.append(datetime.date(2000, 1, 7)) 61 | dates.append(datetime.date(2000, 1, 10)) 62 | dates.append(datetime.date(2000, 12, 22)) 63 | dates.append(datetime.date(2000, 12, 26)) 64 | dates.append(datetime.date(2000, 12, 27)) 65 | dates.append(datetime.date(2000, 12, 28)) 66 | dates.append(datetime.date(2000, 12, 29)) 67 | 68 | predicate = Predicate(dates) 69 | eventProfiler = eventprofiler.Profiler(predicate, 5, 5) 70 | eventProfiler.run(feed, True) 71 | 72 | self.assertEqual(eventProfiler.getResults().getEventCount(), 0) 73 | 74 | def testOneEvent(self): 75 | feed = yahoofeed.Feed() 76 | feed.addBarsFromCSV("orcl", common.get_data_file_path("orcl-2000-yahoofinance.csv")) 77 | predicate = Predicate([datetime.date(2000, 1, 11)]) 78 | eventProfiler = eventprofiler.Profiler(predicate, 5, 5) 79 | eventProfiler.run(feed, True) 80 | 81 | self.assertEqual(eventProfiler.getResults().getEventCount(), 1) 82 | self.assertEqual(eventProfiler.getResults().getValues(0)[0], 1.0) 83 | self.assertEqual(round(eventProfiler.getResults().getValues(5)[0], 5), round(1.016745541, 5)) 84 | -------------------------------------------------------------------------------- /tests/feed_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | from mooquant import dispatcher 23 | 24 | 25 | # This will test both the feed and subject interface. 26 | def tstBaseFeedInterface(testCase, feed): 27 | # This tests the observer.Subject interface. 28 | disp = dispatcher.Dispatcher() 29 | disp.addSubject(feed) 30 | disp.run() 31 | 32 | # This tests the feed.BaseFeed interface. 33 | feed.createDataSeries("any", 10) 34 | feed.getNextValues() 35 | -------------------------------------------------------------------------------- /tests/http_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import http.server 4 | import threading 5 | 6 | 7 | class WebServerThread(threading.Thread): 8 | def __init__(self, host, port, handlerClass): 9 | super(WebServerThread, self).__init__() 10 | self.__host = host 11 | self.__port = port 12 | self.__handlerClass = handlerClass 13 | self.__server = None 14 | 15 | def run(self): 16 | def handler_cls_builder(*args, **kwargs): 17 | return self.__handlerClass(*args, **kwargs) 18 | 19 | self.__server = http.server.HTTPServer((self.__host, self.__port), handler_cls_builder) 20 | self.__server.serve_forever() 21 | 22 | def stop(self): 23 | self.__server.shutdown() 24 | 25 | 26 | # handlerClass should be a subclass of (BaseHTTPServer.BaseHTTPRequestHandler. 27 | # The handler instance will get a thread and server attribute injected. 28 | def run_webserver_thread(host, port, handlerClass): 29 | wss_thread = WebServerThread(host, port, handlerClass) 30 | wss_thread.start() 31 | return wss_thread 32 | -------------------------------------------------------------------------------- /tests/logger_test.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 19 | """ 20 | 21 | import datetime 22 | 23 | from . import common 24 | 25 | 26 | class TestCase(common.TestCase): 27 | # Check that strategy and custom logs have the proper datetime, this is, the bars date time. 28 | def testBacktestingLog1(self): 29 | code = """from tests import logger_test_1 30 | logger_test_1.main() 31 | """ 32 | res = common.run_python_code(code) 33 | expectedLines = [ 34 | "2000-01-01 00:00:00 strategy [INFO] bla", 35 | "2000-01-01 00:00:00 custom [INFO] ble", 36 | ] 37 | 38 | print(res.get_output_lines()) 39 | 40 | self.assertEqual(res.get_output_lines(), expectedLines) 41 | self.assertTrue(res.exit_ok()) 42 | 43 | # Check that strategy and custom logs have the proper datetime, this is, the bars date time. 44 | def testBacktestingLog2(self): 45 | code = """from tests import logger_test_2 46 | logger_test_2.main() 47 | """ 48 | res = common.run_python_code(code) 49 | self.assertEqual(len(res.get_output_lines()), 3) 50 | self.assertEqual(res.get_output_lines()[0], "2000-01-01 00:00:00 strategy [INFO] bla") 51 | self.assertEqual( 52 | res.get_output_lines()[1], 53 | "2000-01-02 00:00:00 broker.backtesting [DEBUG] Not enough cash to fill orcl order [1] for 1 share/s" 54 | ) 55 | self.assertEqual(res.get_output_lines()[2], "2000-01-02 00:00:00 strategy [INFO] bla") 56 | self.assertTrue(res.exit_ok()) 57 | 58 | # Check that strategy and custom logs have the proper datetime, this is, the current date. 59 | def testNonBacktestingLog3(self): 60 | code = """from tests import logger_test_3 61 | logger_test_3.main() 62 | """ 63 | res = common.run_python_code(code) 64 | now = datetime.datetime.now() 65 | 66 | self.assertEqual(len(res.get_output_lines()), 2) 67 | 68 | for line in res.get_output_lines(True): 69 | self.assertEqual(line.find(str(now.date())), 0) 70 | 71 | self.assertNotEqual(res.get_output_lines()[0].find("strategy [INFO] bla"), -1) 72 | self.assertNotEqual(res.get_output_lines()[1].find("custom [INFO] ble"), -1) 73 | self.assertTrue(res.exit_ok()) 74 | -------------------------------------------------------------------------------- /tests/logger_test_1.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 19 | """ 20 | 21 | import datetime 22 | 23 | from mooquant import bar, logger, strategy 24 | from mooquant.barfeed import membf 25 | 26 | 27 | class TestBarFeed(membf.BarFeed): 28 | def barsHaveAdjClose(self): 29 | raise NotImplementedError() 30 | 31 | 32 | class BacktestingStrategy(strategy.BacktestingStrategy): 33 | def __init__(self, barFeed, cash): 34 | strategy.BacktestingStrategy.__init__(self, barFeed, cash) 35 | 36 | def onBars(self, bars): 37 | self.info("bla") 38 | logger.getLogger("custom").info("ble") 39 | 40 | 41 | def main(): 42 | bf = TestBarFeed(bar.Frequency.DAY) 43 | bars = [bar.BasicBar(datetime.datetime(2000, 1, 1), 10, 10, 10, 10, 10, 10, bar.Frequency.DAY),] 44 | bf.addBarsFromSequence("orcl", bars) 45 | 46 | strat = BacktestingStrategy(bf, 1000) 47 | strat.run() 48 | -------------------------------------------------------------------------------- /tests/logger_test_2.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 19 | """ 20 | 21 | import datetime 22 | import logging 23 | 24 | from mooquant import bar, logger, strategy 25 | from mooquant.barfeed import membf 26 | 27 | 28 | class TestBarFeed(membf.BarFeed): 29 | def barsHaveAdjClose(self): 30 | raise NotImplementedError() 31 | 32 | 33 | class BacktestingStrategy(strategy.BacktestingStrategy): 34 | def __init__(self, barFeed, cash): 35 | strategy.BacktestingStrategy.__init__(self, barFeed, cash) 36 | 37 | def onBars(self, bars): 38 | self.info("bla") 39 | self.marketOrder("orcl", 1) 40 | 41 | 42 | def main(): 43 | bf = TestBarFeed(bar.Frequency.DAY) 44 | bars = [ 45 | bar.BasicBar(datetime.datetime(2000, 1, 1), 10, 10, 10, 10, 10, 10, bar.Frequency.DAY), 46 | bar.BasicBar(datetime.datetime(2000, 1, 2), 10, 10, 10, 10, 10, 10, bar.Frequency.DAY), 47 | ] 48 | 49 | bf.addBarsFromSequence("orcl", bars) 50 | logger.getLogger().setLevel(logging.DEBUG) 51 | 52 | strat = BacktestingStrategy(bf, 1) 53 | strat.run() 54 | -------------------------------------------------------------------------------- /tests/logger_test_3.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 19 | """ 20 | 21 | import datetime 22 | 23 | from mooquant import bar, logger, strategy 24 | from mooquant.barfeed import membf 25 | from mooquant.broker import backtesting 26 | 27 | 28 | class TestBarFeed(membf.BarFeed): 29 | def barsHaveAdjClose(self): 30 | raise NotImplementedError() 31 | 32 | 33 | class Strategy(strategy.BaseStrategy): 34 | def __init__(self, barFeed, cash): 35 | strategy.BaseStrategy.__init__(self, barFeed, backtesting.Broker(cash, barFeed)) 36 | 37 | def onBars(self, bars): 38 | self.info("bla") 39 | logger.getLogger("custom").info("ble") 40 | 41 | 42 | def main(): 43 | bf = TestBarFeed(bar.Frequency.DAY) 44 | bars = [ 45 | bar.BasicBar(datetime.datetime(2000, 1, 1), 10, 10, 10, 10, 10, 10, bar.Frequency.DAY), 46 | ] 47 | 48 | bf.addBarsFromSequence("orcl", bars) 49 | 50 | strat = Strategy(bf, 1000) 51 | strat.run() 52 | -------------------------------------------------------------------------------- /tests/memfeed_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import datetime 23 | 24 | from mooquant import dispatcher 25 | from mooquant.feed import memfeed 26 | 27 | from . import common, feed_test 28 | 29 | 30 | class MemFeedTestCase(common.TestCase): 31 | def testBaseFeedInterface(self): 32 | values = [(datetime.datetime.now() + datetime.timedelta(seconds=i), {"i": i}) for i in range(100)] 33 | feed = memfeed.MemFeed() 34 | feed.addValues(values) 35 | feed_test.tstBaseFeedInterface(self, feed) 36 | 37 | def testFeed(self): 38 | values = [(datetime.datetime.now() + datetime.timedelta(seconds=i), {"i": i}) for i in range(100)] 39 | 40 | feed = memfeed.MemFeed() 41 | feed.addValues(values) 42 | 43 | # Check that the dataseries are available after adding values. 44 | self.assertTrue("i" in feed) 45 | self.assertEqual(len(feed["i"]), 0) 46 | self.assertFalse("dt" in feed) 47 | 48 | disp = dispatcher.Dispatcher() 49 | disp.addSubject(feed) 50 | disp.run() 51 | 52 | self.assertTrue("i" in feed) 53 | self.assertFalse("dt" in feed) 54 | self.assertEqual(feed["i"][0], 0) 55 | self.assertEqual(feed["i"][-1], 99) 56 | 57 | def testReset(self): 58 | key = "i" 59 | values = [(datetime.datetime.now() + datetime.timedelta(seconds=i), {key: i}) for i in range(100)] 60 | 61 | feed = memfeed.MemFeed() 62 | feed.addValues(values) 63 | 64 | disp = dispatcher.Dispatcher() 65 | disp.addSubject(feed) 66 | disp.run() 67 | 68 | keys = feed.getKeys() 69 | values = feed[key] 70 | 71 | feed.reset() 72 | disp = dispatcher.Dispatcher() 73 | disp.addSubject(feed) 74 | disp.run() 75 | 76 | reloadedKeys = feed.getKeys() 77 | reloadedValues = feed[key] 78 | 79 | self.assertEqual(keys, reloadedKeys) 80 | self.assertNotEqual(values, reloadedValues) 81 | self.assertEqual(len(values), len(reloadedValues)) 82 | 83 | for i in range(len(values)): 84 | self.assertEqual(values[i], reloadedValues[i]) 85 | -------------------------------------------------------------------------------- /tests/optimizer_testcase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import logging 23 | import sys 24 | 25 | from mooquant import strategy 26 | from mooquant.barfeed import yahoofeed 27 | from mooquant.optimizer import local 28 | 29 | from . import common 30 | from .sma_crossover import SMACrossOver 31 | 32 | 33 | def parameters_generator(instrument, smaFirst, smaLast): 34 | for sma in range(smaFirst, smaLast+1): 35 | yield(instrument, sma) 36 | 37 | 38 | class FailingStrategy(strategy.BacktestingStrategy): 39 | def __init__(self, barFeed, instrument, smaPeriod): 40 | super(FailingStrategy, self).__init__(barFeed) 41 | 42 | def onBars(self, bars): 43 | print("oh no!") 44 | # raise Exception("oh no!") 45 | 46 | 47 | class OptimizerTestCase(common.TestCase): 48 | def testLocal(self): 49 | barFeed = yahoofeed.Feed() 50 | barFeed.addBarsFromCSV("orcl", common.get_data_file_path("orcl-2000-yahoofinance.csv")) 51 | 52 | res = local.run(SMACrossOver, barFeed, parameters_generator("orcl", 5, 100), logLevel=logging.DEBUG ) 53 | 54 | self.assertEqual(round(res.getResult(), 2), 1295462.6) 55 | self.assertEqual(res.getParameters()[1], 20) 56 | 57 | def testFailingStrategy(self): 58 | barFeed = yahoofeed.Feed() 59 | barFeed.addBarsFromCSV("orcl", common.get_data_file_path("orcl-2000-yahoofinance.csv")) 60 | 61 | res = local.run(FailingStrategy, barFeed, parameters_generator("orcl", 5, 100), logLevel=logging.DEBUG) 62 | 63 | self.assertIsNone(res) 64 | -------------------------------------------------------------------------------- /tests/plotter_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import os 23 | import sys 24 | 25 | from mooquant import plotter 26 | from mooquant.barfeed import yahoofeed 27 | 28 | from . import common, sma_crossover 29 | 30 | sys.path.append("examples") 31 | 32 | 33 | class PlotterTestCase(common.TestCase): 34 | def testDownloadAndParseDaily(self): 35 | instrument = "orcl" 36 | barFeed = yahoofeed.Feed() 37 | barFeed.addBarsFromCSV(instrument, common.get_data_file_path("orcl-2000-yahoofinance.csv")) 38 | 39 | strat = sma_crossover.SMACrossOver(barFeed, instrument, 20) 40 | 41 | plt = plotter.StrategyPlotter(strat, True, True, True) 42 | plt.getInstrumentSubplot(instrument).addDataSeries("sma", strat.getSMA()) 43 | 44 | strat.run() 45 | 46 | with common.TmpDir() as tmpPath: 47 | fig, subplots = plt.buildFigureAndSubplots() 48 | 49 | self.assertIsNotNone(fig) 50 | self.assertIsNotNone(subplots) 51 | 52 | # fig = plt.buildFigure() 53 | fig, _ = plt.buildFigureAndSubplots() 54 | fig.set_size_inches(10, 8) 55 | 56 | png = os.path.join(tmpPath, "plotter_test.png") 57 | fig.savefig(png) 58 | 59 | # Check that file size looks ok. 60 | # 118458 on Mac 61 | # 116210 on Linux 62 | # self.assertGreater(os.stat(png).st_size, 110000) 63 | -------------------------------------------------------------------------------- /tests/pusher_test.py: -------------------------------------------------------------------------------- 1 | # MooQuant 2 | # 3 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """ 18 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 19 | """ 20 | 21 | import datetime 22 | import threading 23 | import unittest 24 | 25 | from mooquant.websocket import pusher 26 | 27 | 28 | class WebSocketClient(pusher.WebSocketClient): 29 | def __init__(self): 30 | pusher.WebSocketClient .__init__(self, "de504dc5763aeef9ff52", maxInactivity=1) 31 | self.__errors = 0 32 | self.__unknown_events = 0 33 | self.__connected = None 34 | 35 | def __checkStop(self): 36 | if self.__errors == 0: 37 | return 38 | if self.__unknown_events == 0: 39 | return 40 | # Give it some time to send ping messages. 41 | if self.__connected is None or (datetime.datetime.now() - self.__connected).total_seconds() < 3: 42 | return 43 | 44 | self.close() 45 | 46 | def onConnectionEstablished(self, event): 47 | pusher.WebSocketClient.onConnectionEstablished(self, event) 48 | self.__connected = datetime.datetime.now() 49 | self.sendPong() 50 | self.subscribeChannel("invalid channel") 51 | self.subscribeChannel("order_book") 52 | self.subscribeChannel("live_trades") 53 | 54 | def onError(self, event): 55 | self.__errors += 1 56 | self.__checkStop() 57 | 58 | def onUnknownEvent(self, event): 59 | self.__unknown_events += 1 60 | self.__checkStop() 61 | 62 | def stop(self): 63 | self.close() 64 | 65 | 66 | class WebSocketClientThread(threading.Thread): 67 | def __init__(self): 68 | threading.Thread.__init__(self) 69 | self.__wsclient = WebSocketClient() 70 | 71 | def run(self): 72 | self.__wsclient.connect() 73 | self.__wsclient.startClient() 74 | 75 | def stop(self): 76 | self.__wsclient.stop() 77 | 78 | 79 | class TestCase(unittest.TestCase): 80 | def test_pusher(self): 81 | thread = WebSocketClientThread() 82 | thread.start() 83 | thread.join(30) 84 | # After 30 seconds the thread should have finished. 85 | if thread.isAlive(): 86 | thread.stop() 87 | self.assertTrue(False) 88 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.5.0 2 | tox==3.4.0 3 | coverage==4.5.1 4 | cryptography==2.3.1 5 | PyYAML==3.13 6 | pytest==3.8.1 7 | pytest-runner==4.2 8 | python-dateutil==2.7.3 9 | matplotlib==3.0.0 10 | requests==2.19.1 11 | tornado==5.1.1 12 | tweepy==3.6.0 13 | TA-Lib==0.4.17 14 | numpy==1.15.2 15 | scipy==1.1.0 16 | ws4py==0.5.1 17 | pytz==2018.5 18 | nose==1.3.7 19 | moult==0.1.2 20 | pycallgraph==1.0.1 21 | -------------------------------------------------------------------------------- /tests/sma_crossover.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from mooquant import strategy 5 | from mooquant.technical import cross, ma 6 | 7 | 8 | class SMACrossOver(strategy.BacktestingStrategy): 9 | def __init__(self, feed, instrument, smaPeriod): 10 | strategy.BacktestingStrategy.__init__(self, feed) 11 | self.__instrument = instrument 12 | self.__position = None 13 | # We'll use adjusted close values instead of regular close values. 14 | self.setUseAdjustedValues(True) 15 | self.__prices = feed[instrument].getPriceDataSeries() 16 | self.__sma = ma.SMA(self.__prices, smaPeriod) 17 | 18 | def getSMA(self): 19 | return self.__sma 20 | 21 | def onEnterCanceled(self, position): 22 | self.__position = None 23 | 24 | def onExitOk(self, position): 25 | self.__position = None 26 | 27 | def onExitCanceled(self, position): 28 | # If the exit was canceled, re-submit it. 29 | self.__position.exitMarket() 30 | 31 | def onBars(self, bars): 32 | # If a position was not opened, check if we should enter a long position. 33 | if self.__position is None: 34 | if cross.cross_above(self.__prices, self.__sma) > 0: 35 | shares = int(self.getBroker().getCash() * 0.9 / bars[self.__instrument].getPrice()) 36 | # Enter a buy market order. The order is good till canceled. 37 | self.__position = self.enterLong(self.__instrument, shares, True) 38 | # Check if we have to exit the position. 39 | elif not self.__position.exitActive() and cross.cross_below(self.__prices, self.__sma) > 0: 40 | self.__position.exitMarket() 41 | -------------------------------------------------------------------------------- /tests/technical_cumret_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | from mooquant import dataseries 23 | from mooquant.technical import cumret 24 | 25 | from . import common 26 | 27 | 28 | class CumRetTestCase(common.TestCase): 29 | def testCumRet(self): 30 | values = dataseries.SequenceDataSeries() 31 | rets = cumret.CumulativeReturn(values) 32 | 33 | for value in [1, 2, 3, 4, 4, 3, 1, 1.2]: 34 | values.append(value) 35 | 36 | self.assertEqual(rets[0], None) 37 | self.assertEqual(rets[1], 1) 38 | self.assertEqual(rets[2], 2) 39 | self.assertEqual(rets[3], 3) 40 | self.assertEqual(rets[4], 3) 41 | self.assertEqual(rets[5], 2) 42 | self.assertEqual(rets[6], 0) 43 | self.assertEqual(round(rets[7], 1), 0.2) 44 | -------------------------------------------------------------------------------- /tests/technical_highlow_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | from mooquant import dataseries 23 | from mooquant.technical import highlow 24 | 25 | from . import common 26 | 27 | 28 | class HighLowTestCase(common.TestCase): 29 | def testHighLow(self): 30 | values = dataseries.SequenceDataSeries() 31 | high = highlow.High(values, 5) 32 | low = highlow.Low(values, 3) 33 | 34 | for value in [1, 2, 3, 4, 5]: 35 | values.append(value) 36 | 37 | self.assertEqual(high[-1], 5) 38 | self.assertEqual(low[-1], 3) 39 | -------------------------------------------------------------------------------- /tests/technical_hurst_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import numpy as np 23 | from mooquant import dataseries 24 | from mooquant.technical import hurst 25 | 26 | from . import common 27 | 28 | 29 | def build_hurst(values, period, minLags, maxLags): 30 | ds = dataseries.SequenceDataSeries() 31 | ret = hurst.HurstExponent(ds, period, minLags, maxLags) 32 | 33 | for value in values: 34 | ds.append(value) 35 | 36 | return ret 37 | 38 | 39 | class TestCase(common.TestCase): 40 | def testHurstExpFunRandomWalk(self): 41 | values = np.cumsum(np.random.randn(50000)) + 1000 42 | h = hurst.hurst_exp(np.log10(values), 2, 20) 43 | self.assertEqual(round(h, 1), 0.5) 44 | 45 | def testHurstExpFunTrending(self): 46 | values = np.cumsum(np.random.randn(50000)+1) + 1000 47 | h = hurst.hurst_exp(np.log10(values), 2, 20) 48 | self.assertEqual(round(h), 1) 49 | 50 | def testHurstExpFunMeanRev(self): 51 | values = (np.random.randn(50000)) + 1000 52 | h = hurst.hurst_exp(np.log10(values), 2, 20) 53 | self.assertEqual(round(h), 0) 54 | 55 | def testRandomWalk(self): 56 | num_values = 10000 57 | values = np.cumsum(np.random.randn(num_values)) + 1000 58 | hds = build_hurst(values, num_values - 10, 2, 20) 59 | 60 | self.assertEqual(round(hds[-1], 1), 0.5) 61 | self.assertEqual(round(hds[-2], 1), 0.5) 62 | 63 | def testTrending(self): 64 | num_values = 10000 65 | values = np.cumsum(np.random.randn(num_values) + 10) + 1000 66 | hds = build_hurst(values, num_values - 10, 2, 20) 67 | 68 | self.assertEqual(round(hds[-1], 1), 1) 69 | self.assertEqual(round(hds[-2], 1), 1) 70 | 71 | def testMeanRev(self): 72 | num_values = 10000 73 | values = np.random.randn(num_values) + 100 74 | hds = build_hurst(values, num_values - 10, 2, 20) 75 | 76 | self.assertEqual(round(hds[-1], 1), 0) 77 | self.assertEqual(round(hds[-2], 1), 0) 78 | -------------------------------------------------------------------------------- /tests/technical_linreg_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import datetime 23 | 24 | from mooquant import dataseries 25 | from mooquant.technical import linreg 26 | 27 | from . import common 28 | 29 | 30 | class LeastSquaresRegressionTestCase(common.TestCase): 31 | def testLsreg1(self): 32 | x = [0, 1, 2] 33 | y = [1, 2, 3] 34 | a, b = linreg.lsreg(x, y) 35 | 36 | self.assertEqual(round(a, 2), 1.0) 37 | self.assertEqual(round(b, 2), 1.0) 38 | 39 | def testLsreg2(self): 40 | x = [0, 1, 2] 41 | y = [4, 5, 6] 42 | a, b = linreg.lsreg(x, y) 43 | 44 | self.assertEqual(round(a, 2), 1.0) 45 | self.assertEqual(round(b, 2), 4.0) 46 | 47 | def testLsreg3(self): 48 | x = [1, 2, 3] 49 | y = [1, 2, 3] 50 | a, b = linreg.lsreg(x, y) 51 | 52 | self.assertEqual(round(a, 2), 1.0) 53 | self.assertEqual(round(b, 2), 0) 54 | 55 | def testStraightLine(self): 56 | seqDS = dataseries.SequenceDataSeries() 57 | lsReg = linreg.LeastSquaresRegression(seqDS, 3) 58 | 59 | nextDateTime = datetime.datetime(2012, 1, 1) 60 | seqDS.appendWithDateTime(nextDateTime, 1) 61 | self.assertEqual(lsReg[-1], None) 62 | 63 | nextDateTime = nextDateTime + datetime.timedelta(hours=1) 64 | seqDS.appendWithDateTime(nextDateTime, 2) 65 | self.assertEqual(lsReg[-1], None) 66 | 67 | # Check current value. 68 | nextDateTime = nextDateTime + datetime.timedelta(hours=1) 69 | seqDS.appendWithDateTime(nextDateTime, 3) 70 | self.assertEqual(round(lsReg[-1], 2), 3) 71 | 72 | # Check future values. 73 | futureDateTime = nextDateTime + datetime.timedelta(hours=1) 74 | self.assertEqual(round(lsReg.getValueAt(futureDateTime), 2), 4) 75 | futureDateTime = futureDateTime + datetime.timedelta(minutes=30) 76 | self.assertEqual(round(lsReg.getValueAt(futureDateTime), 2), 4.5) 77 | futureDateTime = futureDateTime + datetime.timedelta(minutes=30) 78 | self.assertEqual(round(lsReg.getValueAt(futureDateTime), 2), 5) 79 | 80 | # Move forward in sub-second increments. 81 | nextDateTime = nextDateTime + datetime.timedelta(milliseconds=50) 82 | seqDS.appendWithDateTime(nextDateTime, 4) 83 | nextDateTime = nextDateTime + datetime.timedelta(milliseconds=50) 84 | seqDS.appendWithDateTime(nextDateTime, 5) 85 | self.assertEqual(round(lsReg[-1], 2), 5) 86 | -------------------------------------------------------------------------------- /tests/technical_macd_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | from mooquant import dataseries 23 | from mooquant.technical import macd 24 | 25 | from . import common 26 | 27 | 28 | class MACDTestCase(common.TestCase): 29 | def testMACD(self): 30 | values = [16.39, 16.4999, 16.45, 16.43, 16.52, 16.51, 16.423, 16.41, 16.47, 16.45, 16.32, 16.36, 16.34, 16.59, 16.54, 16.52, 16.44, 16.47, 16.5, 16.45, 16.28, 16.07, 16.08, 16.1, 16.1, 16.09, 16.43, 16.4899, 16.59, 16.65, 16.78, 16.86, 16.86, 16.76] 31 | # These expected values were generated using TA-Lib 32 | macdValues = [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 0.0067, 0.0106, 0.0028, -0.0342, -0.0937, -0.1214, -0.1276, -0.125, -0.1195, -0.0459, 0.0097, 0.0601, 0.0975, 0.139, 0.1713, 0.1816, 0.1598] 33 | signalValues = [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 0.0036, 0.0056, 0.0048, -0.0064, -0.0313, -0.057, -0.0772, -0.0909, -0.0991, -0.0839, -0.0571, -0.0236, 0.011, 0.0475, 0.0829, 0.1111, 0.125] 34 | histogramValues = [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 0.0031, 0.005, -0.002, -0.0279, -0.0624, -0.0643, -0.0504, -0.0342, -0.0205, 0.0379, 0.0668, 0.0838, 0.0865, 0.0914, 0.0884, 0.0705, 0.0348] 35 | ds = dataseries.SequenceDataSeries() 36 | macdDs = macd.MACD(ds, 5, 13, 6) 37 | 38 | for i, value in enumerate(values): 39 | ds.append(value) 40 | self.assertEqual(common.safe_round(macdDs[i], 4), macdValues[i]) 41 | self.assertEqual(common.safe_round(macdDs.getSignal()[i], 4), signalValues[i]) 42 | self.assertEqual(common.safe_round(macdDs.getHistogram()[i], 4), histogramValues[i]) 43 | -------------------------------------------------------------------------------- /tests/technical_ratio_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | from mooquant import dataseries 23 | from mooquant.technical import ratio 24 | 25 | from . import common 26 | 27 | 28 | class TestCase(common.TestCase): 29 | def __buildRatio(self, values, ratioMaxLen=None): 30 | seqDS = dataseries.SequenceDataSeries() 31 | ret = ratio.Ratio(seqDS, ratioMaxLen) 32 | 33 | for value in values: 34 | seqDS.append(value) 35 | 36 | return ret 37 | 38 | def testSimple(self): 39 | ratio = self.__buildRatio([1, 2, 1]) 40 | self.assertEqual(ratio[0], None) 41 | self.assertEqual(ratio[1], 1) 42 | self.assertEqual(ratio[2], -0.5) 43 | self.assertEqual(ratio[-1], -0.5) 44 | 45 | with self.assertRaises(IndexError): 46 | ratio[3] 47 | 48 | self.assertEqual(ratio[-2], ratio[1]) 49 | self.assertEqual(ratio[-1], ratio[2]) 50 | self.assertEqual(len(ratio.getDateTimes()), 3) 51 | 52 | for i in range(len(ratio)): 53 | self.assertEqual(ratio.getDateTimes()[i], None) 54 | 55 | def testNegativeValues(self): 56 | ratio = self.__buildRatio([-1, -2, -1]) 57 | self.assertEqual(ratio[0], None) 58 | self.assertEqual(ratio[1], -1) 59 | self.assertEqual(ratio[2], 0.5) 60 | self.assertEqual(ratio[-1], 0.5) 61 | 62 | with self.assertRaises(IndexError): 63 | ratio[3] 64 | 65 | self.assertEqual(ratio[-2], ratio[1]) 66 | self.assertEqual(ratio[-1], ratio[2]) 67 | self.assertEqual(len(ratio.getDateTimes()), 3) 68 | 69 | for i in range(len(ratio)): 70 | self.assertEqual(ratio.getDateTimes()[i], None) 71 | 72 | def testBounded(self): 73 | ratio = self.__buildRatio([-1, -2, -1], 2) 74 | self.assertEqual(ratio[0], -1) 75 | self.assertEqual(ratio[1], 0.5) 76 | self.assertEqual(len(ratio), 2) 77 | -------------------------------------------------------------------------------- /tests/technical_roc_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | from mooquant import dataseries 23 | from mooquant.technical import roc 24 | 25 | from . import common 26 | 27 | 28 | class ROCTestCase(common.TestCase): 29 | def __buildROC(self, values, period, rocMaxLen=None): 30 | seqDS = dataseries.SequenceDataSeries() 31 | ret = roc.RateOfChange(seqDS, period, rocMaxLen) 32 | 33 | for value in values: 34 | seqDS.append(value) 35 | 36 | return ret 37 | 38 | def testPeriod12(self): 39 | # http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:rate_of_change 40 | inputValues = [11045.27, 11167.32, 11008.61, 11151.83, 10926.77, 10868.12, 10520.32, 10380.43, 10785.14, 10748.26, 10896.91, 10782.95, 10620.16, 10625.83, 10510.95, 10444.37, 10068.01, 10193.39, 10066.57, 10043.75] 41 | roc_ = self.__buildROC(inputValues, 12) 42 | outputValues = [-3.85, -4.85, -4.52, -6.34, -7.86, -6.21, -4.31, -3.24] 43 | 44 | for i in range(len(outputValues)): 45 | outputValue = roc_[12 + i] * 100 46 | self.assertTrue(round(outputValue, 2) == outputValues[i]) 47 | 48 | self.assertEqual(len(roc_.getDateTimes()), len(inputValues)) 49 | 50 | for i in range(len(roc_)): 51 | self.assertEqual(roc_.getDateTimes()[i], None) 52 | 53 | def testPeriod1(self): 54 | def simple_roc(value1, value2): 55 | return self.__buildROC([value1, value2], 1)[1] 56 | 57 | self.assertTrue(simple_roc(1, 2) == 1) 58 | self.assertTrue(simple_roc(1, 2) == simple_roc(50, 100)) 59 | self.assertTrue(simple_roc(2, 1) == -0.5) 60 | self.assertTrue(simple_roc(2, 1) == simple_roc(100, 50)) 61 | 62 | def testBounded(self): 63 | # http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:rate_of_change 64 | inputValues = [11045.27, 11167.32, 11008.61, 11151.83, 10926.77, 10868.12, 10520.32, 10380.43, 10785.14, 10748.26, 10896.91, 10782.95, 10620.16, 10625.83, 10510.95, 10444.37, 10068.01, 10193.39, 10066.57, 10043.75] 65 | outputValues = [-4.31, -3.24] 66 | roc_ = self.__buildROC(inputValues, 12, 2) 67 | 68 | for i in range(2): 69 | self.assertEqual(round(roc_[i], 4), round(outputValues[i] / 100, 4)) 70 | 71 | def testZeroes(self): 72 | inputValues = [0, 0, 0] 73 | outputValues = [None, 0, 0] 74 | roc_ = self.__buildROC(inputValues, 1) 75 | 76 | for i in range(len(inputValues)): 77 | self.assertEqual(roc_[i], outputValues[i]) 78 | -------------------------------------------------------------------------------- /tests/technical_stats_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import numpy 23 | from mooquant import dataseries 24 | from mooquant.technical import stats 25 | 26 | from . import common 27 | 28 | 29 | class TestCase(common.TestCase): 30 | def testStdDev_1(self): 31 | values = [1, 1, 2, 3, 5] 32 | seqDS = dataseries.SequenceDataSeries() 33 | stdDev = stats.StdDev(seqDS, 1) 34 | 35 | for value in values: 36 | seqDS.append(value) 37 | 38 | for i in stdDev: 39 | self.assertEqual(i, 0) 40 | 41 | def testStdDev(self): 42 | values = [1, 1, 2, 3, 5] 43 | seqDS = dataseries.SequenceDataSeries() 44 | stdDev = stats.StdDev(seqDS, 2) 45 | 46 | for value in values: 47 | seqDS.append(value) 48 | 49 | self.assertEqual(stdDev[0], None) 50 | self.assertEqual(stdDev[1], numpy.array([1, 1]).std()) 51 | self.assertEqual(stdDev[2], numpy.array([1, 2]).std()) 52 | self.assertEqual(stdDev[3], numpy.array([2, 3]).std()) 53 | self.assertEqual(stdDev[4], numpy.array([3, 5]).std()) 54 | 55 | def testStdDev_Bounded(self): 56 | values = [1, 1, 2, 3, 5] 57 | seqDS = dataseries.SequenceDataSeries() 58 | stdDev = stats.StdDev(seqDS, 2, maxLen=2) 59 | 60 | for value in values: 61 | seqDS.append(value) 62 | 63 | self.assertEqual(stdDev[0], numpy.array([2, 3]).std()) 64 | self.assertEqual(stdDev[1], numpy.array([3, 5]).std()) 65 | 66 | def testZScore(self): 67 | values = [1.10, 2.20, 4.00, 5.10, 6.00, 7.10, 8.20, 9.00, 10.10, 3.00, 4.10, 5.20, 7.00, 8.10, 9.20, 16.00, 17.10, 18.20, 19.30, 20.40] 68 | expected = [None, None, None, None, 1.283041407, 1.317884611, 1.440611043, 1.355748299, 1.4123457, -1.831763202, -0.990484842, -0.388358578, 0.449889908, 1.408195169, 1.332948099, 1.867732104, 1.334258333, 1.063608066, 0.939656572, 1.414213562] 69 | seqDS = dataseries.SequenceDataSeries() 70 | zscore = stats.ZScore(seqDS, 5) 71 | i = 0 72 | 73 | for value in values: 74 | seqDS.append(value) 75 | 76 | if i >= 4: 77 | self.assertEqual(round(zscore[-1], 4), round(expected[i], 4)) 78 | 79 | i += 1 80 | -------------------------------------------------------------------------------- /tests/technical_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | from mooquant import dataseries, technical 23 | 24 | from . import common 25 | 26 | 27 | class MyTestEventWindow(technical.EventWindow): 28 | def __init__(self): 29 | technical.EventWindow.__init__(self, 1, skipNone=False, dtype=object) 30 | 31 | def getValue(self): 32 | return self.getValues()[-1] 33 | 34 | 35 | class MyTestFilter(technical.EventBasedFilter): 36 | def __init__(self, dataSeries): 37 | technical.EventBasedFilter.__init__(self, dataSeries, MyTestEventWindow()) 38 | 39 | 40 | class DataSeriesFilterTest(common.TestCase): 41 | def testInvalidPosNotCached(self): 42 | ds = dataseries.SequenceDataSeries() 43 | testFilter = MyTestFilter(ds) 44 | 45 | for i in range(10): 46 | ds.append(i) 47 | ds.append(None) # Interleave Nones. 48 | 49 | self.assertEqual(testFilter[-1], None) 50 | self.assertEqual(testFilter[-2], 9) 51 | self.assertEqual(testFilter[-4], 8) # We go 3 instead of 2 because we need to skip the interleaved None values. 52 | 53 | self.assertEqual(testFilter[18], 9) 54 | self.assertEqual(testFilter[19], None) 55 | 56 | # Absolut pos 20 should have the next value once we insert it, but right now it should be invalid. 57 | with self.assertRaises(IndexError): 58 | testFilter[20] 59 | 60 | ds.append(10) 61 | self.assertEqual(testFilter[20], 10) 62 | -------------------------------------------------------------------------------- /tests/test_strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # MooQuant 3 | # 4 | # Copyright 2011-2015 Gabriel Martin Becedillas Ruiz 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | """ 19 | .. moduleauthor:: Gabriel Martin Becedillas Ruiz 20 | """ 21 | 22 | import datetime 23 | 24 | from mooquant import strategy 25 | 26 | 27 | class BaseTestStrategy(strategy.BaseStrategy): 28 | def __init__(self, barFeed, broker, maxMinutes=5): 29 | super().__init__(barFeed, broker) 30 | 31 | self.posExecutionInfo = [] 32 | self.ordersUpdated = [] 33 | self.orderExecutionInfo = [] 34 | self.begin = datetime.datetime.now() 35 | self.deadline = self.begin + datetime.timedelta(minutes=maxMinutes) 36 | 37 | def onOrderUpdated(self, order): 38 | self.ordersUpdated.append(order) 39 | self.orderExecutionInfo.append(order.getExecutionInfo()) 40 | 41 | def onEnterOk(self, position): 42 | self.posExecutionInfo.append(position.getEntryOrder().getExecutionInfo()) 43 | 44 | def onEnterCanceled(self, position): 45 | self.posExecutionInfo.append(position.getEntryOrder().getExecutionInfo()) 46 | 47 | def onExitOk(self, position): 48 | self.posExecutionInfo.append(position.getExitOrder().getExecutionInfo()) 49 | 50 | def onExitCanceled(self, position): 51 | self.posExecutionInfo.append(position.getExitOrder().getExecutionInfo()) 52 | 53 | def onIdle(self): 54 | if datetime.datetime.now() >= self.deadline: 55 | self.stop() 56 | -------------------------------------------------------------------------------- /tests/websocket_server.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from wsgiref import simple_server 3 | 4 | from ws4py.server import wsgirefserver, wsgiutils 5 | 6 | 7 | class WebSocketServerThread(threading.Thread): 8 | def __init__(self, host, port, webSocketServerClass): 9 | super().__init__() 10 | 11 | self.__host = host 12 | self.__port = port 13 | self.__webSocketServerClass = webSocketServerClass 14 | self.__server = None 15 | 16 | def run(self): 17 | def handler_cls_builder(*args, **kwargs): 18 | return self.__webSocketServerClass(*args, **kwargs) 19 | 20 | self.__server = simple_server.make_server( 21 | self.__host, 22 | self.__port, 23 | server_class=wsgirefserver.WSGIServer, 24 | handler_class=wsgirefserver.WebSocketWSGIRequestHandler, 25 | app=wsgiutils.WebSocketWSGIApplication(handler_cls=handler_cls_builder) 26 | ) 27 | 28 | self.__server.initialize_websockets_manager() 29 | self.__server.serve_forever() 30 | 31 | def stop(self): 32 | self.__server.shutdown() 33 | 34 | 35 | # webSocketServerClass should be a subclass of ws4py.websocket.WebSocket 36 | def run_websocket_server_thread(host, port, webSocketServerClass): 37 | wss_thread = WebSocketServerThread(host, port, webSocketServerClass) 38 | wss_thread.start() 39 | 40 | return wss_thread 41 | -------------------------------------------------------------------------------- /tools/ts2moo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import click 4 | import coloredlogs 5 | import pandas as pd 6 | import tushare as ts 7 | 8 | logger = logging.getLogger(__name__) 9 | coloredlogs.install(level='DEBUG', logger=logger) 10 | 11 | 12 | @click.group() 13 | @click.option('-v', '--verbose', count=True) 14 | @click.pass_context 15 | def cli(ctx, verbose): 16 | ctx.obj["VERBOSE"] = verbose 17 | 18 | 19 | @cli.command(help='读取股票行情数据.') 20 | @click.option('-s', '--symbol', default='600848', help='股票代码') 21 | @click.option('-k', '--ktype', default='D', help='股票代码') 22 | @click.option('-b', '--begin', default=None, help='下载模块') 23 | @click.option('-e', '--end', default=None, help='股票代码') 24 | def fetch(symbol, ktype, begin, end): 25 | logger.info('starting...') 26 | # 得到15分钟数据(股票600848,始于2016-01-01,止于2016-05-24,15分钟数据) 27 | df = ts.get_hist_data(symbol, ktype=ktype) 28 | # 数据存盘 29 | # data.to_csv('15-600848.csv') 30 | # 读出数据,DataFrame格式 31 | # df = pd.read_csv('15-600848.csv') 32 | # 从df中选取数据段,改变段名;新段'Adj Close'使用原有段'close'的数据 33 | logger.info('covering...') 34 | 35 | df2 = pd.DataFrame({'Date Time': df.index, 'Open': df['open'], 36 | 'High': df['high'], 'Close': df['close'], 37 | 'Low': df['low'], 'Volume': df['volume'], 38 | 'Adj Close': df['close']}) 39 | 40 | df2 = df2.sort_index(ascending=True) 41 | df2 = df2.reset_index(drop=True) 42 | 43 | # 按照Yahoo格式的要求,调整df2各段的顺序 44 | df2['Date Time'] = pd.to_datetime(df2['Date Time'], format='%Y-%m-%d %H:%M:%S') 45 | 46 | dt = df2.pop('Date Time') 47 | df2.insert(0, 'Date Time', dt) 48 | 49 | op = df2.pop('Open') 50 | df2.insert(1, 'Open', op) 51 | 52 | high = df2.pop('High') 53 | df2.insert(2, 'High', high) 54 | 55 | low = df2.pop('Low') 56 | df2.insert(3, 'Low', low) 57 | 58 | close = df2.pop('Close') 59 | df2.insert(4, 'Close', close) 60 | 61 | volume = df2.pop('Volume') 62 | df2.insert(5, 'Volume', volume) 63 | 64 | # 新格式数据存盘,不保存索引编号 65 | df2.to_csv("%s-%s.csv" % (symbol, ktype), index=False, date_format='%Y-%m-%d %H:%M:%S') 66 | 67 | logger.info("save %s-%s.csv" % (symbol, ktype)) 68 | 69 | 70 | def main(): 71 | cli(obj={}) 72 | 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py33, py34, py35, py36, py37, flake8 3 | 4 | [travis] 5 | python = 6 | 3.7: py37 7 | 3.6: py36 8 | 3.5: py35 9 | 3.4: py34 10 | 3.3: py33 11 | 12 | [testenv:flake8] 13 | basepython=python 14 | deps=flake8 15 | commands=flake8 mooquant 16 | 17 | [testenv] 18 | setenv = 19 | PYTHONPATH = {toxinidir} 20 | deps = 21 | -r{toxinidir}/requirements.txt 22 | commands = 23 | pip install -U pip 24 | py.test --basetemp={envtmpdir} 25 | -------------------------------------------------------------------------------- /travis/Dockerfile: -------------------------------------------------------------------------------- 1 | # ARG PYALGOTRADE_TAG 2 | # FROM gbecedillas/pyalgotrade:${PYALGOTRADE_TAG} 3 | FROM gbecedillas/pyalgotrade:0.20-py27 4 | 5 | MAINTAINER Gabriel Martin Becedillas Ruiz 6 | 7 | RUN apt-get update 8 | 9 | RUN pip install tox 10 | # Required by matplotlib 11 | RUN apt-get install -y python-tk 12 | 13 | RUN pip freeze 14 | 15 | RUN mkdir /tmp/pyalgotrade 16 | 17 | # Files needed to execute testcases. 18 | COPY setup.py /tmp/pyalgotrade/ 19 | COPY travis/run_tests.sh /tmp/pyalgotrade/ 20 | COPY coverage.cfg /tmp/pyalgotrade/ 21 | COPY tox.ini /tmp/pyalgotrade/ 22 | COPY pyalgotrade /tmp/pyalgotrade/pyalgotrade 23 | COPY testcases /tmp/pyalgotrade/testcases 24 | COPY samples /tmp/pyalgotrade/samples 25 | 26 | # Remove the installed version of PyAlgoTrade since we'll be executing testcases from source. 27 | RUN pip uninstall -y pyalgotrade 28 | -------------------------------------------------------------------------------- /travis/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH 4 | export PYTHONPATH=. 5 | 6 | tox -v -e py36 7 | tox -v -e py37 8 | --------------------------------------------------------------------------------