├── .gitignore ├── MANIFEST.in ├── README.md ├── __init__.py ├── contents ├── bar_data_sample.png ├── data_folder.png ├── data_folder_details.png ├── live_monitor.png ├── pnl.png ├── results.png └── telegram_bot.png ├── examples ├── __init__.py ├── data │ └── k_line │ │ └── K_1M │ │ ├── FUT.GC │ │ ├── 2021-03-15.csv │ │ ├── 2021-03-16.csv │ │ └── 2021-03-17.csv │ │ ├── FUT.SI │ │ ├── 2021-03-15.csv │ │ ├── 2021-03-16.csv │ │ └── 2021-03-17.csv │ │ └── HK.01157 │ │ ├── 2021-03-15.csv │ │ ├── 2021-03-16.csv │ │ └── 2021-03-17.csv └── demo_strategy │ ├── __init__.py │ ├── demo_strategy.py │ ├── main_demo.py │ ├── monitor_config.py │ └── qtrader_config.py ├── qtrader ├── __init__.py ├── core │ ├── __init__.py │ ├── balance.py │ ├── constants.py │ ├── data.py │ ├── deal.py │ ├── engine.cp38-win_amd64.pyd │ ├── engine.cpython-38-darwin.so │ ├── engine.cpython-38-x86_64-linux-gnu.so │ ├── event_engine.cp38-win_amd64.pyd1 │ ├── event_engine.cpython-38-darwin.so │ ├── event_engine.cpython-38-x86_64-linux-gnu.so │ ├── logger.py │ ├── order.py │ ├── portfolio.py │ ├── position.py │ ├── security.py │ ├── strategy.py │ └── utility.py ├── gateways │ ├── __init__.py │ ├── backtest │ │ ├── __init__.py │ │ └── backtest_gateway.py │ ├── base_gateway.py │ ├── cqg │ │ ├── __init__.py │ │ ├── cqg_fees.py │ │ ├── cqg_gateway.py │ │ └── wrapper │ │ │ ├── CELEnvironment.py │ │ │ ├── __init__.py │ │ │ └── cqg_api.py │ ├── futu │ │ ├── __init__.py │ │ ├── futu_fees.py │ │ └── futu_gateway.py │ ├── ib │ │ ├── __init__.py │ │ ├── ib_fees.py │ │ ├── ib_gateway copy.py │ │ └── ib_gateway.py │ └── instrument_cfg.yaml └── plugins │ ├── __init__.py │ ├── analysis │ ├── __init__.py │ ├── livetrade.py │ ├── metrics.py │ └── performance.py │ ├── clickhouse │ ├── __init__.py │ └── client.py │ ├── monitor │ ├── __init__.py │ └── livemonitor.py │ ├── sqlite3 │ ├── __init__.py │ └── db.py │ └── telegram │ ├── __init__.py │ └── bot.py ├── qtrader_config_sample.py ├── requirements.txt ├── setup.py └── tests ├── README.md ├── __init__.py ├── cqg_test.py ├── futu_test.py ├── futufutures_test.py ├── gateway_test.py └── ib_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[co] 4 | *$py.class 5 | 6 | # C extensions 7 | # *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # IDE 141 | .DS_Store 142 | .idea/ 143 | 144 | # cache 145 | .qtrader_cache/ 146 | 147 | # log 148 | log/* 149 | qtrader/log/* 150 | results/* 151 | examples/*/log/* 152 | examples/*/results/* 153 | 154 | # core 155 | config.py 156 | engine.py 157 | event_engine.py 158 | setup.py 159 | 160 | 161 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include qtrader/core *.pyd *.so -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QTrader: A Light Event-Driven Algorithmic Trading Engine 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 |

10 | 11 | **Latest update on 2022-09-07** 12 | 13 | ## Key modifications 14 | - 2022-09-07: Moved `qtrader.config` to `qtrader_config` 15 | 16 | ## Introduction 17 | QTrader is a light and flexible event-driven algorithmic trading engine that 18 | can be used to backtest strategies, and seamlessly switch to live trading 19 | without any pain. 20 | 21 | ## Key Features 22 | 23 | * Completely **same code** for backtesting / simulation / live trading 24 | 25 | * Support trading of various assets: equity, futures 26 | 27 | * Resourceful functionalities to support live monitoring and analysis 28 | 29 | ## Quick Install 30 | 31 | You may run the folllowing command to install QTrader immediately: 32 | 33 | ```python 34 | # Virtual environment is recommended (python 3.8 or above is supported) 35 | >> conda create -n qtrader python=3.8 36 | >> conda activate qtrader 37 | 38 | # Install stable version from pip (currently version 0.0.4) 39 | >> pip install qtrader 40 | 41 | # Alternatively, install latest version from github 42 | >> pip install git+https://github.com/josephchenhk/qtrader@master 43 | ``` 44 | 45 | ## Create a Configuration File 46 | 47 | At your current working directory, create `qtrader_config.py` if not exists. 48 | There is an example `qtrader_config_sample.py` for your reference. Adjust the 49 | items if necessary. 50 | 51 | ## Prepare the Data 52 | 53 | QTrader supports bar data at the moment. What you need to do is creating a 54 | folder with the name of the security you are interested in. Let's say you want 55 | to backtest or trade HK equity **"HK.01157"** in frequency of **1 minute**, your 56 | data folder should be like this (where "K_1M" stands for 1 minute; you can also 57 | find a sample from the qtrader/examples/data): 58 | 59 | ![alt text](https://raw.githubusercontent.com/josephchenhk/qtrader/master/contents/data_folder.png "data folder") 60 | 61 | And you can prepare OHLCV data in CSV format, with dates as their file names, 62 | e.g., **"yyyy-mm-dd.csv"**: 63 | 64 | ![alt text](https://raw.githubusercontent.com/josephchenhk/qtrader/master/contents/data_folder_details.png "data folder details") 65 | 66 | Inside each csv file, the data columns should look like this: 67 | 68 | ![alt text](https://raw.githubusercontent.com/josephchenhk/qtrader/master/contents/bar_data_sample.png "bar data sample") 69 | 70 | Now you can specify the path of data folder in 71 | `qtrader/config/config.py` 72 | `qtrader_config.py`. For example, set 73 | 74 | ```python 75 | DATA_PATH = { 76 | "kline": "path_to_your_qtrader_folder/examples/data/k_line", 77 | } 78 | ``` 79 | 80 | ## Implement a Strategy 81 | 82 | To implement a strategy is simple in QTrader. A strategy needs to implement 83 | `init_strategy` and `on_bar` methods in `BaseStrategy`. Here is a quick sample: 84 | 85 | ```python 86 | from qtrader.core.strategy import BaseStrategy 87 | 88 | class MyStrategy(BaseStrategy): 89 | 90 | def init_strategy(self): 91 | pass 92 | 93 | def on_bar(self, cur_data:Dict[str, Dict[Security, Bar]]): 94 | print(cur_data) 95 | ``` 96 | 97 | 98 | ## Record Variables 99 | 100 | QTrader provides a module named `BarEventEngineRecorder` to record variables 101 | during backtesting and/or trading. By default it saves `datetime`, 102 | `portfolio_value` and `action` at every time step. 103 | 104 | If you want to record additional variables (let's say it is called `var`), you 105 | need to write a method called `get_var` in your strategy: 106 | 107 | ```python 108 | from qtrader.core.strategy import BaseStrategy 109 | 110 | class MyStrategy(BaseStrategy): 111 | 112 | def get_var(self): 113 | return var 114 | ``` 115 | 116 | And initialize your `BarEventEngineRecorder` with the same vairable `var=[]`(if 117 | you want to record every timestep) or `var=None`(if you want to record only the 118 | last updated value): 119 | 120 | ```python 121 | recorder = BarEventEngineRecorder(var=[]) 122 | ``` 123 | 124 | 125 | ## Run a Backtest 126 | 127 | Now we are ready to run a backtest. Here is a sample of running a backtest in 128 | QTrader: 129 | 130 | ```python 131 | # Security 132 | stock_list = [ 133 | Stock(code="HK.01157", lot_size=100, security_name="中联重科", exchange=Exchange.SEHK), 134 | ] 135 | 136 | # Gateway 137 | gateway_name = "Backtest" 138 | gateway = BacktestGateway( 139 | securities=stock_list, 140 | start=datetime(2021, 3, 15, 9, 30, 0, 0), 141 | end=datetime(2021, 3, 17, 16, 0, 0, 0), 142 | gateway_name=gateway_name, 143 | ) 144 | gateway.SHORT_INTEREST_RATE = 0.0 145 | gateway.set_trade_mode(TradeMode.BACKTEST) 146 | 147 | # Core engine 148 | engine = Engine(gateways={gateway_name: gateway}) 149 | 150 | # Strategy initialization 151 | init_capital = 100000 152 | strategy_account = "DemoStrategy" 153 | strategy_version = "1.0" 154 | strategy = DemoStrategy( 155 | securities={gateway_name: stock_list}, 156 | strategy_account=strategy_account, 157 | strategy_version=strategy_version, 158 | init_strategy_cash={gateway_name: init_capital}, 159 | engine=engine, 160 | strategy_trading_sessions={ 161 | "HK.01157": [ 162 | [datetime(1970, 1, 1, 9, 30, 0), datetime(1970, 1, 1, 12, 0, 0)], 163 | [datetime(1970, 1, 1, 13, 0, 0), datetime(1970, 1, 1, 16, 0, 0)], 164 | ], 165 | ) 166 | strategy.init_strategy() 167 | 168 | # Recorder 169 | recorder = BarEventEngineRecorder() 170 | 171 | # Event engine 172 | event_engine = BarEventEngine( 173 | {"demo": strategy}, 174 | {"demo": recorder}, 175 | engine 176 | ) 177 | 178 | # Start event engine 179 | event_engine.run() 180 | 181 | # Program terminates normally 182 | engine.log.info("Program shutdown normally.") 183 | ``` 184 | 185 | After shutdown, you will be able to find the results in qtrader/results, with 186 | the folder name of latest time stamp: 187 | 188 | ![alt text](https://raw.githubusercontent.com/josephchenhk/qtrader/master/contents/results.png "results") 189 | 190 | The result.csv file saves everything you want to record in 191 | `BarEventEngineRecorder`; while pnl.html is an interactive plot of the equity 192 | curve of your running strategy: 193 | 194 | ![alt text](https://raw.githubusercontent.com/josephchenhk/qtrader/master/contents/pnl.png "pnl") 195 | 196 | ## Simulation / Live trading 197 | 198 | Ok, your strategy looks good now. How can you put it to paper trading and/or 199 | live trading? In QTrader it is extremely easy to switch from backtest mode to 200 | simulation or live trading mode. What you need to modify is just 201 | **two** lines (replace a backtest gateway with a live trading gateway!): 202 | 203 | ```python 204 | # Currently you can use "Futu", "Ib", and "Cqg" 205 | gateway_name = "Futu" 206 | 207 | # Use FutuGateway, IbGateway, or CqgGateway accordingly 208 | # End time should be set to a future time stamp when you expect the program terminates 209 | gateway = FutuGateway( 210 | securities=stock_list, 211 | end=datetime(2022, 12, 31, 16, 0, 0, 0), 212 | gateway_name=gateway_name, 213 | ) 214 | 215 | # Choose either TradeMode.SIMULATE or TradeMode.LIVETRADE 216 | gateway.set_trade_mode(TradeMode.LIVETRADE) 217 | ``` 218 | 219 | That's it! You switch from backtest to simulation / live trading mode now. 220 | 221 | **Important Notice**: In the demo sample, the live trading mode will keep on 222 | sending orders, please be aware of the risk when running it. 223 | 224 | ## Live Monitoring 225 | 226 | When running the strategies, the trader typically needs to monitor the market 227 | and see whether the signals are triggered as expected. QTrader provides with 228 | such **dashboard**(visualization panel) which can dynamically update the market data and gives 229 | out entry and exit signals in line with the strategies. 230 | 231 | You can activate this function in your `qtrader_config.py`: 232 | 233 | ```python 234 | ACTIVATED_PLUGINS = [.., "monitor"] 235 | ``` 236 | 237 | After running the main script, you 238 | will be able to open a web-based monitor in the browser: `127.0.0.1:8050`: 239 | 240 | ![alt text](https://raw.githubusercontent.com/josephchenhk/qtrader/master/contents/live_monitor.png "live_monitor") 241 | 242 | QTrader is also equipped with a **Telegram Bot**, which allows you get instant 243 | information from your trading program. To enable this function, you can add your 244 | telegram information in `qtrader_config.py`(you can refer to the 245 | following [link](https://core.telegram.org/bots/api) for detailed guidance): 246 | 247 | ```python 248 | ACTIVATED_PLUGINS = [.., "telegram"] 249 | 250 | TELEGRAM_TOKEN = "50XXXXXX16:AAGan6nFgmrSOx9vJipwmXXXXXXXXXXXM3E" 251 | TELEGRAM_CHAT_ID = 21XXXXXX49 252 | ``` 253 | 254 | In this way, your mobile phone with telegram will automatically receive a 255 | documenting message: 256 | 257 | 258 | 259 | You can use your mobile phone to monitor and control your strategy now. 260 | 261 | ## Contributing 262 | * Fork it (https://github.com/josephchenhk/qtrader/fork) 263 | * Study how it's implemented. 264 | * Create your feature branch (git checkout -b my-new-feature). 265 | * Use [flake8](https://pypi.org/project/flake8/) to ensure your code format 266 | complies with PEP8. 267 | * Commit your changes (git commit -am 'Add some feature'). 268 | * Push to the branch (git push origin my-new-feature). 269 | * Create a new Pull Request. 270 | 271 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/9/2022 5:54 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | -------------------------------------------------------------------------------- /contents/bar_data_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/contents/bar_data_sample.png -------------------------------------------------------------------------------- /contents/data_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/contents/data_folder.png -------------------------------------------------------------------------------- /contents/data_folder_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/contents/data_folder_details.png -------------------------------------------------------------------------------- /contents/live_monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/contents/live_monitor.png -------------------------------------------------------------------------------- /contents/pnl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/contents/pnl.png -------------------------------------------------------------------------------- /contents/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/contents/results.png -------------------------------------------------------------------------------- /contents/telegram_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/contents/telegram_bot.png -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 17/3/2021 4:11 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | -------------------------------------------------------------------------------- /examples/demo_strategy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 18/3/2021 9:27 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | from .demo_strategy import DemoStrategy -------------------------------------------------------------------------------- /examples/demo_strategy/demo_strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 17/3/2021 3:56 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: demo_strategy.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | from datetime import datetime 17 | from time import sleep 18 | from typing import Dict, List 19 | 20 | import pandas as pd 21 | from finta import TA 22 | 23 | from qtrader.core.portfolio import Portfolio 24 | from qtrader.core.constants import Direction, Offset, OrderType, TradeMode, OrderStatus 25 | from qtrader.core.data import Bar 26 | from qtrader.core.engine import Engine 27 | from qtrader.core.security import Stock, Security 28 | from qtrader.core.strategy import BaseStrategy 29 | 30 | 31 | class DemoStrategy(BaseStrategy): 32 | """Demo strategy""" 33 | 34 | def __init__(self, 35 | securities: Dict[str, List[Stock]], 36 | strategy_account: str, 37 | strategy_version: str, 38 | engine: Engine, 39 | strategy_trading_sessions: List[List[datetime]]=None, 40 | init_strategy_portfolios: Dict[str, List[Portfolio]]=None, 41 | **kwargs 42 | ): 43 | super().__init__( 44 | securities=securities, 45 | strategy_account=strategy_account, 46 | strategy_version=strategy_version, 47 | engine=engine, 48 | strategy_trading_sessions=strategy_trading_sessions, 49 | init_strategy_portfolios=init_strategy_portfolios, 50 | **kwargs 51 | ) 52 | # security list 53 | self.securities = securities 54 | # execution engine 55 | self.engine = engine 56 | # For simulation/live trading, set the waiting time > 0 57 | self.sleep_time = 0 58 | for gateway_name in engine.gateways: 59 | if engine.gateways[gateway_name].trade_mode != TradeMode.BACKTEST: 60 | self.sleep_time = 5 61 | 62 | def init_strategy(self): 63 | self.ohlcv = {} 64 | for gateway_name in self.engine.gateways: 65 | self.ohlcv[gateway_name] = {} 66 | for security in self.engine.gateways[gateway_name].securities: 67 | self.ohlcv[gateway_name][security] = [] 68 | 69 | def on_bar(self, cur_data: Dict[str, Dict[Security, Bar]]): 70 | 71 | self.engine.log.info("-" * 30 + "Enter on_bar" + "-" * 30) 72 | self.engine.log.info(cur_data) 73 | 74 | for gateway_name in self.engine.gateways: 75 | 76 | if gateway_name not in cur_data: 77 | continue 78 | 79 | # check balance 80 | account_balance = self.get_strategy_account_balance(gateway_name=gateway_name) 81 | self.engine.log.info(f"{account_balance}") 82 | 83 | # check position 84 | position = self.get_strategy_position(gateway_name=gateway_name) 85 | self.engine.log.info(f"{position}") 86 | 87 | # send orders 88 | for security in cur_data[gateway_name]: 89 | if security not in self.securities[gateway_name]: 90 | continue 91 | bar = cur_data[gateway_name][security] 92 | # Collect bar data (only keep latest 20 records) 93 | self.ohlcv[gateway_name][security].append(bar) 94 | if len(self.ohlcv[gateway_name][security]) > 20: 95 | self.ohlcv[gateway_name][security].pop(0) 96 | 97 | open_ts = [b.open for b in 98 | self.ohlcv[gateway_name][security]] 99 | high_ts = [b.high for b in 100 | self.ohlcv[gateway_name][security]] 101 | low_ts = [b.low for b in 102 | self.ohlcv[gateway_name][security]] 103 | close_ts = [b.close for b in 104 | self.ohlcv[gateway_name][security]] 105 | 106 | ohlc = pd.DataFrame({ 107 | "open": open_ts, 108 | "high": high_ts, 109 | "low": low_ts, 110 | "close": close_ts 111 | }) 112 | macd = TA.MACD( 113 | ohlc, 114 | period_fast=12, 115 | period_slow=26, 116 | signal=9) 117 | 118 | if len(macd) < 2: 119 | continue 120 | 121 | prev_macd = macd.iloc[-2]["MACD"] 122 | cur_macd = macd.iloc[-1]["MACD"] 123 | cur_signal = macd.iloc[-1]["SIGNAL"] 124 | signal = None 125 | if prev_macd > cur_signal > cur_macd > 0: 126 | signal = "SELL" 127 | elif prev_macd < cur_signal < cur_macd < 0: 128 | signal = "BUY" 129 | 130 | position = self.get_strategy_position(gateway_name=gateway_name) 131 | long_position = position.get_position(security=security, direction=Direction.LONG) 132 | short_position = position.get_position(security=security, direction=Direction.SHORT) 133 | 134 | if short_position and signal == "SELL": 135 | continue 136 | elif long_position and signal == "BUY": 137 | continue 138 | elif long_position and signal == "SELL": 139 | order_instruct = dict( 140 | security=security, 141 | quantity=long_position.quantity, 142 | direction=Direction.SHORT, 143 | offset=Offset.CLOSE, 144 | order_type=OrderType.MARKET, 145 | gateway_name=gateway_name, 146 | ) 147 | elif signal == "SELL": 148 | order_instruct = dict( 149 | security=security, 150 | quantity=1, 151 | direction=Direction.SHORT, 152 | offset=Offset.OPEN, 153 | order_type=OrderType.MARKET, 154 | gateway_name=gateway_name, 155 | ) 156 | elif short_position and signal == "BUY": 157 | order_instruct = dict( 158 | security=security, 159 | quantity=short_position.quantity, 160 | direction=Direction.LONG, 161 | offset=Offset.CLOSE, 162 | order_type=OrderType.MARKET, 163 | gateway_name=gateway_name, 164 | ) 165 | elif signal == "BUY": 166 | order_instruct = dict( 167 | security=security, 168 | quantity=1, 169 | direction=Direction.LONG, 170 | offset=Offset.OPEN, 171 | order_type=OrderType.MARKET, 172 | gateway_name=gateway_name, 173 | ) 174 | else: 175 | continue 176 | 177 | self.engine.log.info(f"Submit order: \n{order_instruct}") 178 | orderid = self.engine.send_order(**order_instruct) 179 | if orderid == "": 180 | self.engine.log.info("Fail to submit order") 181 | return 182 | 183 | sleep(self.sleep_time) 184 | order = self.engine.get_order( 185 | orderid=orderid, gateway_name=gateway_name) 186 | self.engine.log.info( 187 | f"Order ({orderid}) has been sent: {order}") 188 | 189 | deals = self.engine.find_deals_with_orderid( 190 | orderid, gateway_name=gateway_name) 191 | for deal in deals: 192 | self.portfolios[gateway_name].update(deal) 193 | 194 | if order.status == OrderStatus.FILLED: 195 | self.engine.log.info(f"Order ({orderid}) has been filled.") 196 | else: 197 | err = self.engine.cancel_order( 198 | orderid=orderid, gateway_name=gateway_name) 199 | if err: 200 | self.engine.log.info( 201 | f"Can't cancel order ({orderid}). Error: {err}") 202 | else: 203 | self.engine.log.info( 204 | f"Successfully cancel order ({orderid}).") 205 | -------------------------------------------------------------------------------- /examples/demo_strategy/main_demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 9/15/2021 4:45 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: main_demo.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | 17 | ########################################################################## 18 | # 19 | # Demo strategy 20 | ########################################################################## 21 | import sys 22 | 23 | from qtrader_config import LOCAL_PACKAGE_PATHS 24 | from qtrader_config import ADD_LOCAL_PACKAGE_PATHS_TO_SYSPATH 25 | if ADD_LOCAL_PACKAGE_PATHS_TO_SYSPATH: 26 | for pth in LOCAL_PACKAGE_PATHS: 27 | if pth not in sys.path: 28 | sys.path.insert(0, pth) 29 | 30 | from datetime import datetime 31 | 32 | from qtrader.core.balance import AccountBalance 33 | from qtrader.core.position import Position 34 | from qtrader.core.portfolio import Portfolio 35 | from qtrader.core.constants import TradeMode, Exchange 36 | from qtrader.core.event_engine import BarEventEngineRecorder, BarEventEngine 37 | from qtrader.core.security import Futures 38 | from qtrader.core.engine import Engine 39 | from qtrader.gateways.cqg import CQGFees 40 | 41 | from demo_strategy import DemoStrategy 42 | 43 | 44 | if __name__ == "__main__": 45 | 46 | trade_mode = TradeMode.BACKTEST 47 | fees = CQGFees 48 | if trade_mode == TradeMode.BACKTEST: 49 | from qtrader.gateways import BacktestGateway 50 | gateway_name = "Backtest" # "Futufutures", "Backtest", "Cqg", "Ib" 51 | UseGateway = BacktestGateway 52 | start = datetime(2021, 3, 15, 15, 0, 0) 53 | end = datetime(2021, 3, 17, 23, 0, 0) 54 | elif trade_mode in (TradeMode.SIMULATE, TradeMode.LIVETRADE): 55 | from qtrader.gateways import CqgGateway 56 | gateway_name = "Cqg" 57 | UseGateway = CqgGateway 58 | start = None 59 | today = datetime.today() 60 | end = datetime(today.year, today.month, today.day, 23, 0, 0) 61 | 62 | stock_list = [ 63 | Futures(code="FUT.GC", lot_size=100, security_name="GCQ2", 64 | exchange=Exchange.NYMEX, expiry_date="20220828"), 65 | Futures(code="FUT.SI", lot_size=5000, security_name="SIN2", 66 | exchange=Exchange.NYMEX, expiry_date="20220727"), 67 | ] 68 | 69 | gateway = UseGateway( 70 | securities=stock_list, 71 | start=start, 72 | end=end, 73 | gateway_name=gateway_name, 74 | fees=fees, 75 | num_of_1min_bar=180 76 | ) 77 | 78 | gateway.SHORT_INTEREST_RATE = 0.0 79 | gateway.trade_mode = trade_mode 80 | if gateway.trade_mode in (TradeMode.SIMULATE, TradeMode.LIVETRADE): 81 | assert datetime.now() < gateway.end, ( 82 | "Gateway end time must be later than current datetime!") 83 | 84 | # Engine 85 | engine = Engine(gateways={gateway_name: gateway}) 86 | 87 | # get activated plugins 88 | plugins = engine.get_plugins() 89 | 90 | # Initialize strategy 91 | strategy_account = "Demo_Strategy" 92 | strategy_version = "1.0" 93 | init_position = Position() 94 | init_capital = 1000000 95 | init_strategy_portfolio = Portfolio( 96 | account_balance=AccountBalance(cash=init_capital), 97 | position=Position(), 98 | market=gateway 99 | ) 100 | 101 | strategy = DemoStrategy( 102 | securities={gateway_name: stock_list}, 103 | strategy_account=strategy_account, 104 | strategy_version=strategy_version, 105 | engine=engine, 106 | strategy_trading_sessions={ 107 | "FUT.GC": [[datetime(1970, 1, 1, 15, 0, 0), 108 | datetime(1970, 1, 1, 23, 0, 0)]], 109 | "FUT.SI": [[datetime(1970, 1, 1, 15, 0, 0), 110 | datetime(1970, 1, 1, 23, 0, 0)]] 111 | }, 112 | init_strategy_portfolios={'Backtest': init_strategy_portfolio}, 113 | ) 114 | strategy.init_strategy() 115 | 116 | # Event recorder 117 | recorder = BarEventEngineRecorder(datetime=[], 118 | open=[], 119 | high=[], 120 | low=[], 121 | close=[], 122 | volume=[]) 123 | event_engine = BarEventEngine( 124 | {"cta": strategy}, 125 | {"cta": recorder}, 126 | engine 127 | ) 128 | 129 | if "telegram" in plugins: 130 | telegram_bot = plugins["telegram"].bot 131 | telegram_bot.send_message(f"{datetime.now()} {telegram_bot.__doc__}") 132 | event_engine.run() 133 | 134 | result_path = recorder.save_csv() 135 | 136 | if "analysis" in plugins: 137 | plot_pnl = plugins["analysis"].plot_pnl 138 | plot_pnl(result_path=result_path, freq="daily") 139 | engine.log.info("Program shutdown normally.") 140 | -------------------------------------------------------------------------------- /examples/demo_strategy/monitor_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 27/4/2023 4:31 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: monitor_config.py 6 | 7 | """ 8 | Copyright (C) 2022 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the terms of the JXW license, 10 | which unfortunately won't be written for another century. 11 | 12 | You should have received a copy of the JXW license with this file. If not, 13 | please write to: josephchenhk@gmail.com 14 | """ 15 | 16 | instruments = { 17 | "Demo_Strategy": { 18 | "Backtest": { 19 | "security": ["FUT.GC", "FUT.SI"], 20 | "lot": [100, 5000], 21 | "commission": [1.98, 1.98], 22 | "slippage": [0.0, 0.0], 23 | "show_fields": {} 24 | } 25 | }, 26 | } -------------------------------------------------------------------------------- /examples/demo_strategy/qtrader_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 9:04 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: qtrader_config.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | 17 | """ 18 | Usage: Modify this config and put it in the same folder of your main script 19 | """ 20 | 21 | BACKTEST_GATEWAY = { 22 | "broker_name": "BACKTEST", 23 | "broker_account": "", 24 | "host": "", 25 | "port": -1, 26 | "pwd_unlock": -1, 27 | } 28 | 29 | GATEWAYS = { 30 | "Backtest": BACKTEST_GATEWAY, 31 | } 32 | 33 | # time step in milliseconds 34 | TIME_STEP = 1 * 60 * 1000 35 | 36 | DATA_PATH = { 37 | "kline": "path_to_your_data_folder/data/k_line", 38 | } 39 | 40 | DATA_MODEL = { 41 | "kline": "Bar", 42 | } 43 | 44 | ACTIVATED_PLUGINS = ["analysis"] 45 | 46 | LOCAL_PACKAGE_PATHS = [ 47 | "path_to_your_lib_folder/qtrader", 48 | "path_to_your_lib_folder/qtalib", 49 | ] 50 | ADD_LOCAL_PACKAGE_PATHS_TO_SYSPATH = True 51 | 52 | if "monitor" in ACTIVATED_PLUGINS: 53 | import monitor_config as MONITOR_CONFIG 54 | 55 | IGNORE_TIMESTEP_OVERFLOW = False 56 | AUTO_OPEN_PLOT = True 57 | 58 | # if true, ffill the historical data 59 | DATA_FFILL = True 60 | 61 | # end: timestamp for bar 9:15-9:16 is stamped as 9:16 62 | # start: timestamp for bar 9:15-9:16 is stamped as 9:15 63 | BAR_CONVENTION = { 64 | 'FUT.GC': 'start', 65 | 'FUT.SI': 'start', 66 | } 67 | 68 | -------------------------------------------------------------------------------- /qtrader/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/3/2021 11:50 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | -------------------------------------------------------------------------------- /qtrader/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/3/2021 11:59 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | -------------------------------------------------------------------------------- /qtrader/core/balance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 9:20 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: balance.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from dataclasses import dataclass 19 | from typing import Dict 20 | 21 | 22 | @dataclass 23 | class AccountBalance: 24 | """Account Balance Information""" 25 | cash: float = 0.0 # CashBalance(BASE) 26 | cash_by_currency: Dict[str, float] = None # CashBlance(HKD, USD, GBP) 27 | available_cash: float = 0.0 # AvailableFunds(HKD) 28 | max_power_short: float = None # Cash Power for Short 29 | net_cash_power: float = None # BuyingPower(HKD) 30 | maintenance_margin: float = None # MaintMarginReq(HKD) 31 | unrealized_pnl: float = 0.0 # UnrealizedPnL(HKD) 32 | realized_pnl: float = 0.0 # RealizedPnL(HKD) 33 | -------------------------------------------------------------------------------- /qtrader/core/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/3/2021 12:00 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: constants.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from enum import Enum 19 | 20 | 21 | class TradeMode(Enum): 22 | """Trading mode""" 23 | BACKTEST = "BACKTEST" 24 | LIVETRADE = "LIVETRADE" 25 | SIMULATE = "SIMULATE" 26 | 27 | 28 | class Direction(Enum): 29 | """Direction of order/trade/position.""" 30 | LONG = "LONG" 31 | SHORT = "SHORT" 32 | NET = "NET" 33 | 34 | 35 | class Offset(Enum): 36 | """Offset of order/trade.""" 37 | NONE = "" 38 | OPEN = "OPEN" 39 | CLOSE = "CLOSE" 40 | CLOSETODAY = "CLOSETODAY" 41 | CLOSEYESTERDAY = "CLOSEYESTERDAY" 42 | 43 | 44 | class OrderType(Enum): 45 | """Order type.""" 46 | LIMIT = "LMT" 47 | MARKET = "MKT" 48 | STOP = "STOP" 49 | FAK = "FAK" 50 | FOK = "FOK" 51 | 52 | 53 | class OrderStatus(Enum): 54 | """Order status""" 55 | UNKNOWN = "UNKNOWN" 56 | SUBMITTING = "SUBMITTING" 57 | SUBMITTED = "SUBMITTED" 58 | FILLED = "FILLED" 59 | PART_FILLED = "PART_FILLED" 60 | CANCELLED = "CANCELLED" 61 | FAILED = "FAILED" 62 | 63 | 64 | class Exchange(Enum): 65 | """Exchanges""" 66 | SEHK = "SEHK" # Stock Exchange of Hong Kong 67 | HKFE = "HKFE" # Hong Kong Futures Exchange 68 | SSE = "SSE" # Shanghai Stock Exchange 69 | SZSE = "SZSE" # Shenzhen Stock Exchange 70 | CME = "CME" # S&P Index, AUDUSD, etc 71 | COMEX = "COMEX" # Gold, silver, copper, etc 72 | NYMEX = "NYMEX" # Brent Oil, etc 73 | CBOT = "CBOT" # Bonds, soybean, rice, etc 74 | ECBOT = "ECBOT" # Bonds, soybean, rice, etc 75 | SGE = "SGE" # Shanghai Gold Exchange 76 | IDEALPRO = "IDEALPRO" # currency 77 | GLOBEX = "GLOBEX" # futures 78 | SMART = "SMART" # SMART in IB 79 | SGX = "SGX" # Singapore Exchange (https://www.sgx.com/) 80 | ICE = "ICE" # Products: QO (Brent Oil) 81 | BGN = "BGN" # Bloomberg price source BGN 82 | 83 | 84 | class Cash(Enum): 85 | """Currency""" 86 | NONE = "UNKNOWN" 87 | HKD = "HKD" 88 | USD = "USD" 89 | CNH = "CNH" 90 | -------------------------------------------------------------------------------- /qtrader/core/data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 6/3/2021 5:47 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: data.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | import os 19 | import importlib 20 | import warnings 21 | from dataclasses import dataclass 22 | from datetime import datetime 23 | from datetime import time as Time 24 | from datetime import date as Date 25 | from datetime import timedelta 26 | from typing import List, Any 27 | import pandas as pd 28 | 29 | from qtrader.core.constants import Exchange 30 | from qtrader.core.security import Stock, Security 31 | from qtrader.core.utility import get_kline_dfield_from_seconds 32 | from qtrader.core.utility import read_row_from_csv 33 | from qtrader_config import DATA_PATH, TIME_STEP, BAR_CONVENTION 34 | 35 | 36 | @dataclass 37 | class Bar: 38 | """OHLCV""" 39 | datetime: datetime 40 | security: Stock 41 | open: float 42 | high: float 43 | low: float 44 | close: float 45 | volume: float 46 | num_trds: int = 0 47 | value: float = 0 48 | ticker: str = '' 49 | trading_date: str = '' 50 | 51 | 52 | @dataclass 53 | class CapitalDistribution: 54 | """Capital Distributions""" 55 | datetime: datetime 56 | security: Stock 57 | capital_in_big: float 58 | capital_in_mid: float 59 | capital_in_small: float 60 | capital_out_big: float 61 | capital_out_mid: float 62 | capital_out_small: float 63 | 64 | 65 | @dataclass 66 | class OrderBook: 67 | """Orderbook""" 68 | security: Stock 69 | exchange: Exchange 70 | datetime: datetime 71 | 72 | bid_price_1: float = 0 73 | bid_price_2: float = 0 74 | bid_price_3: float = 0 75 | bid_price_4: float = 0 76 | bid_price_5: float = 0 77 | bid_price_6: float = 0 78 | bid_price_7: float = 0 79 | bid_price_8: float = 0 80 | bid_price_9: float = 0 81 | bid_price_10: float = 0 82 | 83 | ask_price_1: float = 0 84 | ask_price_2: float = 0 85 | ask_price_3: float = 0 86 | ask_price_4: float = 0 87 | ask_price_5: float = 0 88 | ask_price_6: float = 0 89 | ask_price_7: float = 0 90 | ask_price_8: float = 0 91 | ask_price_9: float = 0 92 | ask_price_10: float = 0 93 | 94 | bid_volume_1: float = 0 95 | bid_volume_2: float = 0 96 | bid_volume_3: float = 0 97 | bid_volume_4: float = 0 98 | bid_volume_5: float = 0 99 | bid_volume_6: float = 0 100 | bid_volume_7: float = 0 101 | bid_volume_8: float = 0 102 | bid_volume_9: float = 0 103 | bid_volume_10: float = 0 104 | 105 | ask_volume_1: float = 0 106 | ask_volume_2: float = 0 107 | ask_volume_3: float = 0 108 | ask_volume_4: float = 0 109 | ask_volume_5: float = 0 110 | ask_volume_6: float = 0 111 | ask_volume_7: float = 0 112 | ask_volume_8: float = 0 113 | ask_volume_9: float = 0 114 | ask_volume_10: float = 0 115 | 116 | bid_num_1: float = 0 117 | bid_num_2: float = 0 118 | bid_num_3: float = 0 119 | bid_num_4: float = 0 120 | bid_num_5: float = 0 121 | bid_num_6: float = 0 122 | bid_num_7: float = 0 123 | bid_num_8: float = 0 124 | bid_num_9: float = 0 125 | bid_num_10: float = 0 126 | 127 | ask_num_1: float = 0 128 | ask_num_2: float = 0 129 | ask_num_3: float = 0 130 | ask_num_4: float = 0 131 | ask_num_5: float = 0 132 | ask_num_6: float = 0 133 | ask_num_7: float = 0 134 | ask_num_8: float = 0 135 | ask_num_9: float = 0 136 | ask_num_10: float = 0 137 | 138 | 139 | @dataclass 140 | class Quote: 141 | """Quote""" 142 | security: Stock 143 | exchange: Exchange 144 | datetime: datetime 145 | 146 | last_price: float = 0 147 | open_price: float = 0 148 | high_price: float = 0 149 | low_price: float = 0 150 | prev_close_price: float = 0 151 | volume: float = 0 152 | turnover: float = 0 153 | turnover_rate: float = 0 154 | amplitude: float = 0 155 | suspension: bool = False 156 | price_spread: float = 0 157 | bid_price: float = 0 158 | ask_price: float = 0 159 | sec_status: str = "NORMAL" 160 | 161 | 162 | def _get_data_path(security: Security, dtype: str, **kwargs) -> str: 163 | """Get the path to corresponding csv files.""" 164 | if dtype == "kline": 165 | if "interval" in kwargs: 166 | interval = kwargs.get("interval") 167 | if "min" in interval: 168 | interval_in_sec = int(interval.replace('min', '')) * 60 169 | elif "hour" in interval: 170 | interval_in_sec = int(interval.replace('hour', '')) * 3600 171 | elif "day" in interval: 172 | interval_in_sec = int(interval.replace('day', '')) * 3600 * 24 173 | else: 174 | raise ValueError(f"interval {interval} is NOT valid!") 175 | else: 176 | interval_in_sec = TIME_STEP // 1000 # TIME_STEP is in millisecond 177 | kline_name = get_kline_dfield_from_seconds(time_step=interval_in_sec) 178 | data_path = f"{DATA_PATH[dtype]}/{kline_name}/{security.code}" 179 | else: 180 | data_path = f"{DATA_PATH[dtype]}/{security.code}" 181 | return data_path 182 | 183 | 184 | def _get_data_files(security: Security, dtype: str, **kwargs) -> List[str]: 185 | """Fetch csv files""" 186 | data_path = _get_data_path(security, dtype, **kwargs) 187 | if not os.path.exists(data_path): 188 | raise FileNotFoundError(f"Data was NOT found in {data_path}!") 189 | data_files = [f for f in os.listdir(data_path) if ".csv" in f] 190 | return data_files 191 | 192 | 193 | def _get_data( 194 | security: Stock, 195 | start: datetime, 196 | end: datetime, 197 | dtype: str, 198 | dfield: List[str] = None, 199 | **kwargs 200 | ) -> pd.DataFrame: 201 | """Get historical data""" 202 | time_col = 'time_key' 203 | if kwargs.get('interval') and 'min' in kwargs.get('interval'): 204 | # Get all csv files of the security given 205 | data_files = _get_data_files(security, dtype, **kwargs) 206 | # Filter out the data that is within the time range given 207 | data_files_in_range = [] 208 | for data_file in data_files: 209 | dt = datetime.strptime( 210 | data_file[-14:].replace(".csv", ""), "%Y-%m-%d").date() 211 | if start.date() <= dt <= end.date(): 212 | data_files_in_range.append(data_file) 213 | # Aggregate the data to a dataframe 214 | full_data = pd.DataFrame() 215 | for data_file in sorted(data_files_in_range): 216 | data_path = _get_data_path(security, dtype) 217 | if 'open' in read_row_from_csv(f"{data_path}/{data_file}", 1): 218 | data = pd.read_csv(f"{data_path}/{data_file}") 219 | elif 'open' in read_row_from_csv(f"{data_path}/{data_file}", 2): 220 | data = pd.read_csv(f"{data_path}/{data_file}", header=[0,1], index_col=[0]) 221 | # get only the principal contract 222 | levels = [lvl for lvl in set(data.columns.get_level_values(0)) if lvl!='meta'] 223 | volumes = {lvl: data.xs(lvl, level=0, axis=1).dropna()['volume'].sum() for lvl in levels} 224 | principal_level = max(volumes, key=volumes.get) 225 | data = data.xs(principal_level, level=0, axis=1).reset_index() 226 | # # Confirm the time column could be parsed into datetime 227 | # try: 228 | # datetime.strptime(data.iloc[0][time_col], "%Y-%m-%d %H:%M:%S") 229 | # 230 | # except BaseException: 231 | # raise ValueError( 232 | # f"{time_col} data {data.iloc[0][time_col]} can not convert to datetime") 233 | full_data = pd.concat([full_data, data]) 234 | if full_data.empty: 235 | raise ValueError( 236 | f"There is no historical data for {security.code} within time range" 237 | f": [{start} - {end}]!") 238 | full_data = full_data.sort_values(by=[time_col]) 239 | if BAR_CONVENTION.get(security.code) == 'start': 240 | start -= timedelta(minutes=int(TIME_STEP/60/1000)) 241 | start_str = start.strftime("%Y-%m-%d %H:%M:%S") 242 | end_str = end.strftime("%Y-%m-%d %H:%M:%S") 243 | full_data = full_data[(full_data[time_col] >= start_str) 244 | & (full_data[time_col] <= end_str)] 245 | full_data["time_key"] = pd.to_datetime(full_data["time_key"]) 246 | full_data = full_data.dropna() 247 | full_data.reset_index(drop=True, inplace=True) 248 | elif kwargs.get('interval') == '1day': 249 | data_path = _get_data_path(security, dtype, interval='1day') 250 | if 'open' in read_row_from_csv(f"{data_path}/ohlcv.csv", 1): 251 | data = pd.read_csv(f"{data_path}/ohlcv.csv") 252 | elif 'open' in read_row_from_csv(f"{data_path}/ohlcv.csv", 2): 253 | data = pd.read_csv(f"{data_path}/ohlcv.csv", header=[0, 1], index_col=[0]) 254 | # get only the principal contract 255 | levels = [lvl for lvl in set(data.columns.get_level_values(0)) if lvl != 'meta'] 256 | volumes = {lvl: data.xs(lvl, level=0, axis=1).dropna()['volume'].sum() for lvl in levels} 257 | principal_level = max(volumes, key=volumes.get) 258 | data = data.xs(principal_level, level=0, axis=1).reset_index() 259 | full_data = data 260 | full_data['time_key'] = pd.to_datetime(data['time_key']) 261 | 262 | # build continuous contracts for futures 263 | # 264 | # | idx | full_data | full_data.shift(1) | full_data.shift(-1) | switch1 | switch2 | 265 | # |-----|-----------------|----------------------|---------------------|-----------|-----------| 266 | # | 0 | 1 | NaN | 1 | Y | | 267 | # | 1 | 1 | 1 | 2 | | Y | 268 | # | 2 | 2 | 1 | 2 | Y | | 269 | # | 3 | 2 | 2 | 2 | | | 270 | # | 4 | 2 | 2 | 3 | | Y | 271 | # | 5 | 3 | 2 | 4 | Y | Y | 272 | # | 6 | 4 | 3 | NaN | Y | Y | 273 | # |-----|-----------------|----------------------|---------------------|-----------|-----------| 274 | # 275 | # we need to find even number of rows, where for each consecutive two rows, the second row with `switch1=Y`, and 276 | # first row with `switch2=Y`. In the above example, this means we want to get the following rows (the `roll`): 277 | # 278 | # | idx | full_data | full_data.shift(1) | full_data.shift(-1) | switch1 | switch2 | 279 | # |-----|-----------------|----------------------|---------------------|-----------|-----------| 280 | # | 1 | 1 | 1 | 2 | | Y | 281 | # | 2 | 2 | 1 | 2 | Y | | 282 | # |-----|-----------------|----------------------|---------------------|-----------|-----------| 283 | # | 4 | 2 | 2 | 3 | | Y | 284 | # | 5 | 3 | 2 | 4 | Y | | 285 | # |-----|-----------------|----------------------|---------------------|-----------|-----------| 286 | # | 5 | 3 | 2 | 4 | | Y | 287 | # | 6 | 4 | 3 | NaN | Y | | 288 | # |-----|-----------------|----------------------|---------------------|-----------|-----------| 289 | # 290 | # where row 5 has been used twice. 291 | 292 | if 'ticker' in full_data.columns: 293 | switch1 = (full_data['ticker'] != full_data['ticker'].shift(1)).astype(int) 294 | switch2 = (full_data['ticker'] != full_data['ticker'].shift(-1)).astype(int) 295 | if switch1[switch1>0].empty: 296 | roll = pd.DataFrame() 297 | else: 298 | switch_rows = [] 299 | for i in switch1[switch1>0].index: 300 | if i == 0: 301 | continue 302 | if switch2.loc[i-1] == 1: 303 | switch_rows.extend([i-1, i]) 304 | roll = full_data.loc[switch_rows] 305 | assert roll.shape[0] % 2 == 0, 'roll records should be an even number' 306 | # get adjustment factors and corresponding indices 307 | adj_factors = [] 308 | adj_indices = [] 309 | for i in range(1, len(roll), 2): 310 | factor = roll.iloc[i]['close'] / roll.iloc[i - 1]['close'] 311 | adj_factors.append(factor) 312 | adj_indices.append(roll.iloc[[i-1]].index[0]) 313 | # Adjust historical prices and make continuous data 314 | for idx, factor in zip(adj_indices, adj_factors): 315 | # print(idx, factor) 316 | full_data.loc[:idx, 'close'] *= factor 317 | full_data.loc[:idx, 'open'] *= factor 318 | full_data.loc[:idx, 'high'] *= factor 319 | full_data.loc[:idx, 'low'] *= factor 320 | return full_data 321 | 322 | 323 | def _get_data_iterator( 324 | security: Stock, 325 | full_data: pd.DataFrame, 326 | class_name: str 327 | ) -> Any: 328 | """Data generator""" 329 | # `class_name` could be Bar, CapitalDistribution, Quote, Orderbook, etc 330 | data_cls = getattr(importlib.import_module( 331 | "qtrader.core.data"), class_name) 332 | time_col = full_data.columns[0] 333 | assert "time" in time_col or "Time" in time_col, ( 334 | "The first column in `full_data` must be a `*time*` column, but " 335 | f"{time_col} was given." 336 | ) 337 | for _, row in full_data.iterrows(): 338 | cur_time = row[time_col].to_pydatetime() 339 | kwargs = {"datetime": cur_time, "security": security} 340 | for col in full_data.columns: 341 | if col == time_col: 342 | continue 343 | kwargs[col] = row[col] 344 | data = data_cls(**kwargs) 345 | yield data 346 | 347 | 348 | def _load_historical_bars_in_reverse( 349 | security: Security, 350 | cur_datetime: datetime, 351 | interval: str = "1min" 352 | ) -> List[str]: 353 | """Load historical csv data in reversed order.""" 354 | data_path = _get_data_path(security, "kline", interval=interval) 355 | csv_files = os.listdir(data_path) 356 | csv_files = [f for f in csv_files if ".csv" in f] 357 | hist_csv_files = [] 358 | for f in csv_files: 359 | if "_" in f: 360 | hist_csv_files.append(f) 361 | else: 362 | if datetime.strptime( 363 | f, "%Y-%m-%d.csv").date() <= cur_datetime.date(): 364 | hist_csv_files.append(f) 365 | hist_csv_files = sorted(hist_csv_files, reverse=True) 366 | return hist_csv_files 367 | 368 | 369 | def get_trading_day( 370 | dt: datetime, 371 | daily_open_time: Time, 372 | daily_close_time: Time 373 | ) -> Date: 374 | """Get futures trading day according to daily_open_time and daily_close_time 375 | given.""" 376 | if daily_open_time <= daily_close_time: 377 | return dt.date() 378 | elif daily_close_time < daily_open_time and dt.time() > daily_close_time: 379 | return (dt + timedelta(days=1)).date() 380 | elif daily_close_time < daily_open_time and dt.time() <= daily_close_time: 381 | return dt.date() 382 | else: 383 | warnings.warn( 384 | f"{dt} is NOT within {daily_open_time} and {daily_close_time}") 385 | return None 386 | -------------------------------------------------------------------------------- /qtrader/core/deal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 8:50 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: deal.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from dataclasses import dataclass 19 | from datetime import datetime 20 | 21 | from qtrader.core.constants import Direction, Offset, OrderType 22 | from qtrader.core.security import Security 23 | 24 | 25 | @dataclass 26 | class Deal: 27 | """Done deal/execution""" 28 | security: Security 29 | direction: Direction 30 | offset: Offset 31 | order_type: OrderType 32 | updated_time: datetime = None 33 | filled_avg_price: float = 0 34 | filled_quantity: int = 0 35 | dealid: str = "" 36 | orderid: str = "" 37 | -------------------------------------------------------------------------------- /qtrader/core/engine.cp38-win_amd64.pyd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/qtrader/core/engine.cp38-win_amd64.pyd -------------------------------------------------------------------------------- /qtrader/core/engine.cpython-38-darwin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/qtrader/core/engine.cpython-38-darwin.so -------------------------------------------------------------------------------- /qtrader/core/engine.cpython-38-x86_64-linux-gnu.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/qtrader/core/engine.cpython-38-x86_64-linux-gnu.so -------------------------------------------------------------------------------- /qtrader/core/event_engine.cp38-win_amd64.pyd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/qtrader/core/event_engine.cp38-win_amd64.pyd1 -------------------------------------------------------------------------------- /qtrader/core/event_engine.cpython-38-darwin.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/qtrader/core/event_engine.cpython-38-darwin.so -------------------------------------------------------------------------------- /qtrader/core/event_engine.cpython-38-x86_64-linux-gnu.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephchenhk/qtrader/f437ad6c2f71dfb4b5d23bb409f16d00596bba9e/qtrader/core/event_engine.cpython-38-x86_64-linux-gnu.so -------------------------------------------------------------------------------- /qtrader/core/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 10:08 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: logger.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | import os 19 | from datetime import datetime 20 | import logging 21 | 22 | # (1) create a logger and set its logging level 23 | logger = logging.getLogger() 24 | logger.setLevel(logging.DEBUG) 25 | 26 | # (2) create a file handler and set its logging level 27 | if "log" not in os.listdir(): 28 | os.mkdir(os.path.join(os.getcwd(), "log")) 29 | logfile = f'./log/{datetime.now().strftime("%Y-%m-%d %H-%M-%S.%f")}.txt' 30 | fh = logging.FileHandler(logfile, mode='a', encoding="utf-8") 31 | fh.setLevel(logging.ERROR) 32 | 33 | # (3) create a stream handler(output to console) and set its logging level 34 | ch = logging.StreamHandler() 35 | ch.setLevel(logging.INFO) 36 | 37 | # (4) define the output format of the two handlers above 38 | formatter = logging.Formatter( 39 | "%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s" 40 | ) 41 | fh.setFormatter(formatter) 42 | ch.setFormatter(formatter) 43 | 44 | # (5) add the two handlers to logger 45 | logger.addHandler(fh) 46 | logger.addHandler(ch) 47 | -------------------------------------------------------------------------------- /qtrader/core/order.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 8:50 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: order.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from dataclasses import dataclass 19 | from datetime import datetime 20 | 21 | from qtrader.core.constants import Direction, Offset, OrderType, OrderStatus 22 | from qtrader.core.security import Stock 23 | 24 | 25 | @dataclass 26 | class Order: 27 | """Order""" 28 | security: Stock 29 | price: float 30 | quantity: float 31 | direction: Direction 32 | offset: Offset 33 | order_type: OrderType 34 | create_time: datetime 35 | updated_time: datetime = None 36 | stop_price: float = None 37 | filled_avg_price: float = 0 38 | filled_quantity: int = 0 39 | status: OrderStatus = OrderStatus.UNKNOWN 40 | orderid: str = "" 41 | -------------------------------------------------------------------------------- /qtrader/core/portfolio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 9:22 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: portfolio.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | import threading 18 | 19 | from qtrader.core.balance import AccountBalance 20 | from qtrader.core.constants import Direction, Offset 21 | from qtrader.core.deal import Deal 22 | from qtrader.core.position import Position, PositionData 23 | from qtrader.gateways import BaseGateway 24 | 25 | lock = threading.Lock() 26 | 27 | class Portfolio: 28 | """Portfolio is bind to a specific gateway, it includes: 29 | 1. Account Balance 30 | 2. Position 31 | 3. Market (Gateway) 32 | """ 33 | 34 | def __init__( 35 | self, 36 | account_balance: AccountBalance, 37 | position: Position, 38 | market: BaseGateway, 39 | reporting_currency: str = '' 40 | ): 41 | self.account_balance = account_balance 42 | self.position = position 43 | self.market = market 44 | self.reporting_currency = reporting_currency 45 | 46 | def update(self, deal: Deal): 47 | with lock: 48 | security = deal.security 49 | lot_size = security.lot_size 50 | price = deal.filled_avg_price 51 | quantity = deal.filled_quantity 52 | direction = deal.direction 53 | offset = deal.offset 54 | filled_time = deal.updated_time 55 | fx_rate = self.market.get_exchange_rate( 56 | base=self.reporting_currency, 57 | quote=security.quote_currency 58 | ) 59 | fee = self.market.fees(deal).total_fees / fx_rate 60 | # update balance 61 | self.account_balance.cash -= fee 62 | if direction == Direction.LONG: 63 | self.account_balance.cash -= price * quantity * lot_size / fx_rate 64 | if offset == Offset.CLOSE: # pay interest when closing short 65 | short_position = self.position.holdings[security][Direction.SHORT] 66 | short_interest = ( 67 | short_position.holding_price 68 | * short_position.quantity 69 | * (filled_time - short_position.update_time).days / 365 70 | * self.market.SHORT_INTEREST_RATE 71 | ) 72 | self.account_balance.cash -= short_interest / fx_rate 73 | elif direction == Direction.SHORT: 74 | self.account_balance.cash += price * quantity * lot_size / fx_rate 75 | # update position 76 | position_data = PositionData( 77 | security=security, 78 | direction=direction, 79 | holding_price=price, 80 | quantity=quantity, 81 | update_time=deal.updated_time 82 | ) 83 | self.position.update( 84 | position_data=position_data, 85 | offset=offset 86 | ) 87 | 88 | @property 89 | def value(self): 90 | with lock: 91 | v = self.account_balance.cash 92 | for security in self.position.holdings: 93 | fx_rate = self.market.get_exchange_rate( 94 | base=self.reporting_currency, 95 | quote=security.quote_currency 96 | ) 97 | recent_data = self.market.get_recent_data( 98 | security=security, 99 | cur_datetime=self.market.market_datetime, 100 | dfield="kline" 101 | ) 102 | if recent_data is not None: 103 | cur_price = recent_data.close 104 | else: 105 | # 2022.02.23 (Joseph): If bar data is not available, we will not 106 | # be able to get the updated portfolio value; We circumvent this 107 | # by using the holding prices of the securities (Be alerted that 108 | # this is an estimation of the portfolio value, it is NOT 109 | # accurate). 110 | cur_price = 0 111 | for i, pos in enumerate(self.position.holdings[security]): 112 | cur_price += self.position.holdings[security][pos].holding_price 113 | cur_price /= (i + 1) 114 | for direction in self.position.holdings[security]: 115 | position_data = self.position.holdings[security][direction] 116 | if direction == Direction.LONG: 117 | v += cur_price * position_data.quantity * security.lot_size / fx_rate 118 | elif direction == Direction.SHORT: 119 | v -= cur_price * position_data.quantity * security.lot_size / fx_rate 120 | return v 121 | -------------------------------------------------------------------------------- /qtrader/core/position.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 9:18 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: position.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | import threading 18 | 19 | from dataclasses import dataclass 20 | from datetime import datetime 21 | from typing import Dict, List 22 | 23 | from qtrader.core.constants import Direction, Offset 24 | from qtrader.core.security import Stock 25 | 26 | lock = threading.Lock() 27 | 28 | @dataclass 29 | class PositionData: 30 | """Position information of a specific security""" 31 | security: Stock 32 | direction: Direction 33 | holding_price: float 34 | quantity: int 35 | update_time: datetime 36 | 37 | 38 | class Position: 39 | """Position information of a specific gateway (may include multiple 40 | securities)""" 41 | 42 | def __init__(self, holdings: Dict = None): 43 | if holdings is None: 44 | holdings = dict() 45 | self.holdings = holdings 46 | 47 | def update(self, position_data: PositionData, offset: Offset): 48 | with lock: 49 | security = position_data.security 50 | direction = position_data.direction 51 | holding_price = position_data.holding_price 52 | quantity = position_data.quantity 53 | update_time = position_data.update_time 54 | if offset == Offset.OPEN: 55 | if security not in self.holdings: 56 | self.holdings[security] = {} 57 | self.holdings[security][direction] = position_data 58 | elif direction not in self.holdings[security]: 59 | self.holdings[security][direction] = position_data 60 | else: 61 | old_position_data = self.holdings[security][direction] 62 | new_quantity = old_position_data.quantity + quantity 63 | new_total_value = ( 64 | old_position_data.holding_price * old_position_data.quantity 65 | + holding_price * quantity 66 | ) 67 | new_holding_price = new_total_value / new_quantity 68 | self.holdings[security][direction] = PositionData( 69 | security=security, 70 | direction=direction, 71 | holding_price=new_holding_price, 72 | quantity=new_quantity, 73 | update_time=update_time 74 | ) 75 | elif offset == offset.CLOSE: 76 | offset_direction = ( 77 | Direction.SHORT 78 | if direction == Direction.LONG 79 | else Direction.LONG) 80 | old_position_data = self.holdings[security][offset_direction] 81 | new_quantity = old_position_data.quantity - quantity 82 | if new_quantity > 0: 83 | new_total_value = ( 84 | old_position_data.holding_price * old_position_data.quantity 85 | - holding_price * quantity 86 | ) 87 | new_holding_price = new_total_value / new_quantity 88 | self.holdings[security][offset_direction] = PositionData( 89 | security=security, 90 | direction=offset_direction, 91 | holding_price=new_holding_price, 92 | quantity=new_quantity, 93 | update_time=update_time 94 | ) 95 | else: 96 | self.holdings[security].pop(offset_direction, None) 97 | if len(self.holdings[security]) == 0: 98 | self.holdings.pop(security, None) 99 | 100 | def get_position( 101 | self, 102 | security: Stock, 103 | direction: Direction 104 | ) -> PositionData: 105 | if security not in self.holdings: 106 | return None 107 | elif direction not in self.holdings[security]: 108 | return None 109 | return self.holdings[security][direction] 110 | 111 | def get_all_positions(self) -> List[PositionData]: 112 | positions = [] 113 | for security in self.holdings: 114 | if Direction.LONG in self.holdings[security]: 115 | positions.append(self.holdings[security][Direction.LONG]) 116 | if Direction.SHORT in self.holdings[security]: 117 | positions.append(self.holdings[security][Direction.SHORT]) 118 | return positions 119 | 120 | def __str__(self): 121 | position_str = "Position(\n" 122 | for security in self.holdings: 123 | for direction in [Direction.LONG, Direction.SHORT]: 124 | if self.holdings[security].get(direction) is not None: 125 | position_str += str(self.holdings[security][direction]) 126 | position_str += "\n" 127 | position_str += ")" 128 | return position_str 129 | __repr__ = __str__ 130 | -------------------------------------------------------------------------------- /qtrader/core/security.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/3/2021 12:05 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: security.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from dataclasses import dataclass 19 | 20 | from qtrader.core.constants import Exchange 21 | 22 | 23 | @dataclass(frozen=True) 24 | class Security: 25 | """Base class for different asset types""" 26 | code: str 27 | security_name: str 28 | lot_size: int = None 29 | exchange: Exchange = None 30 | expiry_date: str = None 31 | quote_currency: str = '' 32 | 33 | def __eq__(self, other): 34 | return ( 35 | self.code == other.code 36 | and self.security_name == other.security_name 37 | and self.exchange.value == other.exchange.value 38 | ) 39 | 40 | def __hash__(self): 41 | return hash(f"{self.security_name}|{self.code}|{self.exchange.value}") 42 | 43 | 44 | @dataclass(frozen=True) 45 | class Stock(Security): 46 | """Cash equity""" 47 | code: str 48 | security_name: str 49 | lot_size: int = 1 # default to 1 lot 50 | exchange: Exchange = Exchange.SEHK # default to HK market 51 | expiry_date = None 52 | 53 | def __post_init__(self): 54 | pass 55 | 56 | 57 | @dataclass(frozen=True) 58 | class Currency(Security): 59 | """Foreign exchange""" 60 | code: str 61 | security_name: str 62 | lot_size: int = 1000 # default to 1000 63 | exchange: Exchange = Exchange.IDEALPRO # default to IDEALPRO 64 | expiry_date = None 65 | 66 | def __post_init__(self): 67 | pass 68 | 69 | 70 | @dataclass(frozen=True) 71 | class Commodity(Security): 72 | """Commodity""" 73 | code: str 74 | security_name: str 75 | lot_size: int = 1000 # default to 1000 76 | exchange: Exchange = Exchange.IDEALPRO # default to IDEALPRO 77 | expiry_date = None 78 | 79 | def __post_init__(self): 80 | pass 81 | 82 | 83 | @dataclass(frozen=True) 84 | class Futures(Security): 85 | """Futures""" 86 | code: str 87 | security_name: str 88 | lot_size: int = 1000 # default to 1000 89 | exchange: Exchange = Exchange.SMART # default to SMART 90 | expiry_date = "" 91 | 92 | def __post_init__(self): 93 | pass 94 | -------------------------------------------------------------------------------- /qtrader/core/strategy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 10:37 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: strategy.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from typing import List, Dict, Any 19 | from datetime import datetime 20 | from functools import wraps 21 | 22 | from qtrader.core.balance import AccountBalance 23 | from qtrader.core.data import Bar 24 | from qtrader.core.engine import Engine 25 | from qtrader.core.portfolio import Portfolio 26 | from qtrader.core.position import Position 27 | from qtrader.core.security import Security 28 | 29 | 30 | def init_portfolio_and_params(init_strategy): 31 | @wraps(init_strategy) 32 | def wrapper(self, *args, **kwargs): 33 | # initialize portfolio (account balance and position) and params 34 | self._init_strategy() 35 | # custom strategy initialization function 36 | result = init_strategy(self, *args, **kwargs) 37 | return result 38 | return wrapper 39 | 40 | class BaseStrategy: 41 | """Base class for strategy 42 | 43 | To write a strategy, override init_strategy and on_bar methods 44 | """ 45 | 46 | def __init__( 47 | self, 48 | securities: Dict[str, List[Security]], 49 | strategy_account: str, 50 | strategy_version: str, 51 | engine: Engine, 52 | strategy_trading_sessions: List[List[datetime]] = None, 53 | init_strategy_portfolios: Dict[str, List[Portfolio]] = None, 54 | init_strategy_params: Dict[str, Dict[str, Dict]] = None, 55 | reporting_currency: str = '' 56 | ): 57 | self.securities = securities 58 | self.engine = engine 59 | self.strategy_account = strategy_account 60 | self.strategy_version = strategy_version 61 | self.strategy_trading_sessions = strategy_trading_sessions 62 | self.reporting_currency = reporting_currency 63 | # portfolios 64 | if init_strategy_portfolios is None: 65 | init_strategy_portfolios = {} 66 | for gateway_name in securities: 67 | init_strategy_portfolios[gateway_name] = Portfolio( 68 | account_balance=AccountBalance(cash=0), 69 | position=Position(), 70 | market=engine.gateways[gateway_name] 71 | ) 72 | self.portfolios = init_strategy_portfolios 73 | # parameters 74 | if init_strategy_params is None: 75 | init_strategy_params = {gw: {} for gw in securities} 76 | self._init_strategy_params = init_strategy_params 77 | # Record the action at each time step 78 | self._actions = {gateway_name: "" for gateway_name in engine.gateways} 79 | # Record bar data at each time step 80 | self._data = { 81 | gateway_name: { 82 | security: None for security in securities.get(gateway_name, []) 83 | } for gateway_name in engine.gateways 84 | } 85 | 86 | def _init_strategy(self): 87 | """Initialize portfolio information for a specific strategy""" 88 | init_strategy_params = self._init_strategy_params 89 | self.params = {} 90 | for gateway_name in self.securities: 91 | self.params[gateway_name] = {} 92 | for sec_code in init_strategy_params[gateway_name]: 93 | self.params[gateway_name][sec_code] = {} 94 | for param in init_strategy_params[gateway_name][sec_code]: 95 | self.params[gateway_name][sec_code][param] = \ 96 | init_strategy_params[gateway_name][sec_code][param] 97 | 98 | @init_portfolio_and_params 99 | def init_strategy(self, *args, **kwargs): 100 | raise NotImplementedError( 101 | "init_strategy has not been implemented yet.") 102 | 103 | def update_bar(self, gateway_name: str, security: Security, data: Bar): 104 | self._data[gateway_name][security] = data 105 | 106 | def on_bar(self, cur_data: Dict[str, Dict[Security, Bar]]): 107 | raise NotImplementedError("on_bar has not been implemented yet.") 108 | 109 | def on_tick(self): 110 | raise NotImplementedError("on_tick has not been implemented yet.") 111 | 112 | def get_datetime(self, gateway_name: str) -> datetime: 113 | return self.engine.gateways[gateway_name].market_datetime 114 | @property 115 | def strategy_portfolio_value(self) -> float: 116 | """Convert gateway portfolio value to strategy reporting currency, and aggregate the portfolio value""" 117 | total_pv = 0 118 | for gw in self.engine.gateways: 119 | gw_pv = self.portfolios[gw].value # self.get_strategy_portfolio_value(gw) 120 | fx_rate = self.engine.gateways[gw].get_exchange_rate( 121 | base=self.reporting_currency, 122 | quote=self.portfolios[gw].reporting_currency 123 | ) 124 | total_pv += gw_pv / fx_rate 125 | return total_pv 126 | 127 | def get_portfolio_value(self, gateway_name: str) -> float: 128 | return self.portfolios[gateway_name].value 129 | 130 | def get_account_balance(self, gateway_name: str) -> AccountBalance: 131 | return self.portfolios[gateway_name].account_balance 132 | 133 | def get_position(self, gateway_name: str) -> Position: 134 | return self.portfolios[gateway_name].position 135 | 136 | def get_action(self, gateway_name: str) -> str: 137 | return self._actions[gateway_name] 138 | 139 | def get_open(self, gateway_name: str) -> List[float]: 140 | opens = [] 141 | for g in self.engine.gateways: 142 | if g == gateway_name: 143 | for security in self.securities[gateway_name]: 144 | opens.append(self._data[gateway_name][security].open) 145 | return opens 146 | 147 | def get_high(self, gateway_name: str) -> List[float]: 148 | highs = [] 149 | for g in self.engine.gateways: 150 | if g == gateway_name: 151 | for security in self.securities[gateway_name]: 152 | highs.append(self._data[gateway_name][security].high) 153 | return highs 154 | 155 | def get_low(self, gateway_name: str) -> List[float]: 156 | lows = [] 157 | for g in self.engine.gateways: 158 | if g == gateway_name: 159 | for security in self.securities[gateway_name]: 160 | lows.append(self._data[gateway_name][security].low) 161 | return lows 162 | 163 | def get_close(self, gateway_name: str) -> List[float]: 164 | closes = [] 165 | for g in self.engine.gateways: 166 | if g == gateway_name: 167 | for security in self.securities[gateway_name]: 168 | closes.append(self._data[gateway_name][security].close) 169 | return closes 170 | 171 | def get_volume(self, gateway_name: str) -> List[float]: 172 | volumes = [] 173 | for g in self.engine.gateways: 174 | if g == gateway_name: 175 | for security in self.securities[gateway_name]: 176 | volumes.append(self._data[gateway_name][security].volume) 177 | return volumes 178 | 179 | def reset_action(self, gateway_name: str): 180 | self._actions[gateway_name] = "" 181 | 182 | def update_action(self, gateway_name: str, action: Dict[str, Any]): 183 | self._actions[gateway_name] += str(action) 184 | self._actions[gateway_name] += "|" 185 | -------------------------------------------------------------------------------- /qtrader/core/utility.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 8:52 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: utility.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | import csv 18 | from functools import wraps 19 | from timeit import default_timer as timer 20 | from datetime import datetime 21 | from datetime import time as Time 22 | import threading 23 | import queue 24 | from typing import Any, List 25 | 26 | import func_timeout 27 | 28 | 29 | class BlockingDict(object): 30 | """Blocking dict can be used to store orders and deals in the gateway 31 | 32 | Ref: https://stackoverflow.com/questions/26586328/blocking-dict-in-python 33 | """ 34 | 35 | def __init__(self): 36 | self.queue = {} 37 | self.cv = threading.Condition() 38 | self.count = 0 39 | 40 | def put(self, key, value): 41 | with self.cv: 42 | self.queue[key] = value 43 | self.cv.notify_all() 44 | 45 | def pop(self) -> Any: 46 | with self.cv: 47 | while not self.queue: 48 | self.cv.wait() 49 | return self.queue.popitem() 50 | 51 | def get(self, key, timeout: float = None, default_item: Any = None) -> Any: 52 | with self.cv: 53 | while key not in self.queue: 54 | if not self.cv.wait(timeout): 55 | return default_item 56 | return self.queue.get(key) 57 | 58 | def __iter__(self): 59 | return self 60 | 61 | def __next__(self): 62 | with self.cv: 63 | if self.count == len(self.queue): 64 | self.count = 0 65 | raise StopIteration 66 | self.count += 1 67 | return list(self.queue.keys())[self.count - 1] 68 | 69 | 70 | class DefaultQueue: 71 | """Default Queue returns a default value if not available""" 72 | 73 | def __init__(self, *args, **kwargs): 74 | self._queue = queue.Queue(*args, **kwargs) 75 | 76 | def qsize(self): 77 | return self._queue.qsize() 78 | 79 | def empty(self): 80 | return self._queue.empty() 81 | 82 | def full(self): 83 | return self._queue.full() 84 | 85 | def put( 86 | self, item: Any, 87 | block: bool = True, 88 | timeout: float = None, 89 | raise_error: bool = False 90 | ): 91 | try: 92 | self._queue.put(item, block, timeout) 93 | except queue.Full: 94 | if raise_error: 95 | raise queue.Full 96 | 97 | def get( 98 | self, 99 | block: bool = True, 100 | timeout: float = None, 101 | default_item: Any = None, 102 | raise_error: bool = False 103 | ) -> Any: 104 | try: 105 | return self._queue.get(block, timeout) 106 | except queue.Empty: 107 | if raise_error: 108 | raise queue.Empty 109 | else: 110 | return default_item 111 | 112 | 113 | def timeit(func): 114 | """Measure execution time of a function""" 115 | 116 | @wraps(func) 117 | def wrapper(*args, **kwargs): 118 | tic = timer() 119 | res = func(*args, **kwargs) 120 | toc = timer() 121 | print( 122 | "{0} Elapsed time: {1:.3f} seconds".format( 123 | func.__name__, 124 | toc - tic)) 125 | return res 126 | return wrapper 127 | 128 | 129 | def safe_call(func): 130 | """Safe call 131 | 132 | Try to call a function. If encounter error, just give warning and skip, 133 | will not interrupt the program. 134 | """ 135 | 136 | @wraps(func) 137 | def wrapper(*args, **kwargs): 138 | try: 139 | res = func(*args, **kwargs) 140 | return res 141 | except Exception as e: 142 | return e 143 | return wrapper 144 | 145 | 146 | def try_parsing_datetime( 147 | text: str, 148 | default: datetime = None 149 | ) -> datetime: 150 | """Parsing different datetime format string, if can not be parsed, return 151 | the default datetime(default is set to now). 152 | """ 153 | dt_formats = ( 154 | "%Y-%m-%d %H:%M:%S.%f", 155 | "%Y-%m-%d %H:%M:%S", 156 | "%Y%m%d %H:%M:%S", 157 | "%m/%d/%Y %H:%M:%S", 158 | "%m/%d/%Y %H:%M", 159 | "%Y%m%d %H:%M:%S Asia/Hong_Kong", 160 | "%Y%m%d %H:%M:%S Asia/Shanghai", 161 | "%Y%m%d %H:%M:%S US/Eastern", 162 | "%Y%m%d", 163 | "%Y%m%d Asia/Hong_Kong", 164 | "%Y%m%d Asia/Shanghai", 165 | "%Y%m%d US/Eastern" 166 | ) 167 | for fmt in dt_formats: 168 | try: 169 | return datetime.strptime(text, fmt) 170 | except ValueError: 171 | pass 172 | print(f"{text} is not a valid date format! Return default datetime instead.") 173 | if default is None: 174 | return datetime.now() 175 | elif isinstance(default, datetime): 176 | return default 177 | raise ValueError( 178 | f"default = {default} is of type {type(default)}, only datetime is " 179 | "valid.") 180 | 181 | 182 | def cast_value( 183 | value: Any, 184 | if_: Any = None, 185 | then: Any = None 186 | ) -> Any: 187 | """Cast a value to `then` if it is equal to `if_`, else return the value 188 | itself.""" 189 | if value == if_: 190 | return then 191 | else: 192 | return value 193 | 194 | 195 | def get_kline_dfield_from_seconds(time_step: int) -> str: 196 | """Get kline dfield (names) from given time steps (note: maximum interval 197 | is day) """ 198 | 199 | if time_step < 60: 200 | return f"K_{time_step}S" 201 | elif time_step < 3600: 202 | assert time_step % 60 == 0, ( 203 | "Given timestep should be multiple of 60 seconds, but " 204 | f"{time_step}*1,000 was given." 205 | ) 206 | time_step_in_mins = int(time_step / 60) 207 | return f"K_{time_step_in_mins}M" 208 | elif time_step < 3600 * 24: 209 | assert time_step % 3600 == 0, ( 210 | "Given timestep should be multiple of 3600 seconds, but " 211 | f"{time_step}*1,000 was given." 212 | ) 213 | time_step_in_hours = int(time_step / 3600) 214 | return f"K_{time_step_in_hours}H" 215 | else: 216 | assert time_step == 3600 * 24, ( 217 | "Given timestep can not exceed 3600*24 seconds, but " 218 | f"{time_step}*1,000 was given." 219 | ) 220 | return "K_1D" 221 | 222 | 223 | def run_function(function, args, kwargs, max_wait, default_value): 224 | """Run a function with limited execution time 225 | 226 | Ref: How to Limit the Execution Time of a Function Call? 227 | https://blog.finxter.com/how-to-limit-the-execution-time-of-a-function-call/ 228 | """ 229 | 230 | try: 231 | return func_timeout.func_timeout(max_wait, function, args, kwargs) 232 | except func_timeout.FunctionTimedOut: 233 | pass 234 | return default_value 235 | 236 | 237 | def is_trading_time( 238 | cur_time: Time, 239 | trading_sessions: List[datetime] 240 | ) -> bool: 241 | """Check whether the security is whitin trading time""" 242 | 243 | _is_trading_time = False 244 | for session in trading_sessions: 245 | if session[0].time() <= session[1].time(): 246 | if session[0].time() <= cur_time <= session[1].time(): 247 | _is_trading_time = True 248 | break 249 | elif session[0].time() > session[1].time(): 250 | if session[0].time() <= cur_time <= Time(23, 59, 59, 999999): 251 | _is_trading_time = True 252 | break 253 | elif Time(0, 0, 0) <= cur_time <= session[1].time(): 254 | _is_trading_time = True 255 | break 256 | return _is_trading_time 257 | 258 | 259 | def read_row_from_csv(filename: str, row: int) -> List[str]: 260 | """""" 261 | with open(filename, 'r') as file: 262 | reader = csv.reader(file) 263 | n = 0 264 | while n < row: 265 | data = next(reader) 266 | n += 1 267 | return data 268 | 269 | if __name__ == "__main__": 270 | blockdict = BlockingDict() 271 | blockdict.put(1, "a") 272 | blockdict.put(2, "b") 273 | for bd in blockdict: 274 | print(bd, blockdict.get(bd)) 275 | for bd in blockdict: 276 | print(bd, blockdict.get(bd)) 277 | print(3, blockdict.get(3, timeout=1, default_item="cc")) 278 | -------------------------------------------------------------------------------- /qtrader/gateways/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 8:57 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | # from pywintypes import com_error 18 | from .base_gateway import BaseGateway 19 | from .backtest import BacktestGateway 20 | 21 | try: 22 | from .futu import FutuGateway, FutuFuturesGateway 23 | except ImportError as e: 24 | print(f"Warning: {e.__class__}: {e.msg}") 25 | 26 | try: 27 | from .ib import IbGateway 28 | except ImportError as e: 29 | print(f"Warning: {e.__class__}: {e.msg}") 30 | 31 | # try: 32 | # from .cqg import CqgGateway 33 | # except (ImportError, com_error) as e: 34 | # print(f"Warning: {e.__class__}: {e}") 35 | 36 | try: 37 | from .cqg import CqgGateway 38 | except Exception as e: 39 | print(f"Warning: {e.__class__}: {e}") 40 | -------------------------------------------------------------------------------- /qtrader/gateways/backtest/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 8:57 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from .backtest_gateway import BacktestGateway 19 | from .backtest_gateway import BacktestFees -------------------------------------------------------------------------------- /qtrader/gateways/base_gateway.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 18/3/2021 1:24 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: base_gateway.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | import os 19 | from pathlib import Path 20 | from datetime import datetime 21 | from datetime import time as Time 22 | from abc import ABC 23 | from typing import List, Dict 24 | import warnings 25 | 26 | import yaml 27 | 28 | from qtrader.core.balance import AccountBalance 29 | from qtrader.core.constants import Direction, TradeMode 30 | from qtrader.core.data import Bar, Quote, OrderBook 31 | from qtrader.core.deal import Deal 32 | from qtrader.core.order import Order 33 | from qtrader.core.position import PositionData 34 | from qtrader.core.security import Security 35 | from qtrader.core.utility import BlockingDict 36 | from qtrader.core.utility import is_trading_time 37 | from qtrader_config import GATEWAYS 38 | 39 | 40 | class BaseGateway(ABC): 41 | """ 42 | Abstract gateway class for creating gateways connection 43 | to different trading systems. 44 | """ 45 | broker_name = "" 46 | broker_account = "" 47 | trading_sessions = {} 48 | 49 | def __init__( 50 | self, 51 | securities: List[Security], 52 | gateway_name: str = "Backtest", 53 | **kwargs 54 | ): 55 | """ 56 | Base gateway 57 | :param securities: securities 58 | :param gateway_name: name of the gateway 59 | :param kwargs: 60 | """ 61 | self._market_datetime = None 62 | self._trade_mode = None 63 | self.securities = securities 64 | assert gateway_name in GATEWAYS, ( 65 | f"{gateway_name} is NOT in GATEWAYS, please check your config file!" 66 | ) 67 | self.gateway_name = gateway_name 68 | self.broker_account = GATEWAYS[gateway_name]["broker_name"] 69 | self.broker_account = GATEWAYS[gateway_name]["broker_account"] 70 | self.orders = BlockingDict() 71 | self.deals = BlockingDict() 72 | self.quote = BlockingDict() 73 | self.orderbook = BlockingDict() 74 | 75 | # If trading sessions are not specified explicitly, we load them from 76 | # yaml file 77 | if 'trading_sessions' in kwargs: 78 | trading_sessions = kwargs.get('trading_sessions') 79 | assert type(trading_sessions) == dict, ( 80 | "trading_sessions should be a dict: Dict[str, List].") 81 | self.trading_sessions = trading_sessions 82 | else: 83 | warnings.warn('This is deprecated, `trading_sessions` is mandatory in future verions.', 84 | DeprecationWarning, stacklevel=2) 85 | gateway_path = os.path.dirname(os.path.realpath(__file__)) 86 | if "instrument_cfg.yaml" not in os.listdir(Path(gateway_path)): 87 | raise ValueError( 88 | "trading_sessions are NOT specified in gateway, and " 89 | "instrument_cfg.yaml can NOT be found in " 90 | f"{os.listdir(Path(gateway_path))} either!") 91 | with open(Path(gateway_path).joinpath("instrument_cfg.yaml"), 92 | 'r', encoding='utf-8') as f: 93 | instrument_cfg = f.read() 94 | instrument_cfg = yaml.load( 95 | instrument_cfg, Loader=yaml.FullLoader) 96 | # check if info of all securities is available 97 | for security in securities: 98 | assert security.code in instrument_cfg, ( 99 | f"{security.code} is NOT available in " 100 | f"{Path(gateway_path).joinpath('instrument_cfg.yaml')}" 101 | ) 102 | self.trading_sessions[security.code] = instrument_cfg[ 103 | security.code]["sessions"] 104 | if 'currency_tickers' in kwargs: 105 | self.currencies = kwargs.get('currency_tickers') 106 | if 'trade_mode' in kwargs: 107 | self.trade_mode = kwargs.get('trade_mode') 108 | 109 | def close(self): 110 | """In backtest, no need to do anything.""" 111 | raise NotImplementedError("[close] is not implemented yet.") 112 | 113 | @property 114 | def market_datetime(self): 115 | """Current Market time.""" 116 | return self._market_datetime 117 | 118 | def get_exchange_rate(self, base: str, quote: str) -> float: 119 | """Exchange rate as of current market time.""" 120 | if base == quote: 121 | return 1.0 122 | if not getattr(self, 'currencies', False): 123 | raise NotImplementedError("[get_exchange_rate] `currency_tickers` has not yet been passed in to " 124 | f"{self.__class__} when initialising it.") 125 | for currency in self.currencies: 126 | asset_type, exchange, curncy = currency.code.split('.') 127 | if curncy not in (f'{base}{quote}', f'{quote}{base}'): 128 | continue 129 | if curncy == f'{base}{quote}': 130 | curncy_bar = self.get_recent_data( 131 | security=currency, 132 | cur_datetime=self.market_datetime 133 | ) 134 | if curncy_bar: 135 | return curncy_bar.close 136 | elif curncy == f'{quote}{base}': 137 | curncy_bar = self.get_recent_data( 138 | security=currency, 139 | cur_datetime=self.market_datetime 140 | ) 141 | if curncy_bar: 142 | return 1. / curncy_bar.close 143 | raise NotImplementedError(f"[get_exchange_rate] Either `{base}{quote}` or `{quote}{base}` shoule be included " 144 | f"in `currency_tickers` when initialising {self.__class__}.") 145 | 146 | def is_security_trading_time( 147 | self, 148 | security: Security, 149 | cur_time: Time 150 | ) -> bool: 151 | """whether the security is whitin trading time""" 152 | trading_sessions = self.trading_sessions[security.code] 153 | return is_trading_time(cur_time, trading_sessions) 154 | 155 | def is_trading_time(self, cur_datetime: datetime) -> bool: 156 | """Whether the gateway is within trading time (a gateway 157 | might have different securities that are of different 158 | trading hours) 159 | """ 160 | # Any security is in trading session, we return True 161 | _is_trading_time = False 162 | for security in self.securities: 163 | _is_trading_time = self.is_security_trading_time( 164 | security, cur_datetime.time()) 165 | if _is_trading_time: 166 | break 167 | return _is_trading_time 168 | 169 | @market_datetime.setter 170 | def market_datetime(self, value): 171 | """Set market time""" 172 | self._market_datetime = value 173 | 174 | def get_order(self, orderid): 175 | """Get order""" 176 | return self.orders.get(orderid) 177 | 178 | def find_deals_with_orderid(self, orderid: str) -> List[Deal]: 179 | """Find deals based on orderid""" 180 | found_deals = [] 181 | for dealid in self.deals: 182 | deal = self.deals.get(dealid) 183 | if deal.orderid == orderid: 184 | found_deals.append(deal) 185 | return found_deals 186 | 187 | def place_order(self, order: Order): 188 | """Place order""" 189 | raise NotImplementedError("[place_order] has not been implemented") 190 | 191 | def cancel_order(self, orderid): 192 | """Cancel order""" 193 | raise NotImplementedError("[cancel_order] has not been implemented") 194 | 195 | def get_broker_balance(self) -> AccountBalance: 196 | """Get broker balance""" 197 | raise NotImplementedError( 198 | "[get_broker_balance] has not been implemented") 199 | 200 | def get_broker_position( 201 | self, 202 | security: Security, 203 | direction: Direction 204 | ) -> PositionData: 205 | """Get broker position""" 206 | raise NotImplementedError( 207 | "[get_broker_position] has not been implemented") 208 | 209 | def get_all_broker_positions(self) -> List[PositionData]: 210 | """Get all broker positions""" 211 | raise NotImplementedError( 212 | "[get_all_broker_positions] has not been implemented") 213 | 214 | def get_all_orders(self) -> List[Order]: 215 | """Get all orders (sent by current algo)""" 216 | all_orders = [] 217 | for orderid, order in self.orders.queue.items(): 218 | order.orderid = orderid 219 | all_orders.append(order) 220 | return all_orders 221 | 222 | def get_all_deals(self) -> List[Deal]: 223 | """Get all deals (sent by current algo and got executed)""" 224 | all_deals = [] 225 | for dealid, deal in self.deals.queue.items(): 226 | deal.dealid = dealid 227 | all_deals.append(deal) 228 | return all_deals 229 | 230 | @property 231 | def trade_mode(self): 232 | return self._trade_mode 233 | 234 | @trade_mode.setter 235 | def trade_mode(self, trade_mode: TradeMode): 236 | self._trade_mode = trade_mode 237 | 238 | def get_quote(self, security: Security) -> Quote: 239 | """Get quote""" 240 | raise NotImplementedError("[get_quote] has not been implemented") 241 | 242 | def get_orderbook(self, security: Security) -> OrderBook: 243 | """Get orderbook""" 244 | raise NotImplementedError("[get_orderbook] has not been implemented") 245 | 246 | def req_historical_bars( 247 | self, 248 | security: Security, 249 | periods: int, 250 | freq: str, 251 | cur_datetime: datetime = None, 252 | daily_open_time: Time = None, 253 | daily_close_time: Time = None, 254 | ) -> List[Bar]: 255 | """request historical bar data.""" 256 | raise NotImplementedError( 257 | "[req_historical_bars] has not been implemented") 258 | 259 | def subscribe(self): 260 | """Subscribe market data (quote and orderbook, and ohlcv)""" 261 | raise NotImplementedError("[subscribe] has not been implemented") 262 | 263 | def unsubscribe(self): 264 | """Unsubscribe market data (quote and orderbook, and ohlcv)""" 265 | raise NotImplementedError("[unsubscribe] has not been implemented") 266 | 267 | 268 | class BaseFees: 269 | """Base class for fees""" 270 | commissions: float = 0 # Broker fee 271 | platform_fees: float = 0 # Broker fee 272 | system_fees: float = 0 # Exchange fee 273 | settlement_fees: float = 0 # Clearing fee 274 | stamp_fees: float = 0 # Government Stamp Duty 275 | trade_fees: float = 0 # Exchange Fee 276 | transaction_fees: float = 0 # (SFC) transaction levy 277 | total_fees: float = 0 278 | total_trade_amount: float = 0 279 | total_number_of_trades: float = 0 280 | -------------------------------------------------------------------------------- /qtrader/gateways/cqg/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 11/22/2021 11:32 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | from pywintypes import com_error 17 | 18 | try: 19 | from .cqg_gateway import CqgGateway 20 | except (ImportError, com_error) as e: 21 | print(f"{e.__class__}: {e}") 22 | 23 | try: 24 | from .cqg_fees import CQGFees 25 | except (ImportError, com_error) as e: 26 | print(f"{e.__class__}: {e}") -------------------------------------------------------------------------------- /qtrader/gateways/cqg/cqg_fees.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 9/15/2021 3:11 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: cqg_fees.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | 17 | from qtrader.core.deal import Deal 18 | from qtrader.gateways.base_gateway import BaseFees 19 | 20 | 21 | class CQGFees(BaseFees): 22 | """ 23 | CQG fee model 24 | """ 25 | 26 | def __init__(self, *deals: Deal): 27 | # Platform fees (to the platform) 28 | commissions = 0 29 | platform_fees = 0 30 | # Agency fees (to other parties such as exchange, tax authorities) 31 | system_fees = 0 32 | settlement_fees = 0 33 | stamp_fees = 0 34 | trade_fees = 0 35 | transaction_fees = 0 36 | 37 | for deal in deals: 38 | # price = deal.filled_avg_price 39 | quantity = deal.filled_quantity 40 | commissions += 1.92 * quantity # 1.92 per contract 41 | 42 | # Total fees 43 | total_fees = ( 44 | commissions 45 | + platform_fees 46 | + system_fees 47 | + settlement_fees 48 | + stamp_fees 49 | + trade_fees 50 | + transaction_fees 51 | ) 52 | 53 | self.commissions = commissions 54 | self.platform_fees = platform_fees 55 | self.system_fees = system_fees 56 | self.settlement_fees = settlement_fees 57 | self.stamp_fees = stamp_fees 58 | self.trade_fees = trade_fees 59 | self.transaction_fees = transaction_fees 60 | self.total_fees = total_fees 61 | -------------------------------------------------------------------------------- /qtrader/gateways/cqg/wrapper/CELEnvironment.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from threading import Thread 3 | 4 | import win32event 5 | import win32com.client 6 | from win32com.client import constants 7 | import pythoncom 8 | import sys 9 | 10 | win32com.client.gencache.EnsureModule('{51F35562-AEB4-4AB3-99E8-AC9666344B64}', 0, 4, 0) 11 | 12 | 13 | def AssertMessage(condition, message): 14 | if not condition: 15 | raise RuntimeError(message) 16 | 17 | 18 | def Trace(message): 19 | threadId = threading.current_thread().ident 20 | formattedMessage = str(threadId) + ': ' + message 21 | 22 | print(formattedMessage) 23 | sys.stdout.flush() 24 | 25 | 26 | # Class implements ICQGCELEvent interface. All sink classes used in examples should be inherited from this class. 27 | class CELSinkBase: 28 | def OnLineTimeChanged(self, newLineTime): 29 | pass 30 | 31 | def OnGWConnectionStatusChanged(self, newStatus): 32 | pass 33 | 34 | def OnDataConnectionStatusChanged(self, newStatus): 35 | pass 36 | 37 | def OnInstrumentSubscribed(self, symbol, cqgInstrument): 38 | pass 39 | 40 | def OnInstrumentChanged(self, cqgInstrument, cqgQuotes, cqgInstrumentProperties): 41 | pass 42 | 43 | def OnInstrumentDOMChanged(self, cqgInstrument, prevAsks, prevBids): 44 | pass 45 | 46 | def OnAccountChanged(self, changeType, cqgAccount, cqgPosition): 47 | pass 48 | 49 | def OnIsReady(self, readyStatus): 50 | pass 51 | 52 | def OnIdle(self): 53 | pass 54 | 55 | def OnDataError(self, cqgError, errorDescription): 56 | pass 57 | 58 | def OnCELStarted(self): 59 | pass 60 | 61 | def OnIncorrectSymbol(self, symbol): 62 | pass 63 | 64 | def OnCommodityInstrumentsResolved(self, commodityName, instrumentTypes, cqgCommodityInstruments): 65 | pass 66 | 67 | def OnGWEnvironmentChanged(self, eventCode, accountId, phase): 68 | pass 69 | 70 | def OnCurrencyRatesChanged(self, cqgCurrencyRates): 71 | pass 72 | 73 | def OnQueryProgress(self, cqgOrdersQuery, cqgError): 74 | pass 75 | 76 | def OnOrderChanged(self, changeType, cqgOrder, oldProperties, cqgFill, cqgError): 77 | pass 78 | 79 | def OnDataSourcesResolved(self, cqgDataSources, cqgError): 80 | pass 81 | 82 | def OnDataSourceSymbolsResolved(self, dataSourceAbbreviation, cqgDataSourceSymbols, cqgError): 83 | pass 84 | 85 | def OnCustomSessionsResolved(self, cqgSessionsCollection, cqgError): 86 | pass 87 | 88 | def OnTradableCommoditiesResolved(self, gwAccountId, cqgCommodities, cqgError): 89 | pass 90 | 91 | def OnTicksResolved(self, cqgTicks, cqgError): 92 | pass 93 | 94 | def OnTicksAdded(self, cqgTicks, added_ticks_count): 95 | pass 96 | 97 | def OnTicksRemoved(self, cqgTicks, removedTickIndex): 98 | pass 99 | 100 | def OnTimedBarsResolved(self, cqgTimedBars, cqgError): 101 | pass 102 | 103 | def OnTimedBarsAdded(self, cqgTimedBars): 104 | pass 105 | 106 | def OnTimedBarsUpdated(self, cqgTimedBars, index): 107 | pass 108 | 109 | def OnConstantVolumeBarsResolved(self, cqgConstantVolumeBars, cqgError): 110 | pass 111 | 112 | def OnConstantVolumeBarsAdded(self, cqgConstantVolumeBars): 113 | pass 114 | 115 | def OnConstantVolumeBarsUpdated(self, cqgConstantVolumeBars, index): 116 | pass 117 | 118 | def OnPointAndFigureBarsResolved(self, cqgPointAndFigureBars, cqgError): 119 | pass 120 | 121 | def OnPointAndFigureBarsAdded(self, cqgPointAndFigureBars): 122 | pass 123 | 124 | def OnPointAndFigureBarsUpdated(self, cqgPointAndFigureBars, index): 125 | pass 126 | 127 | def OnYieldsResolved(self, cqgYields, cqgError): 128 | pass 129 | 130 | def OnYieldsAdded(self, cqgYields): 131 | pass 132 | 133 | def OnYieldsUpdated(self, cqgYields, index): 134 | pass 135 | 136 | def OnTFlowBarsResolved(self, cqgTFlowBars, cqgError): 137 | pass 138 | 139 | def OnTFlowBarsAdded(self, cqgTFlowBars): 140 | pass 141 | 142 | def OnTFlowBarsUpdated(self, cqgTFlowBars, index): 143 | pass 144 | 145 | def OnCustomStudyDefinitionsResolved(self, cqgCustomStudyDefinitions, cqgError): 146 | pass 147 | 148 | def OnTradingSystemDefinitionsResolved(self, cqgCustomStudyDefinitions, cqgError): 149 | pass 150 | 151 | def OnConditionDefinitionsResolved(self, cqgConditionDefinitions, cqgError): 152 | pass 153 | 154 | def OnQFormulaDefinitionsResolved(self, cqgQFormulaDefinitions, cqgError): 155 | pass 156 | 157 | def OnCustomStudyResolved(self, cqgCustomStudy, cqgError): 158 | pass 159 | 160 | def OnCustomStudyAdded(self, cqgCustomStudy): 161 | pass 162 | 163 | def OnCustomStudyUpdated(self, cqgCustomStudy, index): 164 | pass 165 | 166 | def OnConditionResolved(self, cqgCondition, cqgError): 167 | pass 168 | 169 | def OnConditionAdded(self, cqgCondition): 170 | pass 171 | 172 | def OnConditionUpdated(self, cqgCondition, index): 173 | pass 174 | 175 | def OnTradingSystemResolved(self, cqgTradingSystem, cqgError): 176 | pass 177 | 178 | def OnTradingSystemAddNotification(self, cqgTradingSystem, cqgTradingSystemAddInfo): 179 | pass 180 | 181 | def OnTradingSystemUpdateNotification(self, cqgTradingSystem, cqgTradingSystemUpdateInfo): 182 | pass 183 | 184 | def OnExpressionResolved(self, cqgExpression, cqgError): 185 | pass 186 | 187 | def OnExpressionAdded(self, cqgExpression): 188 | pass 189 | 190 | def OnExpressionUpdated(self, cqgExpression, index): 191 | pass 192 | 193 | def OnAlgorithmicOrderRegistrationComplete(self, guid, cqgError): 194 | pass 195 | 196 | def OnAlgorithmicOrderPlaced(self, guid, mainParams, customProps): 197 | pass 198 | 199 | def OnTimedBarsInserted(self, cqgTimedBars, index): 200 | pass 201 | 202 | def OnTimedBarsRemoved(self, cqgTimedBars, index): 203 | pass 204 | 205 | def OnTonstantVolumeBarsInserted(self, cqgConstantVolumeBars, index): 206 | pass 207 | 208 | def OnTonstantVolumeBarsRemoved(self, cqgConstantVolumeBars, index): 209 | pass 210 | 211 | def OnPointAndFigureBarsInserted(self, cqgPointAndFigureBars, index): 212 | pass 213 | 214 | def OnPointAndFigureBarsRemoved(self, cqgPointAndFigureBars, index): 215 | pass 216 | 217 | def OnYieldsInserted(self, cqgYields, index): 218 | pass 219 | 220 | def OnYieldsRemoved(self, cqgYields, index): 221 | pass 222 | 223 | def OnTFlowBarsInserted(self, cqgTFlowBars, index): 224 | pass 225 | 226 | def OnTFlowBarsRemoved(self, cqgTFlowBars, index): 227 | pass 228 | 229 | def OnExpressionInserted(self, cqgExpression, index): 230 | pass 231 | 232 | def OnExpressionRemoved(self, cqgExpression, index): 233 | pass 234 | 235 | def OnTonditionInserted(self, cqgCondition, index): 236 | pass 237 | 238 | def OnTonditionRemoved(self, cqgCondition, index): 239 | pass 240 | 241 | def OnTustomStudyInserted(self, cqgCustomStudy, index): 242 | pass 243 | 244 | def OnTustomStudyRemoved(self, cqgCustomStudy, index): 245 | pass 246 | 247 | def OnICConnectionStatusChanged(self, newStatus): 248 | pass 249 | 250 | def OnAllOrdersCanceled(self, orderSide, gwAccountIds, instrumentNames): 251 | pass 252 | 253 | def OnInstrumentsGroupResolved(self, cqgInstrumentsGroup, cqgError): 254 | pass 255 | 256 | def OnInstrumentsGroupChanged(self, changeType, cqgInstrumentsGroup, instrumentsNames): 257 | pass 258 | 259 | def OnBarsTimestampsResolved(self, cqgBarsTimestamps, cqgError): 260 | pass 261 | 262 | def OnStrategyDefinitionProgress(self, cqgDefinition, cqgError): 263 | pass 264 | 265 | def OnTradingSystemInsertNotification(self, cqgTradingSystem, cqgTradingSystemInsertInfo): 266 | pass 267 | 268 | def OnTradingSystemRemoveNotification(self, cqgTradingSystem, cqgTradingSystemRemoveInfo): 269 | pass 270 | 271 | def OnTradingSystemTradeRelationAddNotification(self, cqgTradingSystem, cqgTradingSystemRelationAddInfo): 272 | pass 273 | 274 | def OnTradableExchangesResolved(self, gwAccountId, cqgExchanges, cqgError): 275 | pass 276 | 277 | def OnHistoricalSessionsResolved(self, cqgHistoricalSessions, cqgHistoricalSessionsRequest, cqgError): 278 | pass 279 | 280 | def OnSummariesStatementResolved(self, cqgSummariesStatement, cqgError): 281 | pass 282 | 283 | def OnPositionsStatementResolved(self, cqgPositionsStatement, cqgError): 284 | pass 285 | 286 | def OnManualFillUpdateResolved(self, cqgManualFillRequest, cqgError): 287 | pass 288 | 289 | def OnManualFillsResolved(self, cqgManualFills, cqgError): 290 | pass 291 | 292 | def OnManualFillChanged(self, cqgManualFill, modifyType): 293 | pass 294 | 295 | def OnAuthenticationStatusChanged(self, newStatus, cqgError): 296 | pass 297 | 298 | def OnPasswordChanged(self, requestStatus, cqgError): 299 | pass 300 | 301 | def OnAdvancedStudyDefinitionsResolved(self, cqgAdvancedStudyDefinitions, cqgError): 302 | pass 303 | 304 | def OnAdvancedStudyResolved(self, cqgAdvancedStudy, cqgError): 305 | pass 306 | 307 | def OnAdvancedStudyAdded(self, cqgAdvancedStudy): 308 | pass 309 | 310 | def OnAdvancedStudyUpdated(self, cqgAdvancedStudy, index): 311 | pass 312 | 313 | def OnAdvancedStudyInserted(self, cqgAdvancedStudy, index): 314 | pass 315 | 316 | def OnAdvancedStudyRemoved(self, cqgAdvancedStudy, index): 317 | pass 318 | 319 | def OnSubMinuteBarsResolved(self, cqgSubminuteBars, cqgError): 320 | pass 321 | 322 | def OnSubMinuteBarsAdded(self, cqgSubminuteBars): 323 | pass 324 | 325 | def OnSubMinuteBarsUpdated(self, cqgSubminuteBars, index): 326 | pass 327 | 328 | def OnSubMinuteBarsInserted(self, cqgSubminuteBars, index): 329 | pass 330 | 331 | def OnSubMinuteBarsRemoved(self, cqgSubminuteBars, index): 332 | pass 333 | 334 | def OnInstrumentResolved(self, symbol, cqgInstrument, cqgError): 335 | pass 336 | 337 | def OnStrategyQuoteRequestResolved(self, cqgRFQ, isProcessed, cqgError): 338 | pass 339 | 340 | 341 | # Auxiliary class 342 | class SinkInternal(CELSinkBase): 343 | def __init__(self): 344 | Trace("Internal sink ctor") 345 | 346 | def Init(self, eventCELStarted, celEnvironment): 347 | self.eventCELStarted = eventCELStarted 348 | self.celEnvironment = celEnvironment 349 | 350 | def OnCELStarted(self): 351 | Trace("SinkInternal: CELStarted!") 352 | self.eventCELStarted.set() 353 | 354 | def OnDataError(self, cqgError, errorDescription): 355 | if cqgError is not None: 356 | dispatchedCQGError = win32com.client.Dispatch(cqgError) 357 | Trace("SinkInternal: OnDataError: Code: {} Description: {}".format(dispatchedCQGError.Code, 358 | dispatchedCQGError.Description)) 359 | 360 | self.celEnvironment.SetError() 361 | self.eventCELStarted.set() 362 | 363 | 364 | # Class prepares environment for comfortable using of CQG API in python 365 | class CELEnvironment: 366 | def __init__(self): 367 | self.eventCELStarted = threading.Event() 368 | self.eventAllPrepared = threading.Event() 369 | self.shutdownEvent = win32event.CreateEvent(None, 0, 0, None) 370 | self.errorHappened = False 371 | 372 | def Init(self, sinkType, customAPIConfiguration): 373 | self.thread = Thread(target=self.threadedFunction, args=(sinkType,), name="cel_threadfunc") 374 | self.thread.start() 375 | 376 | Trace("Waiting for CQGCEL creation..") 377 | self.eventAllPrepared.wait() 378 | Trace("CQGCEL is created!") 379 | 380 | Trace("Marshaled CQGCEL from stream") 381 | self.cqgCEL = win32com.client.Dispatch( 382 | pythoncom.CoGetInterfaceAndReleaseStream(self.cqgCELStream, pythoncom.IID_IDispatch)) 383 | Trace("CQGCEL is marshaled from stream!") 384 | 385 | self.apiConfigurationSet(customAPIConfiguration) 386 | 387 | self.startupCel() 388 | 389 | if self.errorHappened: 390 | Trace("Error happened during CQGCEL start up!") 391 | 392 | return self.sink 393 | 394 | def threadedFunction(self, sinkType): 395 | Trace("COM framework is initializing...") 396 | pythoncom.CoInitialize() 397 | Trace("COM framework is initialized!") 398 | 399 | Trace("CQG.CQGCEL.4 creation...") 400 | cqgCEL = win32com.client.Dispatch("CQG.CQGCEL.4") 401 | Trace("CQG.CQGCEL.4 created!") 402 | 403 | Trace("Marshal CQGCEL in stream") 404 | self.cqgCELStream = pythoncom.CoMarshalInterThreadInterfaceInStream(pythoncom.IID_IDispatch, cqgCEL) 405 | Trace("CQGCEL is marshaled in stream") 406 | 407 | Trace("Internal sink subscription to events") 408 | sinkInternal = win32com.client.WithEvents(cqgCEL, SinkInternal) 409 | Trace("Internal sink subscribed") 410 | 411 | Trace("Sink subscription to events") 412 | self.sink = win32com.client.WithEvents(cqgCEL, sinkType) 413 | Trace("Sink subscribed") 414 | 415 | sinkInternal.Init(self.eventCELStarted, self) 416 | Trace("Internal sink is initialized") 417 | 418 | self.eventAllPrepared.set() 419 | 420 | Trace("Starting message pumping..") 421 | 422 | self.pumpMessages() 423 | 424 | Trace("COM framework is uninitializing...") 425 | pythoncom.CoUninitialize() 426 | Trace("COM framework is uninitialized!") 427 | 428 | def pumpMessages(self): 429 | 430 | while True: 431 | res = win32event.MsgWaitForMultipleObjects((self.shutdownEvent,), 0, win32event.INFINITE, 432 | win32event.QS_ALLEVENTS) 433 | 434 | if res == win32event.WAIT_OBJECT_0: 435 | break 436 | elif res == win32event.WAIT_OBJECT_0 + 1: 437 | if pythoncom.PumpWaitingMessages(): 438 | break # wm_quit 439 | else: 440 | raise RuntimeError("unexpected win32wait return value") 441 | 442 | def apiConfigurationSet(self, customAPIConfiguration): 443 | Trace("CQGCEL default configuring...") 444 | self.cqgCEL.APIConfiguration.CollectionsThrowException = False 445 | self.cqgCEL.APIConfiguration.NewInstrumentChangeMode = True 446 | self.cqgCEL.APIConfiguration.ReadyStatusCheck = 0 447 | self.cqgCEL.APIConfiguration.TimeZoneCode = constants.tzChina # constants.tzCentral 448 | self.cqgCEL.APIConfiguration.IncludeOrderTransactions = True 449 | self.cqgCEL.APIConfiguration.UseOrderSide = True 450 | # self.cqgCEL.APIConfiguration.NewInstrumentMode = True 451 | 452 | if customAPIConfiguration is not None: 453 | Trace("CQGCEL custom configuring...") 454 | customAPIConfiguration(self.cqgCEL.APIConfiguration) 455 | 456 | def startupCel(self): 457 | Trace("Startup CQGCEL..") 458 | self.cqgCEL.Startup() 459 | Trace("Waiting for CQGCEL start..") 460 | self.eventCELStarted.wait() 461 | Trace("CQGCEL has started.") 462 | 463 | def Shutdown(self): 464 | Trace("CQGCEL is being shut down..") 465 | self.cqgCEL.Shutdown() 466 | win32event.SetEvent(self.shutdownEvent) 467 | 468 | self.thread.join() 469 | 470 | Trace("CQGCEL is shut down.") 471 | 472 | def SetError(self): 473 | self.errorHappened = True 474 | 475 | 476 | # Function prepares an environment for a sample starting 477 | # sampleClassType is a class type (not the instance) that implements the sample 478 | # customConfiguration is a function sets APIConfiguration properties in addition to CELEnvironment.apiConfigurationSet 479 | def Start(sampleClassType, customConfiguration = None): 480 | celEnvironment = CELEnvironment() 481 | try: 482 | sample = celEnvironment.Init(sampleClassType, customConfiguration) 483 | if not celEnvironment.errorHappened: 484 | sample.Init(celEnvironment) 485 | sample.Start() 486 | except Exception as e: 487 | Trace("Exception: {}".format(str(e))) 488 | finally: 489 | celEnvironment.Shutdown() 490 | -------------------------------------------------------------------------------- /qtrader/gateways/cqg/wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 11/22/2021 5:48 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | -------------------------------------------------------------------------------- /qtrader/gateways/futu/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 15/3/2021 4:55 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | try: 19 | from .futu_gateway import FutuGateway, FutuFuturesGateway 20 | except ImportError as e: 21 | print(f"{e.__class__}: {e.msg}") 22 | 23 | try: 24 | from .futu_fees import FutuFeesSEHK, FutuFeesHKFE 25 | except ImportError as e: 26 | print(f"{e.__class__}: {e.msg}") -------------------------------------------------------------------------------- /qtrader/gateways/futu/futu_fees.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 16/10/2021 11:18 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: futu_fees.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | import math 19 | 20 | from qtrader.core.deal import Deal 21 | from qtrader.gateways.base_gateway import BaseFees 22 | 23 | 24 | class FutuFeesSEHK(BaseFees): 25 | """ 26 | 港股融资融券(8332)套餐一(适合一般交易者) 27 | 融资利率: 年利率6.8% 28 | 29 | 佣金: 0.03%, 最低3港元 30 | 平台使用费: 15港元/笔 31 | 32 | 交易系统使用费(香港交易所): 每笔成交0.50港元 33 | 交收费(香港结算所): 0.002%, 最低2港元,最高100港元 34 | 印花税(香港政府): 0.13%*成交金额,不足1港元作1港元计,窝轮、牛熊证此费用不收取 (原来0.10%, 新制0.13%) 35 | 交易费(香港交易所): 0.005%*成交金额,最低0.01港元 36 | 交易征费(香港证监会): 0.0027*成交金额,最低0.01港元 37 | ----------------------- 38 | 港股融资融券(8332)套餐二(适合高频交易者) 39 | 融资利率: 年利率6.8% 40 | 41 | 佣金: 0.03%, 最低3港元 42 | 平台使用费: 阶梯式收费(以自然月计算) 43 | 每月累计订单 费用(港币/每笔订单) 44 | --------- ---------------- 45 | 1-5 30 46 | 6-20 15 47 | 21-50 10 48 | 51-100 9 49 | 101-500 8 50 | 501-1000 7 51 | 1001-2000 6 52 | 2001-3000 5 53 | 3001-4000 4 54 | 4001-5000 3 55 | 5001-6000 2 56 | 6001及以上 1 57 | 58 | 交易系统使用费(香港交易所): 每笔成交0.50港元 59 | 交收费(香港结算所): 0.002%, 最低2港元,最高100港元 60 | 印花税(香港政府): 0.13%*成交金额,不足1港元作1港元计,窝轮、牛熊证此费用不收取 (原来0.10%, 新制0.13%) 61 | 交易费(香港交易所): 0.005%*成交金额,最低0.01港元 62 | 交易征费(香港证监会): 0.0027*成交金额,最低0.01港元 63 | """ 64 | 65 | def __init__(self, *deals: Deal): 66 | for deal in deals: 67 | price = deal.filled_avg_price 68 | size = deal.filled_quantity 69 | trade_amount = price * size 70 | self.total_number_of_trades += 1 71 | self.total_trade_amount += trade_amount 72 | 73 | # 交易系统使用费(Exchange Fee) 74 | system_fee = round(0.50, 2) 75 | self.system_fees += system_fee 76 | 77 | # 交收费(CLearing Fee) 78 | settlement_fee = 0.00002 * trade_amount 79 | if settlement_fee < 2.0: 80 | settlement_fee = 2.0 81 | elif settlement_fee > 100.0: 82 | settlement_fee = 100.0 83 | settlement_fee = round(settlement_fee, 2) 84 | self.settlement_fees += settlement_fee 85 | 86 | # 印花税(Government Stamp Duty, applies only to stocks) 87 | stamp_fee = math.ceil(0.0013 * trade_amount) 88 | self.stamp_fees += stamp_fee 89 | 90 | # 交易费(Exchange Fee) 91 | trade_fee = max(0.00005 * trade_amount, 0.01) 92 | trade_fee = round(trade_fee, 2) 93 | self.trade_fees += trade_fee 94 | 95 | # 交易征费(SFC transaction levy, applies to stocks and warrrants) 96 | transaction_fee = max(0.000027 * trade_amount, 0.01) 97 | transaction_fee = round(transaction_fee, 2) 98 | self.transaction_fees += transaction_fee 99 | 100 | # 佣金(Hong Kong Fixed Commissions) 101 | self.commissions += max(0.0003 * self.total_trade_amount, 3) 102 | self.commissions = round(self.commissions, 2) 103 | 104 | # 平台使用费 105 | self.platform_fees = 15 106 | 107 | # 总费用 108 | self.total_fees = ( 109 | self.commissions 110 | + self.platform_fees 111 | + self.system_fees 112 | + self.settlement_fees 113 | + self.stamp_fees 114 | + self.trade_fees 115 | + self.transaction_fees 116 | ) 117 | 118 | 119 | class FutuFeesHKFE(BaseFees): 120 | """ 121 | 综合期货账户(6463) 122 | 123 | 佣金: 2港元/每张合约 124 | 平台使用费: 5港元/每张合约 125 | 126 | 交易系统使用费(香港交易所): N/A 127 | 交收费(香港结算所): N/A 128 | 印花税(香港政府): N/A 129 | 交易费(香港交易所): 3港元/每张合约 130 | 监管费(香港证监会): 0.1元/每张合约 131 | """ 132 | 133 | def __init__(self, *deals: Deal): 134 | for deal in deals: 135 | size = deal.filled_quantity 136 | lot = deal.security.lot_size 137 | num_of_contracts = int(size / lot) 138 | 139 | # 交易费(Exchange Fee) 140 | self.trade_fees += 3.0 * num_of_contracts 141 | 142 | # 监管费 143 | self.transaction_fees += 0.1 * num_of_contracts 144 | 145 | # 佣金(Hong Kong Fixed Commissions) 146 | self.commissions += 2.0 * num_of_contracts 147 | 148 | # 平台使用费 149 | self.platform_fees = 5.0 * num_of_contracts 150 | 151 | # 总费用 152 | self.total_fees = ( 153 | self.commissions 154 | + self.platform_fees 155 | + self.system_fees 156 | + self.settlement_fees 157 | + self.stamp_fees 158 | + self.trade_fees 159 | + self.transaction_fees 160 | ) 161 | -------------------------------------------------------------------------------- /qtrader/gateways/ib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 6/9/2021 3:53 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | try: 19 | from .ib_gateway import IbGateway 20 | except ImportError as e: 21 | print(f"{e.__class__}: {e.msg}") 22 | 23 | try: 24 | from .ib_fees import ( 25 | IbHKEquityFees, IbSHSZHKConnectEquityFees, IbUSFuturesFees) 26 | except ImportError as e: 27 | print(f"{e.__class__}: {e.msg}") 28 | -------------------------------------------------------------------------------- /qtrader/gateways/ib/ib_fees.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 16/10/2021 10:25 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: ib_fees.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | import math 19 | 20 | from qtrader.core.deal import Deal 21 | from qtrader.gateways.base_gateway import BaseFees 22 | 23 | 24 | class IbHKEquityFees(BaseFees): 25 | """ 26 | https://www.interactivebrokers.com.hk/en/index.php?f=1590 27 | 28 | Tier breaks and the applicable commission rates are provided on the IBKR website. In general, volume/value tiers are 29 | calculated *once daily*, not at the time of the trade. As such, execution reductions will start the trading day after 30 | the threshold has been exceeded. (https://ibkr.info/article/1197) 31 | 32 | - Government Stamp Duty and SFC Transaction Levy (https://ibkr.info/article/4017) 33 | - Exchange Fee and Clearing Fee (https://www.interactivebrokers.com.hk/en/index.php?f=1315&nhf=T) 34 | - Hong Kong Tier Commission And Fixed Commission (https://www.interactivebrokers.com.hk/en/index.php?f=49708) 35 | """ 36 | 37 | def __init__(self, *deals: Deal): 38 | for deal in deals: 39 | price = deal.filled_avg_price 40 | size = deal.filled_quantity 41 | trade_amount = price * size 42 | self.total_number_of_trades += 1 43 | self.total_trade_amount += trade_amount 44 | 45 | # Exchange Fee 46 | system_fee = round(0.50, 2) 47 | self.system_fees += system_fee 48 | 49 | # CLearing Fee 50 | settlement_fee = 0.00002 * trade_amount 51 | if settlement_fee < 2.0: 52 | settlement_fee = 2.0 53 | elif settlement_fee > 100.0: 54 | settlement_fee = 100.0 55 | settlement_fee = round(settlement_fee, 2) 56 | self.settlement_fees += settlement_fee 57 | 58 | # Government Stamp Duty, applies only to stocks 59 | stamp_fee = math.ceil(0.0013 * trade_amount) 60 | self.stamp_fees += stamp_fee 61 | 62 | # Exchange Fee 63 | trade_fee = max(0.00005 * trade_amount, 0.01) 64 | trade_fee = round(trade_fee, 2) 65 | self.trade_fees += trade_fee 66 | 67 | # SFC transaction levy, applies to stocks and warrrants 68 | transaction_fee = max(0.000027 * trade_amount, 0.01) 69 | transaction_fee = round(transaction_fee, 2) 70 | self.transaction_fees += transaction_fee 71 | 72 | # Hong Kong Fixed Commissions 73 | self.commissions += max(0.0008 * self.total_trade_amount, 18) 74 | self.commissions = round(self.commissions, 2) 75 | 76 | # Platform fee 77 | self.platform_fees = 0 78 | 79 | # Total fee 80 | self.total_fees = ( 81 | self.commissions 82 | + self.platform_fees 83 | + self.system_fees 84 | + self.settlement_fees 85 | + self.stamp_fees 86 | + self.trade_fees 87 | + self.transaction_fees) 88 | 89 | 90 | class IbSHSZHKConnectEquityFees(BaseFees): 91 | """ 92 | https://www.interactivebrokers.com.hk/en/index.php?f=1590 93 | 94 | Tier breaks and the applicable commission rates are provided on the IBKR website. In general, volume/value tiers are 95 | calculated *once daily*, not at the time of the trade. As such, execution reductions will start the trading day after 96 | the threshold has been exceeded. (https://ibkr.info/article/1197) 97 | 98 | - Government Stamp Duty and SFC Transaction Levy (https://ibkr.info/article/4017) 99 | - Exchange Fee and Clearing Fee (https://www.interactivebrokers.com.hk/en/index.php?f=11719&nhf=T) 100 | - Hong Kong Tier Commission And Fixed Commission (https://www.interactivebrokers.com.hk/en/index.php?f=49708) 101 | """ 102 | 103 | def __init__(self, *deals: Deal): 104 | for deal in deals: 105 | price = deal.filled_avg_price 106 | size = deal.filled_quantity 107 | trade_amount = price * size 108 | self.total_number_of_trades += 1 109 | self.total_trade_amount += trade_amount 110 | 111 | # Exchange Fee, security management 112 | system_fee = round(0.00002 * trade_amount, 2) 113 | self.system_fees += system_fee 114 | 115 | # CLearing Fee 116 | settlement_fee = round(0.00004 * trade_amount, 2) 117 | self.settlement_fees += settlement_fee 118 | 119 | # Sale proceeds Stamp Duty, applies only to stocks 120 | stamp_fee = round(0.001 * trade_amount, 2) 121 | self.stamp_fees += stamp_fee 122 | 123 | # Exchange Fee, handling fee 124 | trade_fee = round(0.0000487 * trade_amount, 2) 125 | self.trade_fees += trade_fee 126 | 127 | # SFC transaction levy, applies to stocks and warrrants 128 | transaction_fee = 0 129 | self.transaction_fees += transaction_fee 130 | 131 | # Hong Kong Fixed Commissions 132 | self.commissions += max(0.0008 * self.total_trade_amount, 18) 133 | self.commissions = round(self.commissions, 2) 134 | 135 | # Platform fee 136 | self.platform_fees = 0 137 | 138 | # Total fee 139 | self.total_fees = ( 140 | self.commissions 141 | + self.platform_fees 142 | + self.system_fees 143 | + self.settlement_fees 144 | + self.stamp_fees 145 | + self.trade_fees 146 | + self.transaction_fees) 147 | 148 | 149 | class IbUSFuturesFees(BaseFees): 150 | """ 151 | https://www.interactivebrokers.com.hk/en/index.php?f=1590&p=futures 152 | 153 | For simplicity, use 0.85 USD/contract 154 | """ 155 | 156 | def __init__(self, *deals: Deal): 157 | # Platform fees (to the platform) 158 | commissions = 0 159 | platform_fees = 0 160 | # Agency fees (to other parties such as exchange, tax authorities) 161 | system_fees = 0 162 | settlement_fees = 0 163 | stamp_fees = 0 164 | trade_fees = 0 165 | transaction_fees = 0 166 | 167 | for deal in deals: 168 | # price = deal.filled_avg_price 169 | quantity = deal.filled_quantity 170 | commissions += 0.85 * quantity # 1.92 per contract 171 | 172 | # Total fees 173 | total_fees = ( 174 | commissions 175 | + platform_fees 176 | + system_fees 177 | + settlement_fees 178 | + stamp_fees 179 | + trade_fees 180 | + transaction_fees 181 | ) 182 | 183 | self.commissions = commissions 184 | self.platform_fees = platform_fees 185 | self.system_fees = system_fees 186 | self.settlement_fees = settlement_fees 187 | self.stamp_fees = stamp_fees 188 | self.trade_fees = trade_fees 189 | self.transaction_fees = transaction_fees 190 | self.total_fees = total_fees 191 | -------------------------------------------------------------------------------- /qtrader/gateways/instrument_cfg.yaml: -------------------------------------------------------------------------------- 1 | FUT.GC: 2 | initial_margin: 7150 3 | contract_size: 100 4 | commission: 1.92 5 | sessions: 6 | - - 1970-01-01 7:00:00 7 | - 1970-01-01 5:00:00 8 | market_open: 1970-01-01 07:00:00 9 | market_close: 1970-01-01 05:00:00 10 | 11 | FUT.SI: 12 | initial_margin: 10450 13 | contract_size: 5000 14 | commission: 1.92 15 | sessions: 16 | - - 1970-01-01 7:00:00 17 | - 1970-01-01 5:00:00 18 | market_open: 1970-01-01 07:00:00 19 | market_close: 1970-01-01 05:00:00 20 | 21 | FUT.PL: 22 | initial_margin: 3575 23 | contract_size: 50 24 | commission: 1.92 25 | sessions: 26 | - - 1970-01-01 7:00:00 27 | - 1970-01-01 5:00:00 28 | market_open: 1970-01-01 07:00:00 29 | market_close: 1970-01-01 05:00:00 30 | 31 | FUT.PA: 32 | initial_margin: 21450 33 | contract_size: 100 34 | commission: 1.92 35 | sessions: 36 | - - 1970-01-01 7:00:00 37 | - 1970-01-01 5:00:00 38 | market_open: 1970-01-01 07:00:00 39 | market_close: 1970-01-01 05:00:00 40 | 41 | FUT.CO: 42 | initial_margin: 12180 43 | contract_size: 1000 44 | commission: 1.92 45 | sessions: 46 | - - 1970-01-01 9:00:00 47 | - 1970-01-01 5:00:00 48 | market_open: 1970-01-01 09:00:00 49 | market_close: 1970-01-01 05:00:00 50 | 51 | FUT.BZ: 52 | initial_margin: 12180 53 | contract_size: 1000 54 | commission: 1.92 55 | sessions: 56 | - - 1970-01-01 8:00:00 57 | - 1970-01-01 6:00:00 58 | market_open: 1970-01-01 08:00:00 59 | market_close: 1970-01-01 06:00:00 60 | 61 | FUT.CO_SPD: 62 | initial_margin: 12180 63 | contract_size: 1000 64 | commission: 1.92 65 | sessions: 66 | - - 1970-01-01 9:00:00 67 | - 1970-01-01 5:00:00 68 | market_open: 1970-01-01 09:00:00 69 | market_close: 1970-01-01 05:00:00 70 | 71 | HK.HHImain: 72 | initial_margin: 35590.40 73 | contract_size: 50 74 | commission: 10.1 75 | sessions: 76 | - - 1970-01-01 09:15:00 77 | - 1970-01-01 12:00:00 78 | - - 1970-01-01 13:00:00 79 | - 1970-01-01 16:30:00 80 | - - 1970-01-01 17:15:00 81 | - 1970-01-01 03:00:00 82 | market_open: 1970-01-01 09:15:00 83 | market_close: 1970-01-01 03:00:00 84 | 85 | HK.MCHmain: 86 | initial_margin: 35590.40 87 | contract_size: 10 88 | commission: 10.1 89 | sessions: 90 | - - 1970-01-01 09:15:00 91 | - 1970-01-01 12:00:00 92 | - - 1970-01-01 13:00:00 93 | - 1970-01-01 16:30:00 94 | - - 1970-01-01 17:15:00 95 | - 1970-01-01 03:00:00 96 | market_open: 1970-01-01 09:15:00 97 | market_close: 1970-01-01 03:00:00 98 | 99 | HK.MHImain: 100 | initial_margin: 19355.49 101 | contract_size: 10 102 | commission: 10.1 103 | sessions: 104 | - - 1970-01-01 09:15:00 105 | - 1970-01-01 12:00:00 106 | - - 1970-01-01 13:00:00 107 | - 1970-01-01 16:30:00 108 | - - 1970-01-01 17:15:00 109 | - 1970-01-01 03:00:00 110 | market_open: 1970-01-01 09:15:00 111 | market_close: 1970-01-01 03:00:00 112 | 113 | HK.TCHmain: 114 | initial_margin: 5860.51 115 | contract_size: 100 116 | commission: 10.1 117 | sessions: 118 | - - 1970-01-01 09:30:00 119 | - 1970-01-01 12:00:00 120 | - - 1970-01-01 13:00:00 121 | - 1970-01-01 16:00:00 122 | market_open: 1970-01-01 09:30:00 123 | market_close: 1970-01-01 16:00:00 124 | 125 | HK.ALBmain: 126 | initial_margin: 10771.40 127 | contract_size: 500 128 | commission: 10.1 129 | sessions: 130 | - - 1970-01-01 09:30:00 131 | - 1970-01-01 12:00:00 132 | - - 1970-01-01 13:00:00 133 | - 1970-01-01 16:00:00 134 | market_open: 1970-01-01 09:30:00 135 | market_close: 1970-01-01 16:00:00 136 | 137 | HK.MCAmain: 138 | initial_margin: 50000 139 | contract_size: 25 140 | commission: 10.1 141 | sessions: 142 | - - 1970-01-01 09:00:00 143 | - 1970-01-01 16:30:00 144 | - - 1970-01-01 17:15:00 145 | - 1970-01-01 03:00:00 146 | market_open: 1970-01-01 09:00:00 147 | market_close: 1970-01-01 03:00:00 148 | 149 | HK.MTWmain: 150 | initial_margin: 50000 151 | contract_size: 100 152 | commission: 10.1 153 | sessions: 154 | - - 1970-01-01 09:00:00 155 | - 1970-01-01 13:45:00 156 | - - 1970-01-01 14:30:00 157 | - 1970-01-01 03:00:00 158 | market_open: 1970-01-01 09:00:00 159 | market_close: 1970-01-01 03:00:00 160 | 161 | HK.HTImain: 162 | initial_margin: 43000 163 | contract_size: 50 164 | commission: 10.1 165 | sessions: 166 | - - 1970-01-01 09:15:00 167 | - 1970-01-01 12:00:00 168 | - - 1970-01-01 13:00:00 169 | - 1970-01-01 16:30:00 170 | - - 1970-01-01 17:15:00 171 | - 1970-01-01 03:00:00 172 | market_open: 1970-01-01 09:15:00 173 | market_close: 1970-01-01 03:00:00 174 | 175 | HK.GDUmain: 176 | initial_margin: 40000 177 | contract_size: 1000 178 | commission: 10.1 179 | sessions: 180 | - - 1970-01-01 08:30:00 181 | - 1970-01-01 16:30:00 182 | - - 1970-01-01 17:15:00 183 | - 1970-01-01 03:00:00 184 | market_open: 1970-01-01 08:30:00 185 | market_close: 1970-01-01 03:00:00 186 | 187 | HK.CUSmain: 188 | initial_margin: 18000 189 | contract_size: 100000 190 | commission: 10.1 191 | sessions: 192 | - - 1970-01-01 08:30:00 193 | - 1970-01-01 16:30:00 194 | - - 1970-01-01 17:15:00 195 | - 1970-01-01 03:00:00 196 | market_open: 1970-01-01 08:30:00 197 | market_close: 1970-01-01 03:00:00 -------------------------------------------------------------------------------- /qtrader/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 23/3/2021 6:51 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | -------------------------------------------------------------------------------- /qtrader/plugins/analysis/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 12:48 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from .performance import plot_pnl 19 | from .performance import PerformanceCTA 20 | -------------------------------------------------------------------------------- /qtrader/plugins/analysis/livetrade.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 4/22/2022 9:20 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: livemonitor.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | 17 | import os 18 | from pathlib import Path 19 | import matplotlib.pyplot as plt 20 | import ast 21 | 22 | import pandas as pd 23 | 24 | results_path = Path(os.path.abspath(__file__) 25 | ).parent.parent.parent.parent.joinpath("results") 26 | date = "2022-04-21" 27 | security_indices = [0, 1] 28 | 29 | qty = 2 30 | df_backtest = pd.read_csv(results_path.joinpath(f"{date} Backtest/result.csv")) 31 | df_backtest = df_backtest.sort_values(by="bar_datetime") 32 | df_backtest["portfolio_value"] = ( 33 | df_backtest["portfolio_value"] 34 | - df_backtest["portfolio_value"].iloc[0] 35 | ) 36 | df_backtest["portfolio_value"] = df_backtest["portfolio_value"] * qty 37 | df_backtest = df_backtest[ 38 | ["bar_datetime", 39 | "portfolio_value", 40 | "action", 41 | "close", 42 | "volume"] 43 | ] 44 | 45 | df_live = pd.read_csv(results_path.joinpath(f"{date} Live/result.csv")) 46 | df_live = df_live.sort_values(by="bar_datetime") 47 | df_live["portfolio_value"] = ( 48 | df_live["portfolio_value"] 49 | - df_live["portfolio_value"].iloc[0] 50 | ) 51 | df_live = df_live[ 52 | ["bar_datetime", 53 | "portfolio_value", 54 | "action", 55 | "close", 56 | "volume"] 57 | ] 58 | 59 | df = df_live.merge( 60 | df_backtest, 61 | on="bar_datetime", 62 | how="inner", 63 | suffixes=[ 64 | "_live", 65 | "_backtest"]) 66 | # df.set_index("bar_datetime", inplace=True) 67 | 68 | # compare portfolio value 69 | ax = df.plot('bar_datetime', 'portfolio_value_live', color="red", alpha=0.9) 70 | df.plot( 71 | 'bar_datetime', 72 | 'portfolio_value_backtest', 73 | ax=ax, 74 | color='green', 75 | alpha=0.8) 76 | plt.xticks(rotation=60) 77 | plt.tight_layout() 78 | plt.show() 79 | 80 | 81 | for security_index in security_indices: 82 | # compare close 83 | df_close = pd.concat([ 84 | df.bar_datetime, 85 | df.close_live.apply(lambda x: ast.literal_eval(x)[security_index]), 86 | df.close_backtest.apply(lambda x: ast.literal_eval(x)[security_index]) 87 | ], axis=1) 88 | df_close["close_diff"] = ( 89 | df_close["close_live"] 90 | - df_close["close_backtest"] 91 | ) 92 | ax = df_close.plot('bar_datetime', 'close_live', color="red", alpha=0.9) 93 | df_close.plot( 94 | 'bar_datetime', 95 | 'close_backtest', 96 | ax=ax, 97 | color='green', 98 | alpha=0.8) 99 | plt.xticks(rotation=60) 100 | ax1 = ax.twinx() 101 | df_close.plot('bar_datetime', 'close_diff', ax=ax1, color='orange') 102 | plt.tight_layout() 103 | plt.show() 104 | 105 | # compare volume 106 | df_volume = pd.concat([ 107 | df.bar_datetime, 108 | df.volume_live.apply(lambda x: ast.literal_eval(x)[security_index]), 109 | df.volume_backtest.apply(lambda x: ast.literal_eval(x)[security_index]) 110 | ], axis=1) 111 | df_volume["volume_diff"] = ( 112 | df_volume["volume_live"] 113 | - df_volume["volume_backtest"] 114 | ) 115 | 116 | ax = df_volume.plot('bar_datetime', 'volume_live', color="red", alpha=0.9) 117 | df_volume.plot( 118 | 'bar_datetime', 119 | 'volume_backtest', 120 | ax=ax, 121 | color='green', 122 | alpha=0.8) 123 | plt.xticks(rotation=60) 124 | ax1 = ax.twinx() 125 | df_volume.plot('bar_datetime', 'volume_diff', ax=ax1, color='orange') 126 | plt.tight_layout() 127 | plt.show() 128 | -------------------------------------------------------------------------------- /qtrader/plugins/analysis/metrics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 3/5/2022 11:01 am 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: metrics.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | 17 | from datetime import datetime 18 | 19 | import numpy as np 20 | import pandas as pd 21 | 22 | 23 | def convert_time(time_: str) -> str: 24 | """time_ in the format: %H:%M:%S""" 25 | hour, min, *sec = time_.split(":") 26 | if int(min) < 30: 27 | return f"{hour}:00:00" 28 | else: 29 | return f"{hour}:30:00" 30 | 31 | 32 | def holding_period(time_: pd.Series) -> float: 33 | """open_datetime and close_datetime are in the format of %Y-%m-%d %H:%M:%S 34 | ( 35 | time_['close_datetime'].astype('datetime64') 36 | - time_['open_datetime'].astype('datetime64')).apply(lambda x: 37 | x.total_seconds() / 60. 38 | ) 39 | """ 40 | begin_dt = datetime.strptime(time_["open_datetime"], "%Y-%m-%d %H:%M:%S") 41 | end_dt = datetime.strptime(time_["close_datetime"], "%Y-%m-%d %H:%M:%S") 42 | return (end_dt - begin_dt).total_seconds() / 60. 43 | 44 | 45 | def percentile(n: float): 46 | """Ref: https://stackoverflow.com/questions/17578115/pass-percentiles-to-pandas-agg-function""" 47 | def percentile_(x): 48 | return x.quantile(n / 100.) 49 | percentile_.__name__ = 'percentile_%s' % n 50 | return percentile_ 51 | 52 | 53 | def sharpe_ratio(returns: np.array, days: int = 252) -> float: 54 | volatility = returns.std() 55 | if volatility == 0: 56 | return np.nan 57 | sharpe_ratio = np.sqrt(days) * returns.mean() / volatility 58 | return sharpe_ratio 59 | 60 | 61 | def information_ratio( 62 | returns: np.array, 63 | benchmark_returns: np.array, 64 | days: int = 252 65 | ) -> float: 66 | return_difference = returns - benchmark_returns 67 | volatility = return_difference.std() 68 | if volatility == 0: 69 | return np.nan 70 | information_ratio = np.sqrt(days) * return_difference.mean() / volatility 71 | return information_ratio 72 | 73 | 74 | def modigliani_ratio(returns: np.array, benchmark_returns, days=252) -> float: 75 | volatility = returns.std() 76 | if volatility == 0: 77 | return np.nan 78 | sharpe_ratio = np.sqrt(days) * returns.mean() / volatility 79 | benchmark_volatility = benchmark_returns.std() 80 | m2_ratio = sharpe_ratio * benchmark_volatility 81 | return m2_ratio 82 | 83 | 84 | def rolling_maximum_drawdown( 85 | portfolio_value: np.array, 86 | window: int = 252 87 | ) -> float: 88 | """(default) use a trailing 252 trading day window 89 | portfolio_value: the *daily* portfolio values 90 | """ 91 | df = pd.Series(portfolio_value, name="pv").to_frame() 92 | # Calculate max drawdown in the past window days for each day in the 93 | # series. 94 | roll_max = df['pv'].rolling(window, min_periods=1).max() 95 | daily_drawdown = df['pv'] / roll_max - 1.0 96 | # Calculate the minimum (negative) daily drawdown in that window. 97 | max_daily_drawdown = daily_drawdown.rolling(window, min_periods=1).min() 98 | return max_daily_drawdown 99 | -------------------------------------------------------------------------------- /qtrader/plugins/clickhouse/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 23/3/2021 6:56 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | -------------------------------------------------------------------------------- /qtrader/plugins/clickhouse/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 23/3/2021 6:57 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: client.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from clickhouse_driver import Client 19 | 20 | from qtrader_config import CLICKHOUSE 21 | 22 | client = Client( 23 | host=CLICKHOUSE["host"], 24 | port=CLICKHOUSE["port"], 25 | user=CLICKHOUSE["user"], 26 | password=CLICKHOUSE["password"] 27 | ) 28 | 29 | sql = "show databases" 30 | 31 | ans = client.execute(sql) 32 | print(ans) 33 | client.disconnect() 34 | -------------------------------------------------------------------------------- /qtrader/plugins/monitor/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/5/2022 2:40 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | -------------------------------------------------------------------------------- /qtrader/plugins/monitor/livemonitor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/5/2022 2:52 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: livemonitor.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | import sys 17 | import os 18 | from datetime import datetime 19 | 20 | import pandas as pd 21 | import plotly.graph_objects as go 22 | from dash.dependencies import Input, Output 23 | import dash 24 | from pathlib import Path 25 | import pickle 26 | 27 | sys.path.insert(0, os.getcwd()) 28 | from monitor_config import instruments 29 | from qtrader_config import TIME_STEP 30 | from qtrader_config import LOCAL_PACKAGE_PATHS 31 | from qtrader_config import ADD_LOCAL_PACKAGE_PATHS_TO_SYSPATH 32 | if ADD_LOCAL_PACKAGE_PATHS_TO_SYSPATH: 33 | for pth in LOCAL_PACKAGE_PATHS: 34 | if pth not in sys.path: 35 | sys.path.insert(0, pth) 36 | from qtrader.plugins.analysis.performance import plot_signals 37 | 38 | if len(sys.argv) > 1: 39 | monitor_name = sys.argv[1] 40 | else: 41 | monitor_name = datetime.now().strftime("%Y%m%d") 42 | 43 | 44 | app = dash.Dash(__name__) 45 | app.layout = dash.html.Div( 46 | dash.html.Div([ 47 | dash.html.H4('Live Strategy Monitor'), 48 | dash.dcc.Dropdown( 49 | id='strategy_name', 50 | options=[{'label': k, 'value': k} for k in instruments], 51 | value=list(instruments.keys())[0] 52 | ), 53 | dash.dcc.Graph(id='live-update-graph'), 54 | dash.dcc.Interval( 55 | id='interval-component', 56 | interval=1 * TIME_STEP, # in milliseconds 57 | n_intervals=0 58 | ) 59 | ]) 60 | ) 61 | 62 | 63 | @app.callback(Output('live-update-graph', 'figure'), 64 | [Input('interval-component', 'n_intervals'), 65 | Input('strategy_name', 'value')]) 66 | def update_graph_live(n, strategy_name): 67 | home_dir = Path(os.getcwd()) 68 | data_path = home_dir.joinpath( 69 | f".qtrader_cache/livemonitor/{strategy_name}/{monitor_name}") 70 | if not os.path.exists(data_path): 71 | # Create a blank figure 72 | fig = go.Figure() 73 | # Add text annotation to the figure 74 | fig.add_annotation( 75 | x=0.5, # X-coordinate of the text (0.5 means centered horizontally) 76 | y=0.5, # Y-coordinate of the text (0.5 means centered vertically) 77 | text=f"{strategy_name}/{monitor_name} not exist", # Text to display 78 | showarrow=False, # Do not display an arrow 79 | font=dict(size=24) # Font size of the text 80 | ) 81 | # Update layout properties if needed (e.g., background color) 82 | fig.update_layout( 83 | plot_bgcolor='white', # Set background color to white 84 | ) 85 | return fig 86 | with open(data_path, "rb") as f: 87 | data = pd.DataFrame(pickle.load(f)) 88 | fig = plot_signals( 89 | data=data, 90 | instruments=instruments.get(f'{strategy_name}'), 91 | ) 92 | return fig 93 | 94 | 95 | if __name__ == "__main__": 96 | app.run_server(debug=True) 97 | -------------------------------------------------------------------------------- /qtrader/plugins/sqlite3/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 16/4/2021 3:46 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | from .db import DB 19 | -------------------------------------------------------------------------------- /qtrader/plugins/sqlite3/db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 16/4/2021 3:48 PM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: db.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | import threading 19 | import sqlite3 20 | from datetime import datetime 21 | from typing import Iterable, List, Dict, Union, Any 22 | 23 | import pandas as pd 24 | import numpy as np 25 | 26 | try: 27 | from qtrader_config import DB 28 | db_path = DB["sqlite3"] 29 | except BaseException: 30 | raise ValueError( 31 | "`sqlite3` is activated, and its path must be specified in " 32 | "`DB` variable in qtrader_config.py") 33 | 34 | lock = threading.Lock() 35 | 36 | 37 | class DB: 38 | 39 | def __init__(self): 40 | self.conn = sqlite3.connect(f"{db_path}/qtrader.db") 41 | self.cursor = self.conn.cursor() 42 | self.create_balance_table() 43 | self.create_position_table() 44 | self.create_order_table() 45 | self.create_deal_table() 46 | 47 | def close(self): 48 | with lock: 49 | self.cursor.close() 50 | self.conn.close() 51 | 52 | def commit(self): 53 | self.conn.commit() 54 | 55 | def execute(self, sql: str, parameters: Iterable = None): 56 | with lock: 57 | if parameters is None: 58 | self.cursor.execute(sql) 59 | self.commit() 60 | return 61 | self.cursor.execute(sql, parameters) 62 | self.commit() 63 | 64 | def _parse_sql_value(self, value: Union[str, float, int, datetime]): 65 | if isinstance(value, str): 66 | return f"\"{value}\"" 67 | elif ( 68 | isinstance(value, float) 69 | or isinstance(value, int) 70 | or isinstance(value, np.int64) 71 | ): 72 | return f"{value}" 73 | elif isinstance(value, datetime): 74 | return f"\"{value.strftime('%Y-%m-%d %H:%M:%S')}\"" 75 | else: 76 | raise ValueError( 77 | f"Data format is not support! type({value})={type(value)}") 78 | 79 | def _parse_sql_where_condition(self, **kwargs): 80 | sql = "" 81 | if len(kwargs) > 0: 82 | sql += "WHERE " 83 | for idx, (k, v) in enumerate(kwargs.items()): 84 | if idx != 0: 85 | sql += "AND " 86 | if k == "condition_str": # all non "=" conditions 87 | sql += f"{v} " 88 | else: 89 | sql += f"{k}={self._parse_sql_value(v)} " 90 | return sql 91 | 92 | def delete_table(self, table_name: str): 93 | sql = f"DROP TABLE {table_name}" 94 | self.execute(sql) 95 | 96 | def select_records( 97 | self, 98 | table_name: str, 99 | columns: List[str] = None, 100 | **kwargs 101 | ): 102 | if columns is None: 103 | columns = "*" 104 | else: 105 | columns = ",".join(columns) 106 | sql = f"SELECT {columns} FROM {table_name} " 107 | sql += self._parse_sql_where_condition(**kwargs) 108 | self.execute(sql) 109 | with lock: 110 | data = self.cursor.fetchall() 111 | return pd.DataFrame( 112 | data, 113 | columns=[d[0] for d in self.cursor.description]) 114 | 115 | def update_records( 116 | self, 117 | table_name: str, 118 | columns: Dict[str, Any], 119 | **kwargs 120 | ): 121 | assert len(columns) > 0, "At least one column needs to be updated!" 122 | sql = f"UPDATE {table_name} " 123 | for idx, (k, v) in enumerate(columns.items()): 124 | if idx == 0: 125 | sql += "SET " 126 | sql += f"{k}={self._parse_sql_value(v)} " 127 | if idx != len(columns) - 1: 128 | sql += "," 129 | sql += self._parse_sql_where_condition(**kwargs) 130 | self.execute(sql) 131 | 132 | def insert_records(self, table_name: str, **kwargs): 133 | assert len(kwargs) > 0, ( 134 | "Must provide columns and values when inserting!" 135 | ) 136 | sql = f"INSERT INTO {table_name} (" 137 | sql += ",".join(kwargs.keys()) 138 | sql += ") VALUES (" 139 | sql += ",".join([self._parse_sql_value(v) for v in kwargs.values()]) 140 | sql += ")" 141 | self.execute(sql) 142 | 143 | def delete_records(self, table_name: str, **kwargs): 144 | sql = (f"DELETE FROM {table_name} " 145 | f"{self._parse_sql_where_condition(**kwargs)}") 146 | self.execute(sql) 147 | 148 | def create_balance_table(self): 149 | sql = ( 150 | "CREATE TABLE IF NOT EXISTS balance " 151 | "(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " 152 | "broker_name VARCHAR(20) NOT NULL, " 153 | "broker_environment VARCHAR(20) NOT NULL, " 154 | "broker_account_id INTEGER NOT NULL, " 155 | "broker_account VARCHAR(20) NOT NULL, " 156 | "strategy_account_id INTEGER NOT NULL, " 157 | "strategy_account VARCHAR(20) NOT NULL, " 158 | "strategy_version VARCHAR(20) NOT NULL, " 159 | "strategy_version_desc VARCHAR(300), " 160 | "strategy_status VARCHAR(15), " 161 | "cash DOUBLE NOT NULL, " 162 | "cash_by_currency VARCHAR(50), " 163 | "available_cash DOUBLE NOT NULL, " 164 | "max_power_short DOUBLE, " 165 | "net_cash_power DOUBLE, " 166 | "maintenance_margin DOUBLE, " 167 | "unrealized_pnl DOUBLE, " 168 | "realized_pnl DOUBLE, " 169 | "update_time DATETIME NOT NULL, " 170 | "remark VARCHAR(300))" 171 | ) 172 | self.execute(sql) 173 | 174 | def create_position_table(self): 175 | sql = ( 176 | "CREATE TABLE IF NOT EXISTS position " 177 | "(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " 178 | "balance_id INTEGER NOT NULL, " 179 | "security_name VARCHAR(20) NOT NULL, " 180 | "security_code VARCHAR(20) NOT NULL, " 181 | "direction VARCHAR(20) NOT NULL, " 182 | "holding_price DOUBLE NOT NULL, " 183 | "quantity INTEGER NOT NULL, " 184 | "update_time DATETIME NOT NULL, " 185 | "remark VARCHAR(300))" 186 | ) 187 | self.execute(sql) 188 | 189 | def create_order_table(self): 190 | sql = ( 191 | "CREATE TABLE IF NOT EXISTS trading_order " 192 | "(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " 193 | "broker_order_id VARCHAR(50) NOT NULL, " 194 | "balance_id INTEGER NOT NULL, " 195 | "security_name VARCHAR(20) NOT NULL, " 196 | "security_code VARCHAR(20) NOT NULL, " 197 | "price DOUBLE NOT NULL, " 198 | "quantity INTEGER NOT NULL, " 199 | "direction VARCHAR(20) NOT NULL, " 200 | "offset VARCHAR(20) NOT NULL, " 201 | "order_type VARCHAR(20) NOT NULL, " 202 | "create_time DATETIME NOT NULL, " 203 | "update_time DATETIME NOT NULL, " 204 | "filled_avg_price DOUBLE NOT NULL, " 205 | "filled_quantity INTEGER NOT NULL, " 206 | "status VARCHAR(20) NOT NULL, " 207 | "remark VARCHAR(300))" 208 | ) 209 | self.execute(sql) 210 | 211 | def create_deal_table(self): 212 | sql = ( 213 | "CREATE TABLE IF NOT EXISTS trading_deal " 214 | "(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " 215 | "broker_deal_id VARCHAR(50) NOT NULL, " 216 | "broker_order_id VARCHAR(50) NOT NULL, " 217 | "order_id INTEGER NOT NULL, " 218 | "balance_id INTEGER NOT NULL, " 219 | "security_name VARCHAR(20) NOT NULL, " 220 | "security_code VARCHAR(20) NOT NULL, " 221 | "direction VARCHAR(20) NOT NULL, " 222 | "offset VARCHAR(20) NOT NULL, " 223 | "order_type VARCHAR(20) NOT NULL, " 224 | "update_time DATETIME NOT NULL, " 225 | "filled_avg_price DOUBLE NOT NULL, " 226 | "filled_quantity INTEGER NOT NULL, " 227 | "remark VARCHAR(300))" 228 | ) 229 | self.execute(sql) 230 | 231 | 232 | if __name__ == "__main__": 233 | db = DB() 234 | # db.delete_records(table_name="balance") 235 | # db.delete_records(table_name="position") 236 | db.delete_table("balance") 237 | db.delete_table("position") 238 | db.delete_table("trading_order") 239 | db.delete_table("trading_deal") 240 | # db.create_balance_table() 241 | 242 | db.insert_records( 243 | table_name="balance", 244 | broker_name="FUTU2", 245 | broker_environment="SIMULATE", 246 | broker_account_id=1, 247 | broker_account="123456", 248 | strategy_account_id=1, 249 | strategy_account="default", 250 | strategy_version="1.0", 251 | strategy_version_desc="manual trading", 252 | strategy_status="active", 253 | cash=100000.0, 254 | power=99000, 255 | max_power_short=-1, 256 | net_cash_power=-1, 257 | update_time=datetime.now(), 258 | remark="N/A" 259 | ) 260 | 261 | records = db.select_records( 262 | table_name="balance", 263 | broker_name="FUTU2", 264 | broker_environment="SIMULATE", 265 | broker_account="123456", 266 | # strategy_account_id=1, 267 | ) 268 | 269 | db.update_records( 270 | table_name="balance", 271 | columns={"cash": 950000}, 272 | id=4 273 | ) 274 | print() 275 | -------------------------------------------------------------------------------- /qtrader/plugins/telegram/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 3/1/2022 5:54 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | 17 | from .bot import bot 18 | -------------------------------------------------------------------------------- /qtrader/plugins/telegram/bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 3/1/2022 5:55 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: bot.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | 17 | from datetime import datetime 18 | 19 | from telegram import Update 20 | from telegram.ext import Updater 21 | from telegram.ext import ExtBot 22 | from telegram.ext import CallbackContext 23 | from telegram.ext import CommandHandler 24 | from telegram.ext import MessageHandler 25 | from telegram.ext import Filters 26 | 27 | from qtrader_config import TELEGRAM_TOKEN 28 | from qtrader_config import TELEGRAM_CHAT_ID 29 | 30 | 31 | class CustomedExtBot(ExtBot): 32 | __slots__ = ('qtrader_status',) 33 | 34 | 35 | def echo(update: Update, context: CallbackContext): 36 | """ 37 | Echo command 38 | """ 39 | context.bot.send_message( 40 | chat_id=update.effective_chat.id, 41 | text="[ECHO] " + update.message.text 42 | ) 43 | 44 | 45 | def stop(update: Update, context: CallbackContext): 46 | """ 47 | /stop [no parameters]: Stop the engine 48 | """ 49 | context.bot.qtrader_status = "Terminated" 50 | context.bot.send_message( 51 | chat_id=update.effective_chat.id, 52 | text=f"[{datetime.now()}] Terminating QTrader..." 53 | ) 54 | 55 | 56 | def balance(update: Update, context: CallbackContext): 57 | """ 58 | /balance [no parameters]: Fetch balance 59 | """ 60 | context.bot.get_balance = True 61 | context.bot.send_message( 62 | chat_id=update.effective_chat.id, 63 | text=f"[{datetime.now()}] Fetching balance..." 64 | ) 65 | 66 | 67 | def positions(update: Update, context: CallbackContext): 68 | """ 69 | /positions [no parameters]: Fetch positions of all gateways 70 | """ 71 | context.bot.get_positions = True 72 | context.bot.send_message( 73 | chat_id=update.effective_chat.id, 74 | text=f"[{datetime.now()}] Fetching positions..." 75 | ) 76 | 77 | 78 | def orders(update: Update, context: CallbackContext): 79 | """ 80 | /orders [parameters]: Fetch orders 81 | - filter:str (optional) if "-a", only return alive orders 82 | - n:int (optional) number of displayed records 83 | """ 84 | context.bot.send_message( 85 | chat_id=update.effective_chat.id, 86 | text=f"[{datetime.now()}] {context.args}..." 87 | ) 88 | if len(context.args) > 2: 89 | context.bot.send_message( 90 | chat_id=update.effective_chat.id, 91 | text=__doc__ 92 | ) 93 | return 94 | if len(context.args) == 2: 95 | if "-a" not in context.args: 96 | context.bot.send_message( 97 | chat_id=update.effective_chat.id, 98 | text=__doc__ 99 | ) 100 | return 101 | p = [arg for arg in context.args if arg != "-a"][0] 102 | try: 103 | num_orders_displayed = int(p) 104 | except ValueError: 105 | context.bot.send_message( 106 | chat_id=update.effective_chat.id, 107 | text=__doc__ 108 | ) 109 | return 110 | context.bot.active_orders = True 111 | context.bot.num_orders_displayed = num_orders_displayed 112 | if len(context.args) == 1: 113 | if context.args[0] == "-a": 114 | context.bot.active_orders = True 115 | else: 116 | try: 117 | num_orders_displayed = int(context.args[0]) 118 | except ValueError: 119 | context.bot.send_message( 120 | chat_id=update.effective_chat.id, 121 | text=__doc__ 122 | ) 123 | return 124 | context.bot.num_orders_displayed = num_orders_displayed 125 | 126 | context.bot.get_orders = True 127 | context.bot.send_message( 128 | chat_id=update.effective_chat.id, 129 | text=(f"[{datetime.now()}] Fetching orders (active_orders=" 130 | f"{context.bot.active_orders}, num_orders_displayed=" 131 | f"{context.bot.num_orders_displayed})...")) 132 | 133 | 134 | def deals(update: Update, context: CallbackContext): 135 | """ 136 | /deals [parameters]: fetch done deals 137 | - n:int (optional) number of displayed records 138 | """ 139 | if len(context.args) > 1: 140 | context.bot.send_message( 141 | chat_id=update.effective_chat.id, 142 | text=__doc__ 143 | ) 144 | return 145 | elif len(context.args) == 1: 146 | try: 147 | num_deals_displayed = int(context.args[0]) 148 | except ValueError: 149 | context.bot.send_message( 150 | chat_id=update.effective_chat.id, 151 | text=__doc__ 152 | ) 153 | return 154 | context.bot.num_deals_displayed = num_deals_displayed 155 | 156 | context.bot.get_deals = True 157 | context.bot.send_message( 158 | chat_id=update.effective_chat.id, 159 | text=(f"[{datetime.now()}] Fetching deals (num_deals_displayed=" 160 | f"{context.bot.num_deals_displayed})...") 161 | ) 162 | 163 | 164 | def cancel_orders(update: Update, context: CallbackContext): 165 | """ 166 | /cancel_orders [no parameters]: cancel all active orders 167 | """ 168 | context.bot.cancel_orders = True 169 | context.bot.send_message( 170 | chat_id=update.effective_chat.id, 171 | text=f"[{datetime.now()}] Canceling all active orders..." 172 | ) 173 | 174 | 175 | def cancel_order(update: Update, context: CallbackContext): 176 | """ 177 | /cancel_order [parameters]: cancel an active order 178 | - order_id: str, the id of the order 179 | - gateway_name: str, the name of the gateway 180 | """ 181 | if len(context.args) != 2: 182 | context.bot.send_message( 183 | chat_id=update.effective_chat.id, 184 | text=__doc__ 185 | ) 186 | return 187 | context.bot.cancel_order = True 188 | context.bot.cancel_order_id = str(context.args[0]) 189 | context.bot.gateway_name = str(context.args[1]) 190 | context.bot.send_message( 191 | chat_id=update.effective_chat.id, text=( 192 | f"[{datetime.now()}] Canceling order: order_id={context.args[0]}," 193 | f"gateway_name={context.args[1]}...")) 194 | 195 | 196 | def send_order(update: Update, context: CallbackContext): 197 | """ 198 | /send_order [parameters] send an order with string instruction: 199 | - order_string: str, security_code, quantity, direction(l/s), offset(o/c), 200 | order_type(m/l/s), gateway_name, price(None), 201 | stop_price(None) 202 | """ 203 | if len(context.args) != 1 or len(context.args[0].split(",")) != 8: 204 | context.bot.send_message( 205 | chat_id=update.effective_chat.id, 206 | text=__doc__ 207 | ) 208 | return 209 | context.bot.send_order = True 210 | context.bot.order_string = context.args[0] 211 | context.bot.send_message( 212 | chat_id=update.effective_chat.id, 213 | text=f"[{datetime.now()}] Send order ({context.args})..." 214 | ) 215 | 216 | 217 | def close_positions(update: Update, context: CallbackContext): 218 | """ 219 | /close_positions [parameters]: 220 | - gateway_name: str, the name of the gateway 221 | """ 222 | if len(context.args) > 1: 223 | context.bot.send_message( 224 | chat_id=update.effective_chat.id, 225 | text=__doc__ 226 | ) 227 | return 228 | elif len(context.args) == 1: 229 | context.bot.close_positions_gateway_name = context.args[0] 230 | context.bot.close_positions = True 231 | context.bot.send_message( 232 | chat_id=update.effective_chat.id, 233 | text=f"[{datetime.now()}] Closing positions..." 234 | ) 235 | 236 | 237 | class TelegramBot: 238 | """ 239 | Available commands:" 240 | 1. /stop 241 | 2. /balance 242 | 3. /positions 243 | 4. /orders 244 | 5. /deals 245 | 6. /cancel_order 246 | 7. /cancel_orders 247 | 8. /send_order 248 | 9. /close_positions 249 | 10. /help 250 | """ 251 | 252 | def __init__(self, token: str): 253 | # Handle responses (make updater.bot subclass, so that we can add 254 | # attributes to it) 255 | self.updater = Updater( 256 | use_context=True, 257 | bot=CustomedExtBot( 258 | token=token)) 259 | self.updater.bot.qtrader_status = "Running" 260 | self.updater.bot.get_balance = False 261 | self.updater.bot.get_positions = False 262 | self.updater.bot.get_orders = False 263 | self.updater.bot.active_orders = False 264 | self.updater.bot.num_orders_displayed = 1 265 | self.updater.bot.get_deals = False 266 | self.updater.bot.num_deals_displayed = 1 267 | self.updater.bot.cancel_order = False 268 | self.updater.bot.cancel_order_id = None 269 | self.updater.bot.gateway_name = None 270 | self.updater.bot.cancel_orders = False 271 | self.updater.bot.send_order = False 272 | self.updater.bot.order_string = "" 273 | self.updater.bot.close_positions = False 274 | self.updater.bot.close_positions_gateway_name = None 275 | 276 | dispatcher = self.updater.dispatcher 277 | stop_handler = CommandHandler('stop', stop) 278 | balance_handler = CommandHandler('balance', balance) 279 | positions_handler = CommandHandler('positions', positions) 280 | orders_handler = CommandHandler('orders', orders) 281 | deals_handler = CommandHandler('deals', deals) 282 | cancel_order_handler = CommandHandler('cancel_order', cancel_order) 283 | cancel_orders_handler = CommandHandler('cancel_orders', cancel_orders) 284 | send_order_handler = CommandHandler('send_order', send_order) 285 | close_positions_handler = CommandHandler( 286 | 'close_positions', close_positions) 287 | echo_handler = MessageHandler(Filters.text & (~Filters.command), echo) 288 | dispatcher.add_handler(echo_handler) 289 | dispatcher.add_handler(stop_handler) 290 | dispatcher.add_handler(balance_handler) 291 | dispatcher.add_handler(positions_handler) 292 | dispatcher.add_handler(orders_handler) 293 | dispatcher.add_handler(deals_handler) 294 | dispatcher.add_handler(cancel_order_handler) 295 | dispatcher.add_handler(cancel_orders_handler) 296 | dispatcher.add_handler(send_order_handler) 297 | dispatcher.add_handler(close_positions_handler) 298 | 299 | self.updater.start_polling() 300 | # self.updater.idle() 301 | 302 | @property 303 | def qtrader_status(self): 304 | return self.updater.bot.qtrader_status 305 | 306 | @qtrader_status.setter 307 | def qtrader_status(self, val: str): 308 | self.updater.bot.qtrader_status = val 309 | 310 | @property 311 | def get_balance(self): 312 | return self.updater.bot.get_balance 313 | 314 | @get_balance.setter 315 | def get_balance(self, val: bool): 316 | self.updater.bot.get_balance = val 317 | 318 | @property 319 | def get_positions(self): 320 | return self.updater.bot.get_positions 321 | 322 | @get_positions.setter 323 | def get_positions(self, val: bool): 324 | self.updater.bot.get_positions = val 325 | 326 | @property 327 | def get_orders(self): 328 | return self.updater.bot.get_orders 329 | 330 | @get_orders.setter 331 | def get_orders(self, val: bool): 332 | self.updater.bot.get_orders = val 333 | 334 | @property 335 | def num_orders_displayed(self): 336 | return self.updater.bot.num_orders_displayed 337 | 338 | @num_orders_displayed.setter 339 | def num_orders_displayed(self, val: bool): 340 | self.updater.bot.num_orders_displayed = val 341 | 342 | @property 343 | def active_orders(self): 344 | return self.updater.bot.active_orders 345 | 346 | @active_orders.setter 347 | def active_orders(self, val: bool): 348 | self.updater.bot.active_orders = val 349 | 350 | @property 351 | def get_deals(self): 352 | return self.updater.bot.get_deals 353 | 354 | @get_deals.setter 355 | def get_deals(self, val: bool): 356 | self.updater.bot.get_deals = val 357 | 358 | @property 359 | def num_deals_displayed(self): 360 | return self.updater.bot.num_deals_displayed 361 | 362 | @num_deals_displayed.setter 363 | def num_deals_displayed(self, val: bool): 364 | self.updater.bot.num_deals_displayed = val 365 | 366 | @property 367 | def cancel_order(self): 368 | return self.updater.bot.cancel_order 369 | 370 | @cancel_order.setter 371 | def cancel_order(self, val: bool): 372 | self.updater.bot.cancel_order = val 373 | 374 | @property 375 | def cancel_order_id(self): 376 | return self.updater.bot.cancel_order_id 377 | 378 | @cancel_order_id.setter 379 | def cancel_order_id(self, val: bool): 380 | self.updater.bot.cancel_order_id = val 381 | 382 | @property 383 | def gateway_name(self): 384 | return self.updater.bot.gateway_name 385 | 386 | @gateway_name.setter 387 | def gateway_name(self, val: bool): 388 | self.updater.bot.gateway_name = val 389 | 390 | @property 391 | def cancel_orders(self): 392 | return self.updater.bot.cancel_orders 393 | 394 | @cancel_orders.setter 395 | def cancel_orders(self, val: bool): 396 | self.updater.bot.cancel_orders = val 397 | 398 | @property 399 | def send_order(self): 400 | return self.updater.bot.send_order 401 | 402 | @send_order.setter 403 | def send_order(self, val: bool): 404 | self.updater.bot.send_order = val 405 | 406 | @property 407 | def order_string(self): 408 | return self.updater.bot.order_string 409 | 410 | @order_string.setter 411 | def order_string(self, val: bool): 412 | self.updater.bot.order_string = val 413 | 414 | @property 415 | def close_positions(self): 416 | return self.updater.bot.close_positions 417 | 418 | @close_positions.setter 419 | def close_positions(self, val: bool): 420 | self.updater.bot.close_positions = val 421 | 422 | @property 423 | def close_positions_gateway_name(self): 424 | return self.updater.bot.close_positions_gateway_name 425 | 426 | @close_positions_gateway_name.setter 427 | def close_positions_gateway_name(self, val: bool): 428 | self.updater.bot.close_positions_gateway_name = val 429 | 430 | def close(self): 431 | self.updater.stop() 432 | 433 | def get_updates(self): 434 | self.updates = self.updater.bot.get_updates() 435 | print(self.updates[0]) 436 | 437 | def get_chat_id(self): 438 | self.get_updates() 439 | chat_id = self.updates[0].message.from_user.id 440 | return chat_id 441 | 442 | def send_message(self, msg: str, chat_id: int = TELEGRAM_CHAT_ID): 443 | # chat_id = updates[0].message.from_user.id 444 | self.updater.bot.send_message( 445 | chat_id=chat_id, 446 | text=msg 447 | ) 448 | 449 | 450 | bot = TelegramBot(token=TELEGRAM_TOKEN) 451 | 452 | if __name__ == "__main__": 453 | if "bot" not in locals(): 454 | bot = TelegramBot(token=TELEGRAM_TOKEN) 455 | 456 | # before get_updates, send a msg to bot to initiate chat 457 | # bot.get_updates() 458 | # chat_id = bot.updates[0].message.from_user.id 459 | 460 | chat_id = TELEGRAM_CHAT_ID 461 | bot.send_message(chat_id=chat_id, msg="Yes I am here!") 462 | 463 | bot.close() 464 | print("Closed.") 465 | -------------------------------------------------------------------------------- /qtrader_config_sample.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 7/3/2021 9:04 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: qtrader_config.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | 19 | BACKTEST_GATEWAY = { 20 | "broker_name": "BACKTEST", 21 | "broker_account": "", 22 | "host": "", 23 | "port": -1, 24 | "pwd_unlock": -1, 25 | } 26 | 27 | IB_GATEWAY = { 28 | "broker_name": "IB", 29 | "broker_account": "", 30 | "host": "127.0.0.1", 31 | "port": 7497, 32 | "clientid": 1, 33 | "pwd_unlock": -1, 34 | } 35 | 36 | CQG_GATEWAY = { 37 | "broker_name": "CQG", 38 | "broker_account": "Demo", 39 | "password": "pass", 40 | "host": "127.0.0.1", 41 | "port": 2823, 42 | } 43 | 44 | FUTU_GATEWAY = { 45 | "broker_name": "FUTU", 46 | "broker_account": "TEST123456", 47 | "host": "127.0.0.1", 48 | "port": 11111, 49 | "pwd_unlock": 123456, 50 | } 51 | 52 | FUTUFUTURES_GATEWAY = { 53 | "broker_name": "FUTUFUTURES", 54 | "broker_account": "TEST123456", 55 | "host": "127.0.0.1", 56 | "port": 11111, 57 | "pwd_unlock": 123456, 58 | } 59 | 60 | GATEWAYS = { 61 | "Ib": IB_GATEWAY, 62 | "Backtest": BACKTEST_GATEWAY, 63 | "Cqg": CQG_GATEWAY, 64 | "Futu": FUTU_GATEWAY, 65 | "Futufutures": FUTUFUTURES_GATEWAY 66 | } 67 | 68 | TIME_STEP = 60000 # time step in milliseconds 69 | 70 | DATA_PATH = { 71 | "kline": "C:/Users/josephchen/data/k_line", 72 | } 73 | 74 | DATA_MODEL = { 75 | "kline": "Bar", 76 | } 77 | 78 | DB = { 79 | "sqlite3": "/Users/qtrader/data" 80 | } 81 | 82 | CLICKHOUSE = { 83 | "host": "localhost", 84 | "port": 9000, 85 | "user": "default", 86 | "password": "" 87 | } 88 | 89 | ACTIVATED_PLUGINS = ["analysis"] 90 | LOCAL_PACKAGE_PATHS = [] 91 | ADD_LOCAL_PACKAGE_PATHS_TO_SYSPATH = False 92 | 93 | AUTO_OPEN_PLOT = True 94 | IGNORE_TIMESTEP_OVERFLOW = False 95 | TELEGRAM_TOKEN = "" 96 | TELEGRAM_CHAT_ID = 1 97 | DATA_FFILL = True 98 | 99 | BAR_CONVENTION = { 100 | 'FUT.GC': 'end', 101 | 'FUT.SI': 'end' 102 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | clickhouse-driver 3 | empyrical 4 | futu-api 5 | ibapi 6 | ipykernel 7 | ipython 8 | ipython-genutils 9 | joblib 10 | json5 11 | jsonschema 12 | jupyter-client 13 | jupyter-core 14 | jupyter-server 15 | jupyterlab 16 | jupyterlab-pygments 17 | jupyterlab-server 18 | keras-nightly 19 | Keras-Preprocessing 20 | lxml 21 | Markdown 22 | matplotlib 23 | matplotlib-inline 24 | notebook 25 | numpy 26 | oauthlib 27 | openpyxl 28 | pandas 29 | pandas-datareader 30 | paramiko 31 | ppscore 32 | protobuf 33 | pyarrow 34 | pyfolio 35 | python-dateutil 36 | pytz 37 | requests 38 | requests-oauthlib 39 | scikit-learn 40 | scipy 41 | seaborn 42 | selenium 43 | simplejson 44 | sklearn 45 | SQLAlchemy 46 | statsmodels 47 | # TA-Lib 48 | tensorboard 49 | tensorboard-data-server 50 | tensorboard-plugin-wit 51 | tensorflow 52 | tensorflow-estimator 53 | termcolor 54 | tqdm 55 | tzlocal 56 | urllib3 57 | websocket-client 58 | Werkzeug 59 | xlrd 60 | XlsxWriter 61 | func-timeout 62 | PyYAML 63 | plotly 64 | finta 65 | pyautogui 66 | dash 67 | 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 29/4/2020 11:04 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: setup.py 6 | # @Software: PyCharm 7 | 8 | """ 9 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 10 | You may use, distribute and modify this code under the 11 | terms of the JXW license, which unfortunately won't be 12 | written for another century. 13 | 14 | You should have received a copy of the JXW license with 15 | this file. If not, please write to: josephchenhk@gmail.com 16 | """ 17 | 18 | # Either specify package data in setup.py or MANIFEST.in: 19 | # https://www.codenong.com/cs106808509/ 20 | 21 | from setuptools import setup, find_packages 22 | from pathlib import Path 23 | this_directory = Path(__file__).parent 24 | long_description = (this_directory / "README.md").read_text(encoding='utf-8') 25 | 26 | setup( 27 | name='qtrader', 28 | version='0.0.4', 29 | keywords=('Quantitative Trading', 'Qtrader', 'Backtest'), 30 | description='Qtrader: Event-Driven Algorithmic Trading Engine', 31 | long_description=long_description, 32 | long_description_content_type='text/markdown', 33 | license='JXW', 34 | install_requires=['sqlalchemy', 35 | 'pandas', 36 | 'numpy', 37 | 'pytz', 38 | 'clickhouse-driver', 39 | 'matplotlib', 40 | 'plotly', 41 | 'python-telegram-bot', 42 | 'dash'], 43 | author='josephchen', 44 | author_email='josephchenhk@gmail.com', 45 | include_package_data=True, 46 | packages=find_packages(), 47 | # package_data={"": [ 48 | # "*.ico", 49 | # "*.ini", 50 | # "*.dll", 51 | # "*.so", 52 | # "*.pyd", 53 | # ]}, 54 | platforms='any', 55 | url='', 56 | entry_points={ 57 | 'console_scripts': [ 58 | 'example=examples.demo_strategy:run' 59 | ] 60 | }, 61 | ) 62 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Test cases for QTrader 2 | 3 | To run the tests, copy `qtrader_config_sample.py` to current directory, and 4 | rename it to `qtrader_config.py`. Modify the gateway information accordingly, 5 | then you should be able to run the tests. -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 5/1/2022 3:44 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: __init__.py.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | -------------------------------------------------------------------------------- /tests/cqg_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 3/2/2022 9:00 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: cqg_test.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | import time 17 | from datetime import datetime, timedelta 18 | from datetime import time as Time 19 | 20 | import pytest 21 | 22 | from qtrader.core.constants import TradeMode, Exchange, Direction, Offset, OrderType 23 | from qtrader.core.order import Order 24 | from qtrader.core.security import Futures, Currency 25 | from qtrader.gateways.cqg import CQGFees 26 | from qtrader.gateways import CqgGateway 27 | 28 | # pytest fixture ussage 29 | # https://iter01.com/578851.html 30 | 31 | 32 | class TestCqgGateway: 33 | 34 | def setup_class(self): 35 | # Must provide full list of exsiting positions 36 | stock_list = [ 37 | # Futures(code="FUT.GC", lot_size=100, security_name="GCJ2", exchange=Exchange.NYMEX, 38 | # expiry_date="20220427"), 39 | # Futures(code="FUT.SI", lot_size=5000, security_name="SIK2", exchange=Exchange.NYMEX, 40 | # expiry_date="20220529"), 41 | Futures(code="FUT.CO", lot_size=1000, security_name="QON2", exchange=Exchange.ICE, 42 | expiry_date="20220727") 43 | ] 44 | gateway_name = "Cqg" 45 | gateway = CqgGateway( 46 | securities=stock_list, 47 | end=datetime.now() + timedelta(hours=1), 48 | gateway_name=gateway_name, 49 | fees=CQGFees 50 | ) 51 | time.sleep(10) 52 | 53 | gateway.SHORT_INTEREST_RATE = 0.0 54 | gateway.trade_mode = TradeMode.SIMULATE 55 | if gateway.trade_mode in (TradeMode.SIMULATE, TradeMode.LIVETRADE): 56 | assert datetime.now() < gateway.end, "Gateway end time must be later than current datetime!" 57 | self.gateway = gateway 58 | 59 | def teardown_class(self): 60 | self.gateway.close() 61 | 62 | # @pytest.mark.skip("Already tested") 63 | def test_send_order(self): 64 | orderids = [] 65 | for security in self.gateway.securities: 66 | print(security) 67 | quote = self.gateway.get_quote(security) 68 | create_time = self.gateway.market_datetime 69 | order = Order( 70 | security=security, 71 | price=quote.ask_price, 72 | stop_price=None, 73 | quantity=1, 74 | direction=Direction.LONG, 75 | offset=Offset.OPEN, 76 | order_type=OrderType.LIMIT, 77 | create_time=create_time 78 | ) 79 | orderid = self.gateway.place_order(order) 80 | orderids.append(orderid) 81 | time.sleep(2) 82 | time.sleep(3) 83 | assert all(len(oid) > 0 for oid in orderids) 84 | -------------------------------------------------------------------------------- /tests/futu_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 11/2/2022 1:46 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: futu_test.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | import sys 17 | import os 18 | import time 19 | from pathlib import Path 20 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 21 | sys.modules.pop("qtrader", None) 22 | sys.path.insert(0, str(Path(SCRIPT_PATH).parent.parent.joinpath("qtalib"))) 23 | sys.path.insert(0, str(Path(SCRIPT_PATH).parent.parent.joinpath("qtrader"))) 24 | sys.path.insert(0, str(Path(SCRIPT_PATH))) 25 | 26 | 27 | from datetime import datetime, timedelta 28 | from datetime import time as Time 29 | 30 | import pytest 31 | 32 | from qtrader.core.security import Security 33 | from qtrader.core.engine import Engine 34 | from qtrader.core.constants import TradeMode, Exchange 35 | from qtrader.core.security import Futures, Currency, Stock 36 | from qtrader.gateways.futu import FutuFeesHKFE, FutuFeesSEHK 37 | from qtrader.gateways import FutuGateway, FutuFuturesGateway 38 | from qtrader.core.constants import ( 39 | Direction, Offset, OrderType, TradeMode, OrderStatus 40 | ) 41 | 42 | # pytest fixture ussage 43 | # https://iter01.com/578851.html 44 | 45 | 46 | class TestFutuGateway: 47 | 48 | def setup_class(self): 49 | stock_list = [ 50 | # Futures( 51 | # code="HK.MCHmain", 52 | # lot_size=10, 53 | # security_name="HK.MCHmain", 54 | # exchange=Exchange.HKFE, 55 | # expiry_date="20230228"), 56 | Stock( 57 | code="HK.00981", 58 | lot_size=500, 59 | security_name="HK.00981", 60 | exchange=Exchange.SEHK), 61 | ] 62 | gateway_name = "Futu" 63 | gateway = FutuGateway( 64 | securities=stock_list, 65 | end=datetime.now() + timedelta(minutes=2), 66 | gateway_name=gateway_name, 67 | fees=FutuFeesSEHK, 68 | trading_sessions={'HK.00981': [ 69 | [datetime(1970, 1, 1, 9, 30, 0), 70 | datetime(1970, 1, 1, 12, 0, 0)], 71 | [datetime(1970, 1, 1, 13, 0, 0), 72 | datetime(1970, 1, 1, 16, 0, 0)]] 73 | } 74 | ) 75 | 76 | gateway.SHORT_INTEREST_RATE = 0.0 77 | gateway.trade_mode = TradeMode.LIVETRADE 78 | if gateway.trade_mode in (TradeMode.SIMULATE, TradeMode.LIVETRADE): 79 | assert datetime.now() < gateway.end, ( 80 | "Gateway end time must be later than current datetime!") 81 | engine = Engine(gateways={gateway_name: gateway}) 82 | self.gateway_name = gateway_name 83 | self.gateway = gateway 84 | self.engine = engine 85 | self.sleep_time = 5 86 | 87 | def teardown_class(self): 88 | self.gateway.close() 89 | 90 | @pytest.mark.skip("Already tested") 91 | def test_get_recent_bar(self): 92 | for _ in range(len(self.gateway.securities)): 93 | for security in self.gateway.securities: 94 | bar = self.gateway.get_recent_bar(security) 95 | print(f"Bar data: {bar}") 96 | assert bar.datetime.second == 0 97 | assert isinstance(bar.close, float) 98 | time.sleep(5) 99 | 100 | @pytest.mark.skip("Already tested") 101 | def test_get_broker_balance(self): 102 | balance = self.gateway.get_broker_balance() 103 | assert balance.cash > 0 104 | 105 | @pytest.mark.skip("Already tested") 106 | def test_get_all_broker_positions(self): 107 | positions = self.gateway.get_all_broker_positions() 108 | if positions: 109 | position = positions[0] 110 | assert position.quantity != 0 111 | 112 | def send_order(self, security: Security, 113 | quantity: int, 114 | direction: Direction, 115 | offset: Offset, 116 | order_type: OrderType, 117 | gateway_name: str 118 | ) -> bool: 119 | """return True if order is successfully fully filled, else False""" 120 | order_instruct = dict( 121 | security=security, 122 | quantity=quantity, 123 | direction=direction, 124 | offset=offset, 125 | order_type=order_type, 126 | gateway_name=gateway_name, 127 | ) 128 | 129 | self.engine.log.info(f"Submit order:\n{order_instruct}") 130 | orderid = self.engine.send_order(**order_instruct) 131 | # TODO: sometimes timeout here. 132 | if orderid == "": 133 | self.engine.log.info("Fail to submit order") 134 | return False 135 | self.engine.log.info(f"Order {orderid} has been submitted") 136 | time.sleep(self.sleep_time) 137 | order = self.engine.get_order( 138 | orderid=orderid, gateway_name=gateway_name) 139 | self.engine.log.info(f"Order {orderid} details: {order}") 140 | 141 | deals = self.engine.find_deals_with_orderid( 142 | orderid, gateway_name=gateway_name) 143 | self.engine.log.info(f"\tDeals: {deals}") 144 | self.engine.log.info(f"\tBefore portfolio update: " 145 | f"{self.engine.portfolios[gateway_name].value}") 146 | for deal in deals: 147 | self.engine.portfolios[gateway_name].update(deal) 148 | # self.portfolios[gateway_name].update(deal) 149 | self.engine.log.info(f"\tAfter portfolio update: " 150 | f"{self.engine.portfolios[gateway_name].value}") 151 | 152 | if order.status == OrderStatus.FILLED: 153 | self.engine.log.info(f"Order {orderid} has been filled.") 154 | return True 155 | else: 156 | err = self.engine.cancel_order( 157 | orderid=orderid, gateway_name=gateway_name) 158 | if err: 159 | self.engine.log.info( 160 | f"Fail to cancel order {orderid} for reason: {err}") 161 | else: 162 | self.engine.log.info(f"Successfully cancelled order {orderid}") 163 | return False 164 | 165 | def test_send_order(self): 166 | # security = Futures( 167 | # code="HK.MCHmain", 168 | # lot_size=10, 169 | # security_name="HK.MCHmain", 170 | # exchange=Exchange.HKFE, 171 | # expiry_date="20230228" 172 | # ) 173 | security = Stock( 174 | code="HK.00981", 175 | lot_size=500, 176 | security_name="HK.00981", 177 | exchange=Exchange.SEHK 178 | ) 179 | quantity = 1 180 | direction = Direction.SHORT 181 | offset = Offset.OPEN 182 | order_type = OrderType.MARKET 183 | gateway_name = self.gateway_name 184 | filled = self.send_order( 185 | security, 186 | quantity, 187 | direction, 188 | offset, 189 | order_type, 190 | gateway_name 191 | ) 192 | assert filled 193 | 194 | if __name__ == "__main__": 195 | test = TestFutuGateway() 196 | test.setup_class() 197 | test.test_send_order() 198 | test.teardown_class() 199 | print("Done.") -------------------------------------------------------------------------------- /tests/futufutures_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 11/2/2022 1:46 pm 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: futufutures_test.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | import sys 17 | import os 18 | import time 19 | from pathlib import Path 20 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 21 | sys.modules.pop("qtrader", None) 22 | sys.path.insert(0, str(Path(SCRIPT_PATH).parent.parent.joinpath("qtalib"))) 23 | sys.path.insert(0, str(Path(SCRIPT_PATH).parent.parent.joinpath("qtrader"))) 24 | sys.path.insert(0, str(Path(SCRIPT_PATH))) 25 | 26 | 27 | from datetime import datetime, timedelta 28 | from datetime import time as Time 29 | 30 | import pytest 31 | 32 | from qtrader.core.security import Security 33 | from qtrader.core.engine import Engine 34 | from qtrader.core.constants import TradeMode, Exchange 35 | from qtrader.core.security import Futures, Currency, Stock 36 | from qtrader.gateways.futu import FutuFeesHKFE, FutuFeesSEHK 37 | from qtrader.gateways import FutuGateway, FutuFuturesGateway 38 | from qtrader.core.constants import ( 39 | Direction, Offset, OrderType, TradeMode, OrderStatus 40 | ) 41 | 42 | # pytest fixture ussage 43 | # https://iter01.com/578851.html 44 | 45 | 46 | class TestFutuGateway: 47 | 48 | def setup_class(self): 49 | stock_list = [ 50 | Futures( 51 | code="HK.MCHmain", 52 | lot_size=10, 53 | security_name="HK.MCHmain", 54 | exchange=Exchange.HKFE, 55 | expiry_date="20230228"), 56 | ] 57 | gateway_name = "Futufutures" 58 | gateway = FutuFuturesGateway( 59 | securities=stock_list, 60 | end=datetime.now() + timedelta(minutes=2), 61 | gateway_name=gateway_name, 62 | fees=FutuFeesHKFE, 63 | trading_sessions={'HK.MCHmain': [ 64 | [datetime(1970, 1, 1, 9, 15, 0), 65 | datetime(1970, 1, 1, 12, 0, 0)], 66 | [datetime(1970, 1, 1, 13, 0, 0), 67 | datetime(1970, 1, 1, 16, 0, 0)], 68 | [datetime(1970, 1, 1, 17, 15, 0), 69 | datetime(1970, 1, 1, 3, 0, 0)]] 70 | } 71 | ) 72 | 73 | gateway.SHORT_INTEREST_RATE = 0.0 74 | gateway.trade_mode = TradeMode.LIVETRADE 75 | if gateway.trade_mode in (TradeMode.SIMULATE, TradeMode.LIVETRADE): 76 | assert datetime.now() < gateway.end, ( 77 | "Gateway end time must be later than current datetime!") 78 | engine = Engine(gateways={gateway_name: gateway}) 79 | self.gateway_name = gateway_name 80 | self.gateway = gateway 81 | self.engine = engine 82 | self.sleep_time = 5 83 | 84 | def teardown_class(self): 85 | self.gateway.close() 86 | 87 | # @pytest.mark.skip("Already tested") 88 | def test_get_recent_bar(self): 89 | for _ in range(len(self.gateway.securities)): 90 | for security in self.gateway.securities: 91 | bar = self.gateway.get_recent_bar(security) 92 | print(f"Bar data: {bar}") 93 | assert bar.datetime.second == 0 94 | assert isinstance(bar.close, float) 95 | time.sleep(5) 96 | 97 | # @pytest.mark.skip("Already tested") 98 | def test_get_broker_balance(self): 99 | balance = self.gateway.get_broker_balance() 100 | assert balance.cash > 0 101 | 102 | # @pytest.mark.skip("Already tested") 103 | def test_get_all_broker_positions(self): 104 | positions = self.gateway.get_all_broker_positions() 105 | if positions: 106 | position = positions[0] 107 | assert position.quantity != 0 108 | 109 | def send_order(self, security: Security, 110 | quantity: int, 111 | direction: Direction, 112 | offset: Offset, 113 | order_type: OrderType, 114 | gateway_name: str 115 | ) -> bool: 116 | """return True if order is successfully fully filled, else False""" 117 | order_instruct = dict( 118 | security=security, 119 | quantity=quantity, 120 | direction=direction, 121 | offset=offset, 122 | order_type=order_type, 123 | gateway_name=gateway_name, 124 | ) 125 | 126 | self.engine.log.info(f"Submit order:\n{order_instruct}") 127 | orderid = self.engine.send_order(**order_instruct) 128 | # TODO: sometimes timeout here. 129 | if orderid == "": 130 | self.engine.log.info("Fail to submit order") 131 | return False 132 | self.engine.log.info(f"Order {orderid} has been submitted") 133 | time.sleep(self.sleep_time) 134 | order = self.engine.get_order( 135 | orderid=orderid, gateway_name=gateway_name) 136 | self.engine.log.info(f"Order {orderid} details: {order}") 137 | 138 | deals = self.engine.find_deals_with_orderid( 139 | orderid, gateway_name=gateway_name) 140 | self.engine.log.info(f"\tDeals: {deals}") 141 | self.engine.log.info(f"\tBefore portfolio update: " 142 | f"{self.engine.portfolios[gateway_name].value}") 143 | for deal in deals: 144 | self.engine.portfolios[gateway_name].update(deal) 145 | # self.portfolios[gateway_name].update(deal) 146 | self.engine.log.info(f"\tAfter portfolio update: " 147 | f"{self.engine.portfolios[gateway_name].value}") 148 | 149 | if order.status == OrderStatus.FILLED: 150 | self.engine.log.info(f"Order {orderid} has been filled.") 151 | return True 152 | else: 153 | err = self.engine.cancel_order( 154 | orderid=orderid, gateway_name=gateway_name) 155 | if err: 156 | self.engine.log.info( 157 | f"Fail to cancel order {orderid} for reason: {err}") 158 | else: 159 | self.engine.log.info(f"Successfully cancelled order {orderid}") 160 | return False 161 | 162 | def test_send_order(self): 163 | security = Futures( 164 | code="HK.MCHmain", 165 | lot_size=10, 166 | security_name="HK.MCHmain", 167 | exchange=Exchange.HKFE, 168 | expiry_date="20230228" 169 | ) 170 | quantity = 1 171 | direction = Direction.SHORT 172 | offset = Offset.OPEN 173 | order_type = OrderType.MARKET 174 | gateway_name = self.gateway_name 175 | filled = self.send_order( 176 | security, 177 | quantity, 178 | direction, 179 | offset, 180 | order_type, 181 | gateway_name 182 | ) 183 | assert filled 184 | 185 | if __name__ == "__main__": 186 | test = TestFutuGateway() 187 | test.setup_class() 188 | test.test_send_order() 189 | test.teardown_class() 190 | print("Done.") -------------------------------------------------------------------------------- /tests/gateway_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 1/6/2022 9:53 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: gateway_test.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | from datetime import datetime 17 | from datetime import timedelta 18 | from datetime import time as Time 19 | from sys import platform 20 | 21 | import pytest 22 | 23 | from qtrader.config import GATEWAYS 24 | from qtrader.core.constants import Exchange, TradeMode 25 | from qtrader.core.engine import Engine 26 | from qtrader.core.security import Futures 27 | from qtrader.gateways import CqgGateway 28 | from qtrader.gateways.cqg import CQGFees 29 | from qtrader.gateways import IbGateway 30 | from qtrader.gateways.ib import IbHKEquityFees 31 | 32 | 33 | def check_sys_platform(): 34 | if platform == "linux" or platform =="linux2": 35 | return "Linux" 36 | elif platform == "darwin": 37 | return "OS X" 38 | elif platform == "win32": 39 | return "Windows" 40 | else: 41 | raise ValueError(f"Platform {platform} is not recognized.") 42 | 43 | class GatewayEngines: 44 | 45 | @classmethod 46 | def cqg(self): 47 | stock_list = [ 48 | # Futures(code="FUT.GC", lot_size=100, security_name="GCZ1", exchange=Exchange.NYMEX, expiry_date="20211229"), 49 | Futures(code="FUT.ZUC", lot_size=100, security_name="ZUCF22", exchange=Exchange.SGX, expiry_date="20220131"), 50 | ] 51 | 52 | gateway_name = "Cqg" 53 | init_capital = 100000 54 | gateway = CqgGateway( 55 | securities=stock_list, 56 | end=datetime.now() + timedelta(hours=1), 57 | gateway_name=gateway_name, 58 | fees=CQGFees 59 | ) 60 | gateway.SHORT_INTEREST_RATE = 0.0 61 | gateway.set_trade_mode(TradeMode.SIMULATE) 62 | gateway.TRADING_HOURS_AM = [Time(9, 0, 0), Time(10, 0, 0)] 63 | gateway.TRADING_HOURS_PM = [Time(10, 0, 0), Time(16, 0, 0)] 64 | 65 | engine = Engine(gateways={gateway_name: gateway}) 66 | return engine 67 | 68 | cqg_engine = GatewayEngines.cqg() 69 | 70 | class TestCQG: 71 | 72 | def test_sys_platform(self): 73 | """ CQG only works in Windows platform 74 | """ 75 | if "Cqg" in GATEWAYS: 76 | assert check_sys_platform() == "Windows" 77 | else: 78 | assert 1 79 | 80 | def test_wincom32_installed(self): 81 | """ Install pywin32 82 | > pip install pywin32==225 83 | """ 84 | if "Cqg" in GATEWAYS: 85 | installed = 0 86 | try: 87 | import win32com.client 88 | installed = 1 89 | except ImportError: 90 | pass 91 | assert installed 92 | else: 93 | assert 1 94 | 95 | if __name__ == "__main__": 96 | print() -------------------------------------------------------------------------------- /tests/ib_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 1/16/2022 10:07 AM 3 | # @Author : Joseph Chen 4 | # @Email : josephchenhk@gmail.com 5 | # @FileName: ib_test.py 6 | 7 | """ 8 | Copyright (C) 2020 Joseph Chen - All Rights Reserved 9 | You may use, distribute and modify this code under the 10 | terms of the JXW license, which unfortunately won't be 11 | written for another century. 12 | 13 | You should have received a copy of the JXW license with 14 | this file. If not, please write to: josephchenhk@gmail.com 15 | """ 16 | import sys 17 | import os 18 | import time 19 | from pathlib import Path 20 | SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) 21 | sys.modules.pop("qtrader", None) 22 | sys.path.insert(0, str(Path(SCRIPT_PATH).parent.parent.joinpath("qtalib"))) 23 | sys.path.insert(0, str(Path(SCRIPT_PATH).parent.parent.joinpath("qtrader"))) 24 | sys.path.insert(0, str(Path(SCRIPT_PATH))) 25 | 26 | 27 | from datetime import datetime, timedelta 28 | from datetime import time as Time 29 | 30 | import pytest 31 | 32 | from qtrader.core.constants import TradeMode, Exchange 33 | from qtrader.core.security import Futures, Currency 34 | from qtrader.gateways.cqg import CQGFees 35 | from qtrader.gateways import IbGateway 36 | 37 | # pytest fixture ussage 38 | # https://iter01.com/578851.html 39 | 40 | 41 | class TestIbGateway: 42 | 43 | def setup_class(self): 44 | stock_list = [ 45 | # Currency(code="EUR.USD", lot_size=1000, security_name="EUR.USD", exchange=Exchange.IDEALPRO), 46 | Futures(code="FUT.GC", lot_size=100, security_name="GCZ2", 47 | exchange=Exchange.NYMEX, expiry_date="20221228"), 48 | Futures(code="FUT.SI", lot_size=5000, security_name="SIZ2", 49 | exchange=Exchange.NYMEX, expiry_date="20221228"), 50 | ] 51 | gateway_name = "Ib" 52 | gateway = IbGateway( 53 | securities=stock_list, 54 | end=datetime.now() + timedelta(hours=1), 55 | gateway_name=gateway_name, 56 | fees=CQGFees 57 | ) 58 | 59 | gateway.SHORT_INTEREST_RATE = 0.0 60 | gateway.trade_mode = TradeMode.SIMULATE 61 | if gateway.trade_mode in (TradeMode.SIMULATE, TradeMode.LIVETRADE): 62 | assert datetime.now() < gateway.end, "Gateway end time must be later than current datetime!" 63 | # Asia time 64 | gateway.TRADING_HOURS_AM = [Time(9, 0, 0), Time(10, 0, 0)] 65 | gateway.TRADING_HOURS_PM = [Time(10, 0, 0), Time(23, 30, 0)] 66 | self.gateway = gateway 67 | 68 | def teardown_class(self): 69 | self.gateway.close() 70 | 71 | # @pytest.mark.skip("Already tested") 72 | def test_get_recent_bars(self): 73 | for security in self.gateway.securities: 74 | bars = self.gateway.get_recent_bars(security, "2min") 75 | print(f"Number of bars: {len(bars)}") 76 | assert len(bars) == 60 77 | 78 | # @pytest.mark.skip("Already tested") 79 | def test_get_recent_bar(self): 80 | for _ in range(2): 81 | for security in self.gateway.securities: 82 | bar = self.gateway.get_recent_bar(security, "2min") 83 | print(f"Bar data: {bar}") 84 | assert bar.datetime.second == 0 85 | assert isinstance(bar.close, float) 86 | time.sleep(130) 87 | 88 | @pytest.mark.skip("Already tested") 89 | def test_get_broker_balance(self): 90 | balance = self.gateway.get_broker_balance() 91 | assert balance.available_cash > 0 92 | 93 | @pytest.mark.skip("Already tested") 94 | def test_get_all_broker_positions(self): 95 | positions = self.gateway.get_all_broker_positions() 96 | if positions: 97 | position = positions[0] 98 | assert position.quantity != 0 --------------------------------------------------------------------------------