├── .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 | 
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 | 
65 |
66 | Inside each csv file, the data columns should look like this:
67 |
68 | 
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 | 
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 | 
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 | 
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
--------------------------------------------------------------------------------