├── tests
├── __init__.py
├── connector
│ ├── __init__.py
│ ├── quik
│ │ ├── __init__.py
│ │ ├── test_MsgConverter.py
│ │ └── test_WebQuikFeed.py
│ └── test_CsvFeedConnector.py
└── strategy
│ ├── __init__.py
│ └── features
│ ├── __init__.py
│ ├── test_Level2Features.py
│ └── test_TargetFeatures.py
├── pytrade
├── __init__.py
├── backtest
│ ├── __init__.py
│ └── Backtest.py
├── feed
│ ├── __init__.py
│ ├── Feed2Csv.py
│ └── Feed.py
├── interop
│ ├── __init__.py
│ ├── FeedInterop.py
│ └── BrokerInterop.py
├── model
│ ├── __init__.py
│ ├── feed
│ │ ├── __init__.py
│ │ ├── Level2Item.py
│ │ ├── Quote.py
│ │ ├── Ohlcv.py
│ │ ├── Asset.py
│ │ └── Level2.py
│ └── broker
│ │ └── Order.py
├── strategy
│ ├── __init__.py
│ ├── features
│ │ ├── __init__.py
│ │ ├── PriceFeatures.py
│ │ ├── TargetFeatures.py
│ │ └── Level2Features.py
│ ├── PeriodicalLearnStrategy.py
│ └── DataAnalysis.py
├── connector
│ ├── __init__.py
│ ├── quik
│ │ ├── __init__.py
│ │ ├── QueueName.py
│ │ ├── MsgConverter.py
│ │ ├── MsgId.py
│ │ ├── WebQuikBroker.py
│ │ ├── WebQuikFeed.py
│ │ └── WebQuikConnector.py
│ ├── EmptyBrokerConnector.py
│ ├── MemoryBrokerConnector.py
│ └── CsvFeedConnector.py
├── cfg
│ ├── app-defaults.yaml
│ └── log-defaults.cfg
├── App.py
└── broker
│ └── Broker.py
├── www
├── public
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── robots.txt
│ ├── manifest.json
│ └── index.html
├── src
│ ├── pricechart
│ │ ├── pricechart.css
│ │ └── PriceChart.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── reportWebVitals.js
│ ├── index.css
│ ├── broker
│ │ ├── Broker.css
│ │ ├── Replies.js
│ │ ├── StockLimits.js
│ │ ├── RawMsg.js
│ │ ├── MoneyLimits.js
│ │ ├── TradeAccount.js
│ │ ├── BuySell.js
│ │ └── Orders.js
│ ├── index.js
│ ├── stompConfig.js
│ ├── logo.svg
│ ├── App.css
│ └── App.js
├── .gitignore
├── Dockerfile
├── package.json
└── README.md
├── rabbit
└── Dockerfile
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── README.md
└── .gitignore
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pytrade/backtest/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/feed/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/interop/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/model/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/strategy/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/connector/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/strategy/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/connector/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/model/feed/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/connector/quik/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/connector/quik/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pytrade/strategy/features/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/strategy/features/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/www/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryPukhov/pytrade/HEAD/www/public/favicon.ico
--------------------------------------------------------------------------------
/www/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryPukhov/pytrade/HEAD/www/public/logo192.png
--------------------------------------------------------------------------------
/www/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DmitryPukhov/pytrade/HEAD/www/public/logo512.png
--------------------------------------------------------------------------------
/www/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/rabbit/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rabbitmq:3.6-management-alpine
2 | RUN rabbitmq-plugins enable rabbitmq_stomp
3 | RUN rabbitmq-plugins enable rabbitmq_web_stomp
4 |
5 |
6 |
--------------------------------------------------------------------------------
/www/src/pricechart/pricechart.css:
--------------------------------------------------------------------------------
1 |
2 | .pricechart.panel{
3 | background-color: white;
4 | background-color:rgb(254, 255, 244);
5 | width:100%;
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pyyaml~=5.4.1
2 | websocket-client==1.2.1
3 | pandas~=1.2.1
4 | pika~=1.2.0
5 | sortedcontainers~=2.4.0
6 | numpy~=1.19.2
7 | matplotlib~=3.4.2
8 | plotly~=5.3.1
9 |
--------------------------------------------------------------------------------
/pytrade/model/feed/Level2Item.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 |
4 | @dataclass
5 | class Level2Item:
6 | price: float
7 | bid_vol: float
8 | ask_vol: float
9 |
--------------------------------------------------------------------------------
/www/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/www/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/pytrade/model/broker/Order.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 |
4 |
5 | @dataclass(frozen=True)
6 | class Order:
7 | number: str
8 | dt: datetime
9 | class_code: str
10 | sec_code: str
11 | is_sell: bool
12 | account: str
13 | price: float
14 | quantity: int
15 | volume: float
16 | status: str
17 |
--------------------------------------------------------------------------------
/pytrade/model/feed/Quote.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 |
4 | from model.feed.Asset import Asset
5 |
6 |
7 | @dataclass
8 | class Quote:
9 | """
10 | Quote with dt,asset,bid,ask,last,last change
11 | """
12 | dt: datetime
13 | asset: Asset
14 | bid: float
15 | ask: float
16 | last: float
17 | last_change: float
18 |
--------------------------------------------------------------------------------
/pytrade/strategy/features/PriceFeatures.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import numpy as np
3 |
4 |
5 | class PriceFeatures:
6 | """
7 | Price features engineering
8 | """
9 |
10 | def prices(self, quotes: pd.DataFrame) -> pd.DataFrame:
11 | """
12 | Quotes and candles features
13 | """
14 | #df = quotes[['ask', 'bid']][-1]
15 | return quotes
16 |
--------------------------------------------------------------------------------
/pytrade/model/feed/Ohlcv.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 |
4 | from model.feed.Asset import Asset
5 |
6 |
7 | @dataclass
8 | class Ohlcv:
9 | """
10 | Candle open, high, low, close, volume with datetime
11 | """
12 | dt: datetime
13 | asset: Asset
14 | o: float
15 | h: float
16 | l: float
17 | c: float
18 | v: float
19 |
--------------------------------------------------------------------------------
/www/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/www/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/www/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # set base image (host OS)
2 | FROM python:3.8
3 |
4 | # set the working directory in the container
5 | WORKDIR /pytrade
6 |
7 | # copy the dependencies file to the working directory
8 | COPY requirements.txt .
9 |
10 | # install dependencies
11 | RUN pip install -r requirements.txt
12 |
13 | # copy the content of the local src directory to the working directory
14 | COPY pytrade/ .
15 |
16 | # command to run on container start
17 | CMD [ "python", "./App.py" ]
--------------------------------------------------------------------------------
/www/src/broker/Broker.css:
--------------------------------------------------------------------------------
1 | /*
2 | .buysell .asset{
3 | padding:0.5em;
4 | } */
5 |
6 |
7 | .buysell input{
8 | width:3em;
9 | }
10 | .buysell label{
11 | display:inline-block;
12 | margin-right:0.2em;
13 | }
14 | .buysell .param{
15 | display:inline-block;
16 | text-align: right;
17 |
18 | /* float: left; */
19 | }
20 | /* .buysell .buttons{
21 | text-align:right;
22 | } */
23 |
24 | .rawmsg textarea{
25 | width:100%
26 | }
27 |
28 |
29 |
--------------------------------------------------------------------------------
/pytrade/connector/EmptyBrokerConnector.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | class EmptyBrokerConnector:
5 | """
6 | Empty stub for broker when application needs only feed
7 | """
8 |
9 | def __init__(self, config):
10 | self._logger = logging.getLogger(__name__)
11 | self._logger.info(f"Init{__name__}")
12 | self.client_code = None
13 | self.trade_account = None
14 |
15 | def subscribe_broker(self, subscriber): return
16 |
17 | def run(self): return
18 |
--------------------------------------------------------------------------------
/pytrade/model/feed/Asset.py:
--------------------------------------------------------------------------------
1 | import re
2 | from dataclasses import dataclass
3 |
4 |
5 | @dataclass(frozen=True)
6 | class Asset:
7 | """
8 | Asset code and name
9 | """
10 | class_code: str
11 | sec_code: str
12 |
13 | def __str__(self):
14 | return f"{self.class_code}/{self.sec_code}"
15 |
16 | @staticmethod
17 | def of(strval: str):
18 | return Asset(*strval.split("/"))
19 |
20 | @staticmethod
21 | def any_asset():
22 | return Asset("*", "*")
23 |
--------------------------------------------------------------------------------
/pytrade/connector/quik/QueueName.py:
--------------------------------------------------------------------------------
1 | class QueueName(object):
2 | """
3 | Rabbit queue names
4 | """
5 | TRADE_ACCOUNT = "pytrade.broker.trade.account"
6 | ORDERS = "pytrade.broker.orders"
7 | CMD_BUYSELL = "pytrade.broker.cmd.buysell"
8 | MSG_RAW = "pytrade.broker.msg.raw"
9 | MSG_REPLY = "pytrade.broker.msg.reply"
10 | TRADES = "pytrade.broker.trades"
11 | MONEY_LIMITS = "pytrade.broker.money.limits"
12 | STOCK_LIMITS = "pytrade.broker.stock.limits"
13 | LIMIT = "pytrade.broker.stock.limit"
14 | CANDLES = "pytrade.feed.candles"
15 |
--------------------------------------------------------------------------------
/www/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/www/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/www/src/stompConfig.js:
--------------------------------------------------------------------------------
1 | export const stompConfig = {
2 | // Typically login, passcode and vhost
3 | // Adjust these for your broker
4 | connectHeaders: {
5 | login: "guest",
6 | passcode: "guest"
7 | },
8 |
9 | // Broker URL, should start with ws:// or wss:// - adjust for your broker setup
10 | // Rabbit should have
11 | brokerURL: "ws://localhost:15674/ws",
12 |
13 | // Keep it off for production, it can be quit verbose
14 | // Skip this key to disable
15 | debug: function (str) {
16 | console.log('STOMP: ' + str);
17 | },
18 |
19 | // If disconnected, it will retry after 200ms
20 | reconnectDelay: 200,
21 | };
22 | export default stompConfig;
--------------------------------------------------------------------------------
/www/Dockerfile:
--------------------------------------------------------------------------------
1 | # This defines our starting point
2 | FROM node:13.12.0-alpine
3 | # alpine images doesn't have bash installed out of box. Install it here.
4 | RUN apk update && apk add bash
5 |
6 | WORKDIR /www
7 |
8 | # add `/app/node_modules/.bin` to $PATH
9 | ENV PATH /www/node_modules/.bin:$PATH
10 |
11 | # install node dependencies
12 | COPY package.json ./
13 | COPY package-lock.json ./
14 | RUN npm install
15 | RUN npm install react-scripts@3.4.1 -g
16 |
17 | # Stomp over websocket to get data from rabbit
18 | # Not sure all these are required ))
19 | RUN npm install websocket stomp stomp-websocket stompjs
20 |
21 | RUN npm install react-plotly.js
22 |
23 | COPY . .
24 | # start app
25 | CMD ["npm", "start"]
26 |
--------------------------------------------------------------------------------
/pytrade/model/feed/Level2.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 | from typing import List
4 |
5 | from sortedcontainers import SortedList
6 |
7 | from model.feed.Asset import Asset
8 | from model.feed.Level2Item import Level2Item
9 |
10 |
11 | @dataclass
12 | class Level2:
13 | """
14 | All level2 quotes at the moment
15 | """
16 | dt: datetime
17 | asset: Asset
18 | # Level 2 items price: bid or ask, sorted by price
19 | items: List[Level2Item] = SortedList(key=lambda item: item.price)
20 |
21 | @staticmethod
22 | def of(dt: datetime, asset: str, items: List[Level2Item]):
23 | return Level2(dt, asset, SortedList(items, key=lambda item: item.price))
24 |
--------------------------------------------------------------------------------
/pytrade/strategy/features/TargetFeatures.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import numpy as np
3 |
4 |
5 | class TargetFeatures:
6 | """
7 | Target features engineering
8 | """
9 |
10 | def min_max_future(self, df: pd.DataFrame, periods: int, freq: str) -> pd.DataFrame:
11 | """
12 | Add target features: min and max price during future window
13 | :param freq : time unit for future window
14 | :param periods: duration of future window in given time units
15 | """
16 | # Trick to implement forward rolling window for timeseries with unequal intervals:
17 | # reverse, apply rolling, then reverse back
18 | windowspec = f'{periods} {freq}'
19 | #df2 = df.reset_index(level='ticker', drop=True)
20 | df2 = df.reset_index(level='ticker', drop=True)[['ask', 'bid']].sort_index(ascending=False).rolling(windowspec, min_periods=0).agg(
21 | {'ask': 'max', 'bid': 'min'}, closed='right')
22 | df2[['fut_ask_max', 'fut_bid_min']] = df2[['ask', 'bid']]
23 | return df2.sort_index()
24 |
--------------------------------------------------------------------------------
/pytrade/connector/MemoryBrokerConnector.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | class MemoryBrokerConnector:
5 | """
6 | Broker emulator, keeps all trades in memory
7 | """
8 |
9 | def __init__(self, config):
10 | self._logger = logging.getLogger(__name__)
11 | self._logger.info(f"Init{__name__}")
12 | self.client_code = None
13 | self.trade_account = None
14 | self._broker_subscribers = []
15 |
16 | def subscribe_broker(self, subscriber):
17 | self._broker_subscribers.append(subscriber)
18 |
19 | def run(self):
20 | return
21 |
22 | def buy(self, class_code, sec_code, price, quantity):
23 | self._logger.info(f"Got buy command. class_code:{class_code}, sec_code: {sec_code}, "
24 | f"price: {price}, quantity: {quantity}")
25 | #self._broker_connector.buy(class_code, sec_code, price, quantity)
26 |
27 | def sell(self, class_code, sec_code, price, quantity):
28 | self._logger.info(f"Got sell command. class_code:{class_code}, sec_code: {sec_code}, "
29 | f"price: {price}, quantity: {quantity}")
30 | #self._broker_connector.sell(class_code, sec_code, price, quantity)
31 |
32 |
--------------------------------------------------------------------------------
/pytrade/cfg/app-defaults.yaml:
--------------------------------------------------------------------------------
1 | # Web quik config
2 | connector.webquik.url: 'wss://junior.webquik.ru:443/quik'
3 |
4 | # Account and password, provided by broker
5 | connector.webquik.account: 'U0181659'
6 | connector.webquik.passwd: '09381' # Client code, provided by broker
7 | connector.webquik.client_code: '10058'
8 | # Trade account for specific market. Can be seen as "firm" or "account" column in quik tables
9 | connector.webquik.trade_account: "NL0011100043"
10 |
11 | log.dir: "../logs"
12 |
13 | # Main asset to trade
14 | trade.asset.sec_class: 'QJSIM'
15 | trade.asset.sec_code: 'SBER'
16 |
17 | # Interop mode, sends feed and receives orders from external systems through rabbitmq
18 | interop.is_interop: True
19 | interop.rabbit.host: "rabbit"
20 |
21 | # Gather feed to csv or not
22 | is_feed2csv: False
23 | strategy: PeriodicalLearnStrategy
24 |
25 | feed.connector: WebQuikFeed
26 | #feed_connector: CsvFeedConnector
27 | #csv_feed_candles: data/QJSIM_SBER_candles_2021-11-07.csv
28 | #csv_feed_quotes: data/QJSIM_SBER_quotes_2021-11-07.csv
29 | #csv_feed_level2: data/QJSIM_SBER_level2_2021-11-07.csv
30 |
31 | broker.connector: WebQuikBroker
32 | #broker_connector: EmptyBrokerConnector
33 |
34 | # run or learn
35 | app.action: run
--------------------------------------------------------------------------------
/www/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "www",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@stomp/rx-stomp": "^1.1.0",
7 | "@testing-library/jest-dom": "^5.11.10",
8 | "@testing-library/react": "^11.2.6",
9 | "@testing-library/user-event": "^12.8.3",
10 | "plotly.js": "^1.58.4",
11 | "react": "^17.0.2",
12 | "react-dom": "^17.0.2",
13 | "react-plotly.js": "^2.5.1",
14 | "react-scripts": "4.0.3",
15 | "react-stomp": "^5.1.0",
16 | "react-websocket": "^2.1.0",
17 | "stomp": "^0.1.1",
18 | "stomp-websocket": "^2.3.4-next",
19 | "web-vitals": "^1.1.1",
20 | "websocket": "^1.0.33"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ]
33 | },
34 | "browserslist": {
35 | "production": [
36 | ">0.2%",
37 | "not dead",
38 | "not op_mini all"
39 | ],
40 | "development": [
41 | "last 1 chrome version",
42 | "last 1 firefox version",
43 | "last 1 safari version"
44 | ]
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/www/src/broker/Replies.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react'
2 |
3 |
4 | /**
5 | * Reply messages from broker.
6 | */
7 | class Replies extends Component{
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 | // Get stomp client for rabbit connection
13 | this.stompClient = props.stompClient;
14 | this.queueName='pytrade.broker.msg.reply';
15 | this.state={msgs: []};
16 | }
17 |
18 | onConnect(){
19 | // Subscribe to rabbit queue
20 | console.log('Subscribing to '+ this.queueName);
21 | this.stompClient.subscribe(this.queueName, this.onMsg.bind(this),{});
22 | }
23 |
24 | /**
25 | * Got new reply message
26 | */
27 | onMsg(msg) {
28 | console.log('Got reply msg '+ msg.body)
29 | var msgs = this.state.msgs
30 | var msgBody = msg.body.replace(/'/g, '"');
31 | msgs.push(msgBody)
32 | this.setState({msgs: msgs});
33 | }
34 |
35 | render() {
36 | return (
37 |
38 |
39 | {Array.from(this.state.msgs).map(msg =>
40 | {msg}
41 |
)}
42 |
43 | );
44 | }
45 | }
46 | export default Replies;
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 | services:
3 | # Main app - pytrade
4 | pytrade:
5 | container_name: pytrade
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | volumes:
10 | - ./pytrade/data:/pytrade/data
11 | # depends_on:
12 | # rabbit:
13 | # condition: service_healthy
14 | # links:
15 | # - rabbit
16 | restart: on-failure
17 | networks:
18 | - pytrade
19 |
20 | # Rabbit for exchange between systems
21 | rabbit:
22 | container_name: pytrade-rabbit
23 | build:
24 | context: ./rabbit
25 | dockerfile: Dockerfile
26 | ports:
27 | # The standard AMQP protocol port
28 | - '5672:5672'
29 | # HTTP management UI
30 | - '15672:15672'
31 | # Stomp
32 | - '15674:15674'
33 | - '61613:61613'
34 |
35 | healthcheck:
36 | test: [ "CMD", "nc", "-z", "rabbit", "5672" ]
37 | #test: rabbitmq-diagnostic -q ping
38 | interval: 5s
39 | timeout: 15s
40 | retries: 3
41 | networks:
42 | - pytrade
43 |
44 | # React web ui for basic management
45 | www:
46 | container_name: pytrade-www
47 | depends_on:
48 | pytrade:
49 | condition: service_started
50 | build:
51 | context: www
52 | dockerfile: Dockerfile
53 | ports:
54 | - '3000:3000' # localhost:3000 in browser
55 | command: >
56 | bash -c "npm install && npm start --host 0.0.0.0 --port 3000 --live-reload -o"
57 | networks:
58 | - pytrade
59 |
60 | # Network for exchange between systems
61 | networks:
62 | pytrade:
63 | external: false
--------------------------------------------------------------------------------
/tests/connector/quik/test_MsgConverter.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from unittest import TestCase
3 |
4 | from pytrade.connector.quik.MsgConverter import MsgConverter
5 |
6 |
7 | class TestMsgConverter(TestCase):
8 | def test_msg2order(self):
9 | msg = {'msgid': 21001, 'qdate': 20210724, 'qtime': 144005, 'ccode': 'QJSIM', 'scode': 'SBER',
10 | 'sell': 0, 'account': 'NL0011100043', 'price': 29500, 'qty': 1, 'volume': 123, 'balance': 1,
11 | 'yield': 0, 'accr': 0, 'refer': '10815//', 'type': 25, 'firm': 'NC0011100000', 'ucode': '10815',
12 | 'number': '6059372033', 'status': 234, 'price_currency': '', 'settle_currency': ''}
13 | order = MsgConverter.msg2order(msg)
14 | self.assertEqual(datetime(2021, 7, 24, 14, 40, 5), order.dt)
15 | self.assertEqual('QJSIM', order.class_code)
16 | self.assertEqual('SBER', order.sec_code)
17 | self.assertEqual('SBER', order.sec_code)
18 | self.assertFalse(order.is_sell)
19 | self.assertEqual('NL0011100043', order.account)
20 | self.assertEqual(29500, order.price)
21 | self.assertEqual(1, order.quantity)
22 | self.assertEqual(123, order.volume)
23 | self.assertEqual(234, order.status)
24 |
25 | def test_decode_datetime(self):
26 | dt = MsgConverter().decode_datetime(20210724, 160412)
27 | self.assertEqual(2021, dt.year)
28 | self.assertEqual(7, dt.month)
29 | self.assertEqual(24, dt.day)
30 | self.assertEqual(16, dt.hour)
31 | self.assertEqual(4, dt.minute)
32 | self.assertEqual(12, dt.second)
33 |
--------------------------------------------------------------------------------
/pytrade/connector/quik/MsgConverter.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from model.broker.Order import Order
4 |
5 |
6 | class MsgConverter:
7 | @staticmethod
8 | def decode_datetime(date: int, time: int) -> datetime:
9 | """
10 | Decode time like date=20210724, time=144005
11 | """
12 | year = date // 10000
13 | month = (date % 10000) // 100
14 | day = date % 100
15 | hour = time // 10000
16 | minute = (time % 10000) // 100
17 | sec = time % 100
18 | return datetime(year, month, day, hour, minute, sec)
19 |
20 | @staticmethod
21 | def msg2order(msg) -> Order:
22 | """
23 | Convert Quik message to Order model
24 | """
25 | # msg={'msgid': 21001, 'qdate': 20210724, 'qtime': 144005, 'ccode': 'QJSIM', 'scode': 'SBER',
26 | # 'sell': 0, 'account': 'NL0011100043', 'price': 29500, 'qty': 1, 'volume': 295000, 'balance': 1,
27 | # 'yield': 0, 'accr': 0, 'refer': '10815//', 'type': 25, 'firm': 'NC0011100000', 'ucode': '10815',
28 | # 'number': '6059372033', 'status': 1, 'price_currency': '', 'settle_currency': ''}
29 | return Order(number=msg['number'],
30 | dt=MsgConverter.decode_datetime(msg['qdate'],msg['qtime']),
31 | class_code=msg['ccode'],
32 | sec_code=msg['scode'],
33 | is_sell=msg['sell'],
34 | account=msg['account'],
35 | price=msg['price'],
36 | quantity=msg['qty'],
37 | volume=msg['volume'],
38 | status=msg['status'])
39 |
--------------------------------------------------------------------------------
/pytrade/cfg/log-defaults.cfg:
--------------------------------------------------------------------------------
1 | [loggers]
2 | keys = root,WebQuikConnector,WebQuikBroker,Feed,WebQuikFeed,FeedInterop,Feed2Csv,Broker,BrokerInterop,PeriodicalLearnStrategy,CsvFeedConnector
3 |
4 | [handlers]
5 | keys = stream_handler,stream_handler_err
6 |
7 | [formatters]
8 | keys = formatter
9 |
10 | [logger_WebQuikConnector]
11 | qualname = connector.quik.WebQuikConnector
12 | handlers =
13 | level = INFO
14 |
15 | [logger_WebQuikBroker]
16 | qualname = connector.quik.WebQuikBroker
17 | handlers =
18 | level = INFO
19 |
20 | [logger_Feed]
21 | qualname = feed.Feed
22 | handlers =
23 | level = INFO
24 |
25 | [logger_WebQuikFeed]
26 | qualname = connector.quik.WebQuikFeed
27 | handlers =
28 | level = INFO
29 |
30 | [logger_Feed2Csv]
31 | qualname = feed.Feed2Csv
32 | handlers =
33 | level = INFO
34 |
35 | [logger_FeedInterop]
36 | qualname = interop.FeedInterop
37 | handlers =
38 | level = INFO
39 |
40 | [logger_Broker]
41 | qualname = broker.Broker
42 | handlers =
43 | level = INFO
44 |
45 | [logger_BrokerInterop]
46 | qualname = interop.BrokerInterop
47 | handlers =
48 | level = INFO
49 |
50 | [logger_CsvFeedConnector]
51 | qualname = connector.CsvFeedConnector
52 | handlers =
53 | level = INFO
54 |
55 |
56 | [logger_PeriodicalLearnStrategy]
57 | qualname = strategy.PeriodicalLearnStrategy
58 | handlers =
59 | level = INFO
60 |
61 | [logger_root]
62 | level=ERROR
63 | handlers = stream_handler,stream_handler_err
64 |
65 | [handler_stream_handler_err]
66 | level=ERROR
67 | class = StreamHandler
68 | formatter = formatter
69 | args = (sys.stderr,)
70 |
71 | [handler_stream_handler]
72 | class = StreamHandler
73 | formatter = formatter
74 | args = (sys.stderr,)
75 |
76 | [formatter_formatter]
77 | format = %(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s
78 | date_fmt = %Y-%m-%d %H:%M:%S
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pytrade
2 | Trading robots written in Python. Connects directly to web quik server to read data and make orders.
3 |
4 | ## Current status
5 | Feed implemented. Simple buy/sell orders are implemented. Stop and market orders are under development.
6 | Dev web ui is in progress.
7 |
8 | ## Prerequisites
9 | Demo or live account with web quik https://arqatech.com/en/products/quik/terminals/user-applications/webquik/ from any broker.
10 | I use demo account at [junior.webquik.ru](https://junior.webquik.ru/).
11 |
12 | ## Setting up
13 | Copy **pytrade/cfg/app-defaults.yaml** to a new local config **pytrade/cfg/app.yaml**
14 | Configure **conn**, **account**, **passwd** and **client_code** variables in your **pytrade/cfg/app.yaml**
15 |
16 | ## Running
17 | docker-compose up
18 |
19 | Open dev tools in browser: [http://localhost:3000](http://localhost:3000) - price chart should appear in real time.
20 | Open rabbitmq at [http://localhost:15672/](http://localhost:15672/), use rabbitmq default login and password: quest/quest
21 |
22 | ## Using in your robots
23 |
24 | ### Option 1. Single mode.
25 | Only Python lives here, no integration with external systems.
26 | Set *is_interop: False*, in *app.yaml*. Add your strategy python class to strategy folder and set *strategy: ...* in *app.yaml* Run and debug in your preferrable IDE using *App.py* entry point
27 |
28 | ### Option 2. Interop mode - manage pytrade from external system
29 | Integration with external systems through rabbitmq If *is_interop: True* in app.yaml, pytrade sends the prices and receives buy/sell instructions to/from rabbit mq. Any external system can read prices and make orders through rabbit.
30 |
31 | ## Capturing the Feed to file system
32 | Set *is_feed2csv: True* in *app.yaml* and pytrade will save all received data into *data* folder in csv format.
33 |
--------------------------------------------------------------------------------
/www/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/www/src/broker/StockLimits.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react'
2 |
3 |
4 | /**
5 | * Stock limits (positions on assets) from Quik
6 | */
7 | class StockLimits extends Component{
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 | // Get stomp client for rabbit connection
13 | this.stompClient = props.stompClient;
14 | this.queueName='pytrade.broker.stock.limits';
15 |
16 | //this.state={stockLimits: new Map([['11',{'scode':'scodeval', 'cbal':100,'vavg':123}]])};
17 | this.state={stockLimits: new Map([])};
18 | }
19 |
20 | onConnect(){
21 | // Subscribe to rabbit queue
22 | console.log('Subscribing to '+ this.queueName);
23 | this.stompClient.subscribe(this.queueName, this.onStockLimits.bind(this),{});
24 | }
25 |
26 | /**
27 | * Got info
28 | */
29 | onStockLimits(msg) {
30 | console.log('Got msg '+ msg.body)
31 |
32 | // Update stock limit map with new position
33 | var stockLimitMap = this.state.stockLimits
34 | var msgData = JSON.parse(msg.body.replace(/'/g, '"'));
35 | stockLimitMap.set(msgData["scode"], msgData);
36 | this.setState({stockLimits: stockLimitMap});
37 |
38 | }
39 |
40 | render() {
41 | var lst = Array.from(this.state.stockLimits.values());
42 | return (
43 |
44 |
45 |
46 |
47 | | asset | count | price |
48 |
49 | {Array.from(this.state.stockLimits.values()).map(v=>
50 | | {v.scode} | {v.cbal} | {v.avg} |
51 |
)}
52 |
53 |
54 | );
55 | }
56 | }
57 | export default StockLimits;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Config files
3 | /pytrade/cfg/app.yaml
4 | /pytrade/cfg/log.cfg
5 |
6 | # Project and logs
7 | .vscode
8 | logs
9 | # Idea
10 | .idea
11 | *.iml
12 |
13 | # Byte-compiled / optimized / DLL files
14 | __pycache__/
15 | *.py[cod]
16 | *$py.class
17 |
18 | # C extensions
19 | *.so
20 |
21 | # Distribution / packaging
22 | .Python
23 | build/
24 | develop-eggs/
25 | dist/
26 | downloads/
27 | eggs/
28 | .eggs/
29 | lib/
30 | lib64/
31 | parts/
32 | sdist/
33 | var/
34 | wheels/
35 | *.egg-info/
36 | .installed.cfg
37 | *.egg
38 | MANIFEST
39 |
40 | # PyInstaller
41 | # Usually these files are written by a python script from a template
42 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
43 | *.manifest
44 | *.spec
45 |
46 | # Installer logs
47 | pip-log.txt
48 | pip-delete-this-directory.txt
49 |
50 | # Unit test / coverage reports
51 | htmlcov/
52 | .tox/
53 | .coverage
54 | .coverage.*
55 | .cache
56 | nosetests.xml
57 | coverage.xml
58 | *.cover
59 | .hypothesis/
60 | .pytest_cache/
61 |
62 | # Translations
63 | *.mo
64 | *.pot
65 |
66 | # Django stuff:
67 | *.log
68 | local_settings.py
69 | db.sqlite3
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # celery beat schedule file
91 | celerybeat-schedule
92 |
93 | # SageMath parsed files
94 | *.sage.py
95 |
96 | # Environments
97 | .env
98 | .venv
99 | env/
100 | venv/
101 | ENV/
102 | env.bak/
103 | venv.bak/
104 |
105 | # Spyder project settings
106 | .spyderproject
107 | .spyproject
108 |
109 | # Rope project settings
110 | .ropeproject
111 |
112 | # mkdocs documentation
113 | /site
114 |
115 | # mypy
116 | .mypy_cache/
117 |
118 | tmp
119 | pytrade/logs
--------------------------------------------------------------------------------
/www/src/broker/RawMsg.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react'
2 | import './Broker.css'
3 |
4 | /**import './Broker.cimport './Broker.css'ss'
5 | * Sends raw message to broker
6 | */
7 | class RawMsg extends Component{
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 |
13 | // Get stomp client for rabbit connection
14 | this.stompClient = props.stompClient;
15 | this.queueName='pytrade.broker.msg.raw';
16 |
17 | var rawMsg = {
18 | "transid": Math.floor(Math.random()*1000000),
19 | "msgid": 12000, // Order msg id
20 | "action": "SIMPLE_STOP_ORDER",
21 | "MARKET_STOP_LIMIT": "YES",
22 |
23 | "ccode": "QJSIM",
24 | "scode": "SBER",
25 | "operation": "B",
26 |
27 | "quantity": 1,
28 | "clientcode": "10058",
29 | "account": "NL0011100043",
30 | "stopprice": 215
31 | }
32 | //rawMsg = {"msgid":12100,"clientcode":"10058","account":"NL0011100043", "number":"6059372033"}
33 | this.state={rawMsg: JSON.stringify(rawMsg)};
34 | }
35 |
36 | send(e) {
37 | console.log("Sending");
38 | this.stompClient.send(this.queueName, {'auto-delete':true}, this.state.rawMsg );
39 | }
40 |
41 | render() {
42 |
43 | return (
44 |
45 |
Send raw message to broker
46 |
47 |
48 |
49 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 | }
58 | }
59 | export default RawMsg;
--------------------------------------------------------------------------------
/pytrade/interop/FeedInterop.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import pika
4 | from connector.quik.MsgId import MsgId
5 | from connector.quik.QueueName import QueueName
6 | from model.feed.Asset import Asset
7 | from model.feed.Ohlcv import Ohlcv
8 |
9 |
10 | class FeedInterop:
11 | """
12 | Get data from feed and publish it to rabbitmq for interop with external systems
13 | """
14 |
15 | def __init__(self, feed, rabbit_host: str):
16 | self._logger = logging.getLogger(__name__)
17 | self._feed = feed
18 |
19 | # Subscribe to feed
20 | self.callbacks = {
21 | # MsgId.QUOTES: self._on_quotes,
22 | MsgId.GRAPH: self.on_candle,
23 | # MsgId.LEVEL2: self._on_level2,
24 | }
25 | self._feed.subscribe_feed(Asset.any_asset(), self)
26 |
27 | # Init rabbitmq connection
28 | self._logger.info(f"Connecting to rabbit host {rabbit_host}")
29 | self._rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(rabbit_host))
30 | self._rabbit_channel = self._rabbit_connection.channel()
31 | for q in [QueueName.CANDLES]:
32 | self._logger.info(f"Declaring rabbit queue {q}")
33 | self._rabbit_channel.queue_declare(queue=q, durable=True)
34 |
35 | def on_candle(self, ohlcv: Ohlcv):
36 | """
37 | Receive ohlc data and transfer to rabbit mq for interop
38 | :param data: dict like {"msgid":21016,"graph":{"QJSIM\u00A6SBER\u00A60":[{"d":"2019-10-01
39 | 10:02:00","o":22649,"c":22647,"h":22649,"l":22646,"v":1889}]}} :return:
40 | """
41 |
42 | self._logger.debug(f'Got candle: {ohlcv}')
43 | # ohlcv = {'d': str(dt), 'o': ohlcv, 'h': h, 'l': l_, 'c': c, 'v': asset}
44 | # self._rabbit_channel.basic_publish(exchange='', routing_key=QueueName.CANDLES, body=str(ohlcv))
45 | asset_ohlcv = {'asset': str(ohlcv.asset), 'dt': str(ohlcv.dt), 'o': ohlcv.o, 'h': ohlcv.h, 'l': ohlcv.l, 'c': ohlcv.c,
46 | 'v': ohlcv.v}
47 | self._rabbit_channel.basic_publish(exchange='', routing_key=QueueName.CANDLES, body=str(asset_ohlcv))
48 |
--------------------------------------------------------------------------------
/www/src/broker/MoneyLimits.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react'
2 |
3 |
4 | /**
5 | * Money limits (positions on assets) from Quik
6 | */
7 | class MoneyLimits extends Component{
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 | // Get stomp client for rabbit connection
13 | this.stompClient = props.stompClient;
14 | this.queueName='pytrade.broker.money.limits';
15 |
16 | // Money limit msg sample: {'msgid': 21004, 'mid': 13800, 'valut': 'SUR', 'tag': 'RTOD', 'cbal': 300000, 'clim': 0, 'obal': 300000, 'olim': 0, 'block': 0, 'ucode': '10815', 'status': 1, 'firmid': 'MB1000100000', 'limit_kind': 0, 'qty_scale': 2}
17 | this.state={moneyLimits: []};
18 | }
19 |
20 | onConnect(){
21 | // Subscribe to rabbit queue
22 | console.log('Subscribing to '+ this.queueName);
23 | this.stompClient.subscribe(this.queueName, this.onMoneyLimits.bind(this),{});
24 | }
25 |
26 | /**
27 | * Got info
28 | */
29 | onMoneyLimits(msg) {
30 | console.log('Got msg '+ msg.body)
31 |
32 | // Update stock limit map with new position
33 | var moneyLimits = this.state.moneyLimits
34 | moneyLimits.push(JSON.parse(msg.body.replace(/'/g, '"')));
35 | this.setState({"moneyLimits": moneyLimits});
36 |
37 | }
38 |
39 | render() {
40 | // {'msgid': 21004, 'mid': 13800, 'valut': 'SUR', 'tag': 'RTOD', 'cbal': 300000, 'clim': 0, 'obal': 300000, 'olim': 0, 'block': 0, 'ucode': '10815', 'status': 1, 'firmid': 'MB1000100000', 'limit_kind': 0, 'qty_scale': 2}
41 | return (
42 |
43 |
44 |
45 |
46 | | asset | count | price |
47 |
48 | {Array.from(this.state.moneyLimits.values()).map(v=>
49 | | {v.scode} | {v.cbal} | {v.avg} |
50 |
)}
51 |
52 |
53 | );
54 | }
55 | }
56 | export default StockLimits;
--------------------------------------------------------------------------------
/tests/connector/test_CsvFeedConnector.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import pandas as pd
4 |
5 | from pytrade.connector.CsvFeedConnector import CsvFeedConnector
6 | from datetime import datetime
7 |
8 | from model.feed.Asset import Asset
9 |
10 |
11 | class TestCsvFeedConnector(TestCase):
12 | def test_level2_of(self):
13 | # Set input
14 | dt = datetime.fromisoformat("2021-11-14 10:00")
15 | data = {'datetime': [dt, dt],
16 | 'ticker': ['stock1/ticker1', 'stock1/ticker1'],
17 | 'price': [1, 11],
18 | 'bid_vol': [2, 22],
19 | 'ask_vol': [3, 33]}
20 | data = pd.DataFrame(data).to_numpy()
21 |
22 | # Process
23 | level2 = CsvFeedConnector._level2_of(data)
24 |
25 | # Assert
26 | self.assertEqual(level2.asset, Asset("stock1", "ticker1"))
27 | self.assertEqual(level2.dt, dt)
28 | self.assertEqual([item.price for item in level2.items], [1, 11])
29 | self.assertEqual([item.bid_vol for item in level2.items], [2, 22])
30 | self.assertEqual([item.ask_vol for item in level2.items], [3, 33])
31 |
32 | def test__quote_of(self):
33 | # Set input
34 | dt = datetime.now()
35 | data = {'ticker': str(Asset("stock1", "name1")), 'bid': 1, 'ask': 2, 'last': 3, 'last_change': 4}
36 | # Call
37 | quote = CsvFeedConnector._quote_of(dt, data)
38 | # Assert
39 | self.assertEqual(quote.dt, dt)
40 | self.assertEqual(quote.asset, Asset.of(data['ticker']))
41 | self.assertEqual(quote.bid, data['bid'])
42 | self.assertEqual(quote.ask, data['ask'])
43 | self.assertEqual(quote.last, data['last'])
44 | self.assertEqual(quote.last_change, data['last_change'])
45 |
46 | def test__ohlcv_of(self):
47 | # Set input
48 | dt = datetime.now()
49 | data = {'ticker': str(Asset("stock1", "name1")), 'open': 1, 'high': 2, 'low': 3, 'close': 4, 'volume': 5}
50 | # Call
51 | candle = CsvFeedConnector._ohlcv_of(dt, data)
52 | # Assert
53 | self.assertEqual(candle.dt, dt)
54 | self.assertEqual(candle.asset, Asset.of(data['ticker']))
55 | self.assertEqual(candle.o, data['open'])
56 | self.assertEqual(candle.h, data['high'])
57 | self.assertEqual(candle.l, data['low'])
58 | self.assertEqual(candle.c, data['close'])
59 | self.assertEqual(candle.v, data['volume'])
60 |
--------------------------------------------------------------------------------
/tests/strategy/features/test_Level2Features.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from unittest import TestCase
3 |
4 | import pandas as pd
5 |
6 | from pytrade.strategy.features.Level2Features import Level2Features
7 |
8 |
9 | class TestLevel2Features(TestCase):
10 | def test_level2_features__equal_buckets(self):
11 | # datetime, price, ask_vol, bid_vol
12 | # Each bucket has one item
13 | # todo: set index as in feed
14 | asks = [{'asset': 'asset1', 'datetime': datetime.fromisoformat('2021-11-26 17:39:00'), 'price': i, 'ask_vol': 1,
15 | 'bid_vol': None} for i in range(10, 20)]
16 | bids = [
17 | {'asset': 'asset1', 'datetime': datetime.fromisoformat('2021-11-26 17:39:00'), 'price': i, 'ask_vol': None,
18 | 'bid_vol': 1} for i in range(0, 10)]
19 | data = pd.DataFrame(asks+bids)
20 | # Call
21 | features = Level2Features().level2_buckets(data).values.tolist()
22 |
23 | # Assert all features should be 1.0
24 | self.assertEqual([[1.0] * 20], features)
25 |
26 | def test_level2_absent_levels(self):
27 | # datetime, price, ask_vol, bid_vol
28 | # Not all level buckets are present
29 | data = pd.DataFrame([
30 | {'datetime': datetime.fromisoformat('2021-11-26 17:39:00'), 'price': 0.9, 'ask_vol': 1, 'bid_vol': None},
31 | {'datetime': datetime.fromisoformat('2021-11-26 17:39:00'), 'price': 0.9, 'ask_vol': 1, 'bid_vol': None},
32 | {'datetime': datetime.fromisoformat('2021-11-26 17:39:00'), 'price': -0.9, 'ask_vol': None, 'bid_vol': 1},
33 | {'datetime': datetime.fromisoformat('2021-11-26 17:39:00'), 'price': -0.9, 'ask_vol': None, 'bid_vol': 1}
34 | ])
35 |
36 | features = Level2Features().level2_buckets(data, l2size=20, buckets=20)
37 | lst = features.values.tolist()
38 |
39 | # All features should be 1.0
40 | self.assertEqual([[0, 0, 0, 0, 0, 0, 0, 0, 0, 2.0, 2.0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], lst)
41 | self.assertSequenceEqual(
42 | ['l2_bucket_-10', 'l2_bucket_-9', 'l2_bucket_-8', 'l2_bucket_-7',
43 | 'l2_bucket_-6', 'l2_bucket_-5', 'l2_bucket_-4', 'l2_bucket_-3',
44 | 'l2_bucket_-2', 'l2_bucket_-1', 'l2_bucket_0', 'l2_bucket_1',
45 | 'l2_bucket_2', 'l2_bucket_3', 'l2_bucket_4', 'l2_bucket_5',
46 | 'l2_bucket_6', 'l2_bucket_7', 'l2_bucket_8', 'l2_bucket_9'],
47 | features.columns.tolist())
48 |
--------------------------------------------------------------------------------
/www/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/www/src/broker/TradeAccount.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react'
2 |
3 |
4 | /**
5 | * Trade account information
6 | */
7 | class TradeAccount extends Component{
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 | // Get stomp client for rabbit connection
13 | this.stompClient = props.stompClient;
14 |
15 | this.tradeAccountQueue='pytrade.broker.trade.account';
16 |
17 |
18 | this.state={tradeAccounts: new Map()};
19 |
20 | // this.state.tradeAccounts.set('trdacc', "{'trdacc':'acc1'}")
21 | // this.state.tradeAccounts.set('trdacc', "{'trdacc':'acc2'}")
22 | }
23 |
24 | onConnect(){
25 | // Subscribe to rabbit queue
26 | console.log('Subscribing to '+ this.tradeAccountQueue);
27 | this.stompClient.subscribe(this.tradeAccountQueue, this.onTradeAccount.bind(this),{});
28 | }
29 |
30 | /**
31 | * Got trade account info
32 | */
33 | onTradeAccount(msg) {
34 | // {'msgid': 21022, 'trdacc': 'NL0011100043', 'firmid': 'NC0011100000', 'classList': ['QJSIM'], 'mainMarginClasses': ['QJSIM', 'SPBFUT'], 'limitsInLots': 0, 'limitKinds': ['0', '1', '2']}
35 | console.log('Got trade account msg '+ msg.body)
36 | var tradeAccountMap = this.state.tradeAccounts;
37 | var msgData = JSON.parse(msg.body.replace(/'/g, '"'));
38 |
39 | tradeAccountMap.set(msgData["trdacc"], msgData);
40 |
41 | this.setState({tradeAccounts: tradeAccountMap});
42 | }
43 |
44 | render() {
45 | // var body = "{'msgid': 21022, 'trdacc': 'NL0011100043', 'firmid': 'NC0011100000', 'classList': ['QJSIM'], 'mainMarginClasses': ['QJSIM', 'SPBFUT'], 'limitsInLots': 0, 'limitKinds': ['0', '1', '2']}"
46 | // var msgData = JSON.parse(body.replace(/'/g, '"'));
47 | // this.state.tradeAccounts.set(msgData.trdacc,msgData);
48 |
49 |
50 | // var body = "{'msgid': 21022, 'trdacc': '2NL0011100043', 'firorders.itemsmid': 'NC0011100000', 'classList': ['QJSIM'], 'mainMarginClasses': ['QJSIM', 'SPBFUT'], 'limitsInLots': 0, 'limitKinds': ['0', '1', '2']}"
51 | // var msgData = JSON.parse(body.replace(/'/g, '"'));
52 | // this.state.tradeAccounts.set(msgData.trdacc,msgData);
53 |
54 |
55 | var lst = Array.from(this.state.tradeAccounts.values());
56 | return (
57 |
58 |
59 |
60 | {Array.from(lst).map(v=> - {v.trdacc}
)}
61 |
62 |
63 |
64 | );
65 | }
66 | }
67 | export default TradeAccount;
--------------------------------------------------------------------------------
/www/src/broker/BuySell.js:
--------------------------------------------------------------------------------
1 | import './Broker.css'
2 | import React,{Component} from 'react'
3 |
4 |
5 | /**
6 | * Buy or sell order fill in and send
7 | */
8 | class BuySell extends Component{
9 |
10 | constructor(props) {
11 | super(props);
12 |
13 | // Asset to trade
14 | // todo: parameterise
15 | this.secClass = 'QJSIM';
16 | this.secCode= 'SBER';
17 |
18 | // Get stomp client for rabbit connection
19 | this.stompClient = props.stompClient;
20 | this.queueName='pytrade.broker.cmd.buysell';
21 | this.quantity=React.createRef();
22 |
23 | this.state={quantity: 1, price:0.0};
24 | }
25 | // updateQuantity(e) {
26 | // this.setState({'quantity':e.target.value})
27 | // }
28 |
29 | buy(e) {
30 | console.log("Buy pressed");
31 | var msg = {"operation": "buy", "secClass": this.secClass, "secCode": this.secCode, "quantity": this.state.quantity, "price": Number(this.state.price)};
32 | this.stompClient.send(this.queueName, {'auto-delete':true}, JSON.stringify(msg), );
33 | }
34 | sell(e) {
35 | console.log("Sell pressed");
36 | var msg = {"operation": "sell", "secClass": this.secClass, "secCode": this.secCode, "quantity": this.state.quantity, "price": Number(this.state.price)};
37 | this.stompClient.send(this.queueName, {'auto-delete':true}, JSON.stringify(msg));
38 | }
39 |
40 | render() {
41 |
42 | return (
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | this.setState({'price':event.target.value})}>
52 |
53 |
54 |
55 | this.setState({ 'quantity': event.target.value})}>
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | );
64 | }
65 | }
66 | export default BuySell;
--------------------------------------------------------------------------------
/pytrade/connector/quik/MsgId.py:
--------------------------------------------------------------------------------
1 | class MsgId:
2 | """
3 | Message id constants
4 | """
5 | # Responce codes starts from 2 {mapIdName:{20000:"Авторизация",20001:"Запрос PIN",20002:"Ответ на запрос длины
6 | # пароля",20003:"Ответ на запрос смены пароля",20004:"Сообщения авторизации",20005:"Результат сохранения
7 | # профиля",20008:"Статус соединения",20014:"Сообщения сервера",21000:"Классы",21001:"Заявки",21003:"Сделки",
8 | # 21004:"Денежн.лимиты",21005:"Бумаж.лимиты",21006:"Ограничения",21007:"Позиции",21008:"Сообщения брокера",
9 | # 21009:"Ответы на транзакции",21011:"Основные торги",21011:"Торги для ТТП",21012:"Купить/Продать",
10 | # 21013:"Портфель",21014:"Стаканы",21015:"Ответ по RFS",21016:"Графики",21017:"Новости",21018:"Тексты новостей",
11 | # 21019:"Валютные пары",21020:"Инфо по бумаге",21021:"Котировки RFS",21022:"Торговые счета",21023:"Торги по
12 | # вал.парам",21024:"Список вал.пар",22000:"Ответ по заявке",22001:"Ответ по стоп-заявке",22002:"Ответ по
13 | # связ.стоп-заявке",22003:"Ответ по услов.стоп-заявке",22004:"Ответ по FX-заявке",22100:"Ответ по снятию заявки",
14 | # 22101:"Ответ по снятию стоп-заявки"},
15 |
16 | PIN_REQ = 20001
17 | AUTH = 20006
18 | STATUS = 20008
19 | TRADE_SESSION_OPEN = 20000
20 | SERVER_MSG = 20014
21 | EXIT = 10006
22 | CREATE_DATASOURCE = 11016
23 | CREATE_LEVEL2_DATASOURCE = 11014
24 | QUOTES = 21011
25 | GRAPH = 21016
26 | LEVEL2 = 21014 # 21014:"Стаканы"
27 |
28 | # Broker messages ???
29 | ORDERS = 21001 # 21001:"Заявки",
30 | TRADES = 21003 # 21003:"Сделки",
31 | LIMITS = 21006 # 21006: "Ограничения",
32 | MONEY_LIMITS = 21004 # 21004:"Денежн.лимиты"
33 | STOCK_LIMITS = 21005 # 21005:"Бумаж.лимиты",
34 | POSITIONS = 21007 # 21007:"Позиции"
35 | TRANS_REPLY = 21009 # 21009:"Ответы на транзакции"
36 | BUYSELL = 21012 #:"Купить/Продать",
37 | PORTFOLIO = 21013 # 21013:"Портфель",
38 | TRADE_ACCOUNTS = 21022 # 21022:"Торговые счета",
39 | TRADES_FX = 21023 # 21023:"Торги по # вал.парам"
40 | LIMIT_HAS_RECEIVED = 21063 # LimitHasReceived = 21063
41 | # MSG_ID_LEVEL2 = 21014, # 21014:"Стаканы"
42 |
43 | ORDER = 12000
44 | ORDER_REPLY = 22000 # 22000:"Ответ по заявке"
45 | STOP_ORDER_REPLY = 22001 # 22001:"Ответ по стоп-заявке"
46 | LINKED_STOP_ORDER_REPLY = 22002 # 22002:"Ответ по связ.стоп-заявке"
47 | CONDITIONAL_STOP_ORDER_REPLY = 22003 # 22003:"Ответ по услов.стоп-заявке"
48 | FX_ORDER_REPLY = 22004 # ,22004:"Ответ по FX-заявке",
49 | REMOVE_ORDER_REPLY = 22100 # 22100:"Ответ по снятию заявки",
50 | REMOVE_STOP_ORDER_REPLY = 22101 # 22101:"Ответ по снятию стоп-заявки"
51 |
52 | HEARTBEAT = "heartbeat"
53 |
--------------------------------------------------------------------------------
/www/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: left;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-link {
17 | color: #61dafb;
18 | }
19 |
20 | @keyframes App-logo-spin {
21 | from {
22 | transform: rotate(0deg);
23 | }
24 | to {
25 | transform: rotate(360deg);
26 | }
27 | }
28 |
29 | html, body {
30 | height: 100%;
31 | color:#5a5900;
32 | background-color:rgb(254, 255, 244);
33 | }
34 | .App-header {
35 | justify-content: center;
36 | font-family: cursive, sans-serif;
37 | font-size:2em;
38 | font-weight: bold;
39 | padding: 20px;
40 | background-color: rgb(222 220 131)
41 | }
42 |
43 | header {
44 | text-align: center;
45 | font-weight: bold;
46 | }
47 |
48 | Main{
49 | display: grid;
50 | /* grid-template-columns: 15% 15% 100% 15%; */
51 |
52 | }
53 | .rawmsg.panel {
54 | /* grid-row: 1;
55 | grid-column-start:1;
56 | grid-column-end: 5; */
57 |
58 | /* grid-column-start:1;
59 | grid-column-end: 5; */
60 | }
61 | .trdacc.panel {
62 | /* grid-row-start: 2;
63 | grid-row-end:4 */
64 | /* width:fit-content; */
65 | }
66 |
67 | .stocklimits.panel{
68 | /* grid-row-start:2;
69 | grid-row-end:4;
70 | grid-column:2; */
71 | /* width:fit-content; */
72 | }
73 | .pricechart.panel{
74 | /* grid-row-start: 2;
75 | grid-row-end:4; */
76 | /* width: auto; */
77 | }
78 | .orders.panel {
79 | /* grid-row: 2;
80 | grid-column:4 */
81 | }
82 | .buysell.panel{
83 | /* grid-row: 3;
84 | grid-column:4; */
85 | }
86 |
87 | .panel{
88 | margin:0.5em;
89 | padding:0.5em 1.5em 0.5em 1.5em;
90 | border-style: dotted;
91 | border-width: thin;
92 | background-color:rgb(252, 251, 216);
93 | border-radius: 5px;
94 | }
95 | .panel header{
96 | padding-bottom:0.7em;
97 | }
98 | table {
99 | border-spacing: 0;
100 | border-top-style:dotted;
101 | border-top-width:thin;
102 | }
103 | th, td {
104 | border-color: #5a5900;
105 | border-style: dotted;
106 | /* border-left-style: none;
107 | border-right-style: none;
108 | border-top-style:none; */
109 | border-width: thin;
110 | padding: 0.2em 1em 0.2em 1em;
111 | }
112 | th {
113 | background-color:#5a5900;
114 | color: #f5e8b0;
115 | }
116 |
117 | .trade-account li{
118 | list-style-type:none;
119 | }
120 | ul{
121 | padding-inline-start: 0px;
122 | }
123 |
124 | input {
125 | margin:0.5em
126 | }
127 | button {
128 | margin:0.2em;
129 | margin-left: 1em;
130 | padding: 0.2em 1em 0.2em 1em;
131 | background-color:#5a5900;
132 | color: #f5e8b0;
133 | border-radius: 5px;
134 |
135 | }
136 |
137 |
--------------------------------------------------------------------------------
/www/src/broker/Orders.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react'
2 |
3 |
4 | /**
5 | * Opened orders information
6 | */
7 | class Orders extends Component{
8 |
9 | constructor(props) {
10 | super(props);
11 |
12 | // Get stomp client for rabbit connection
13 | this.stompClient = props.stompClient;
14 | this.queueName='pytrade.broker.orders';
15 | this.state={orders: new Map()};
16 | }
17 |
18 | onConnect(){
19 | // Subscribe to rabbit queue
20 | console.log('Subscribing to '+ this.queueName);
21 | this.stompClient.subscribe(this.queueName, this.onOrders.bind(this),{});
22 | }
23 |
24 | /**
25 | * Got orders info
26 | */
27 | onOrders(msg) {
28 | console.log('Got orders msg '+ msg.body)
29 | var orders = this.state.orders
30 | var msgData = JSON.parse(msg.body.replace(/'/g, '"'));
31 | orders.set(msgData['number'],msgData)
32 | this.setState({orders: orders});
33 | }
34 |
35 | render() {
36 | //2021-04-16 19:56:38,164.164 DEBUG WebQuikBroker - on_orders: On orders. msg={'msgid': 21001, 'qdate': 20210416, 'qtime': 195529, 'ccode': 'QJSIM', 'scode': 'SBER', 'sell': 0, 'account': 'NL0011100043', 'price': 28250, 'qty': 1, 'volume': 282500, 'balance': 0, 'yield': 0, 'accr': 0, 'refer': '10058//', 'type': 24, 'firm': 'NC0011100000', 'ucode': '10058', 'number': '5830057748', 'status': 2, 'price_currency': '', 'settle_currency': ''}
37 | // class Order:
38 | // number: str
39 | // dt: datetime
40 | // class_code: str
41 | // sec_code: str
42 | // is_sell: bool
43 | // account: str
44 | // price: float
45 | // quantity: int
46 | // volume: float
47 | // status: str
48 | return (
49 |
50 |
51 |
52 |
53 | | number |
54 | dt |
55 | asset |
56 | type |
57 | account |
58 | price |
59 | quantity |
60 | volume |
61 | status |
62 |
63 | {Array.from(this.state.orders.values()).map(v=>
64 | | {v.number} |
65 | {v.dt} |
66 | {v.class_code}/{v.sec_code} |
67 | {v.is_sell ? "sell": "buy"} |
68 | {v.account} |
69 | {v.price} |
70 | {v.quantity} |
71 | {v.volume} |
72 | {v.status} |
73 |
)}
74 |
75 |
76 |
77 | );
78 | }
79 | }
80 | export default Orders;
--------------------------------------------------------------------------------
/www/src/App.js:
--------------------------------------------------------------------------------
1 | import logo from './logo.svg';
2 | import './App.css';
3 | import {stompConfig} from './stompConfig'
4 | import Stomp from 'stompjs'
5 | import PriceChart from './pricechart/PriceChart.js';
6 | import TradeAccount from './broker/TradeAccount.js';
7 | import Orders from './broker/Orders.js';
8 | import StockLimits from './broker/StockLimits.js';
9 | import BuySell from './broker/BuySell';
10 | import RawMsg from './broker/RawMsg'
11 | import Replies from './broker/Replies';
12 |
13 | import React,{Component} from 'react'
14 |
15 |
16 |
17 | class App extends React.Component {
18 |
19 | constructor(props) {
20 | super(props);
21 | this.rawMsg = React.createRef()
22 | this.replies = React.createRef()
23 | this.priceChart = React.createRef();
24 | this.tradeAccount = React.createRef();
25 | this.orders = React.createRef();
26 | this.stockLimits = React.createRef();
27 | this.buySell = React.createRef();
28 | // Initialize stomp
29 | this.stompConfig = stompConfig;
30 | console.log('Creating stomp client over web socket: ' + this.stompConfig.brokerURL)
31 | this.stompClient = Stomp.over(new WebSocket(this.stompConfig.brokerURL))
32 | }
33 |
34 | componentDidMount() {
35 | // Connect to rabbit
36 | console.log('Connecting to rabbit')
37 | this.stompClient.connect(this.stompConfig.connectHeaders, this.onConnect.bind(this), this.onError)
38 | }
39 |
40 | componentWillUnmount() {
41 | // Disconnect from rabbit on exit
42 | console.log('Disconnecting')
43 | this.stompClient.disconnect();
44 | }
45 |
46 |
47 | onConnect(){
48 | console.log('Connected');
49 | // Call child components' handlers
50 | this.orders.current.onConnect.bind(this.orders.current)();
51 | this.priceChart.current.onConnect.bind(this.priceChart.current)();
52 | this.tradeAccount.current.onConnect.bind(this.tradeAccount.current)();
53 | this.stockLimits.current.onConnect.bind(this.stockLimits.current)();
54 | //this.rawMsg.current.onConnect.bind(this.rawMsg.current)();
55 | this.replies.current.onConnect.bind(this.replies.current)();
56 | }
57 |
58 | onError(e){
59 | console.log('Connection error: '+e)
60 | }
61 |
62 |
63 | render(){
64 | return (
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 | }
81 |
82 | export default App;
83 |
--------------------------------------------------------------------------------
/tests/strategy/features/test_TargetFeatures.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from unittest import TestCase
3 |
4 | import pandas as pd
5 |
6 | from pytrade.strategy.features.TargetFeatures import TargetFeatures
7 |
8 |
9 | class TestTargetFeatures(TestCase):
10 |
11 | def test_target_features__window_should_include_current(self):
12 | # quotes columns: ['datetime', 'ticker', 'bid', 'ask', 'last', 'last_change']
13 | quotes = pd.DataFrame([{'datetime': datetime.fromisoformat('2021-12-08 07:00:00'), 'ticker': 'asset1', 'bid': 1,
14 | 'ask': 10, 'last': 5},
15 | {'datetime': datetime.fromisoformat('2021-12-08 07:01:01'), 'ticker': 'asset1', 'bid': 4,
16 | 'ask': 6, 'last': 5}
17 | ]).set_index(['datetime','ticker'])
18 |
19 | withminmax = TargetFeatures().min_max_future(quotes, 1, 'min')
20 | self.assertEqual([1, 4], withminmax['fut_bid_min'].values.tolist())
21 | self.assertEqual([10, 6], withminmax['fut_ask_max'].values.tolist())
22 |
23 | def test_target_features__window_should_include_right_bound(self):
24 | # quotes columns: ['datetime', 'ticker', 'bid', 'ask', 'last', 'last_change']
25 | quotes = pd.DataFrame([{'datetime': datetime.fromisoformat('2021-12-08 07:00:00'), 'ticker': 'asset1', 'bid': 4,
26 | 'ask': 6, 'last': 5},
27 | {'datetime': datetime.fromisoformat('2021-12-08 07:00:59'), 'ticker': 'asset1', 'bid': 1,
28 | 'ask': 10, 'last': 5}
29 | ]).set_index(['datetime','ticker'])
30 |
31 | withminmax = TargetFeatures().min_max_future(quotes, 1, 'min')
32 | self.assertEqual([1, 1], withminmax['fut_bid_min'].values.tolist())
33 | self.assertEqual([10, 10], withminmax['fut_ask_max'].values.tolist())
34 |
35 | def test_target_features__(self):
36 | # quotes columns: ['datetime', 'ticker', 'bid', 'ask', 'last', 'last_change']
37 | quotes = pd.DataFrame([
38 | {'datetime': datetime.fromisoformat('2021-11-26 17:00:00'), 'ticker': 'asset1', 'bid': 4, 'ask': 6},
39 | {'datetime': datetime.fromisoformat('2021-11-26 17:00:30'), 'ticker': 'asset1', 'bid': 1, 'ask': 10},
40 | {'datetime': datetime.fromisoformat('2021-11-26 17:00:59'), 'ticker': 'asset1', 'bid': 3, 'ask': 7},
41 | {'datetime': datetime.fromisoformat('2021-11-26 17:01:58'), 'ticker': 'asset1', 'bid': 2, 'ask': 8},
42 |
43 | {'datetime': datetime.fromisoformat('2021-11-26 17:03:00'), 'ticker': 'asset1', 'bid': 4, 'ask': 6},
44 | {'datetime': datetime.fromisoformat('2021-11-26 17:04:00'), 'ticker': 'asset1', 'bid': 3, 'ask': 7},
45 | {'datetime': datetime.fromisoformat('2021-11-26 17:05:00'), 'ticker': 'asset1', 'bid': 4, 'ask': 6},
46 | {'datetime': datetime.fromisoformat('2021-11-26 17:06:00'), 'ticker': 'asset1', 'bid': 3, 'ask': 7}
47 | ]).set_index(['datetime', 'ticker'])
48 |
49 | withminmax = TargetFeatures().min_max_future(quotes, 1, 'min')
50 |
51 | self.assertEqual([1, 1, 2, 2, 4, 3, 4, 3], withminmax['fut_bid_min'].values.tolist())
52 | self.assertEqual([10, 10, 8, 8, 6, 7, 6, 7], withminmax['fut_ask_max'].values.tolist())
53 |
--------------------------------------------------------------------------------
/www/src/pricechart/PriceChart.js:
--------------------------------------------------------------------------------
1 | import './pricechart.css'
2 | import React,{Component} from 'react'
3 | import {stompConfig} from '../stompConfig'
4 | import Stomp from 'stompjs'
5 | import Plot from 'react-plotly.js'
6 | import Plotly from 'react-plotly.js'
7 |
8 |
9 | class PriceChart extends Component{
10 | graph = {
11 | data: [
12 | { x: [],
13 | y:[],
14 | // Candlestick chart does not look well on test data where o=c=h=l etc
15 | type: 'scatter',
16 | line: {
17 | color: '#ff8300'
18 | }
19 | }
20 | ],
21 | layout: { title: 'Price',
22 | xaxis: { autorange: true},
23 | yaxis: { autorange: true}
24 | }
25 | };
26 |
27 | constructor(props) {
28 | super(props);
29 | this.plotly = React.createRef();
30 |
31 | // Get stomp client for rabbit connection
32 | this.stompClient = props.stompClient;
33 |
34 | this.candlesQueue='pytrade.feed.candles';
35 | this.state={lastCandle:{d:null,c:null}, data: this.graph.data, layout: this.graph.layout, candles: []};
36 | }
37 |
38 | /***
39 | * Got new candle event handler
40 | */
41 | onCandle(msg){
42 | console.log('Got message: '+msg);
43 | var ohlcv = JSON.parse(msg.body.replace(/'/g, '"'))
44 | ohlcv.millis = Date.parse(ohlcv.dt);
45 | // Update last candle state
46 | if(ohlcv == null){
47 | // If badly parsed
48 | this.setState({lastCandle: {d:null,c:null}});
49 | return;
50 | } else {
51 | this.setState({lastCandle: {d:ohlcv.dt, c:ohlcv.c}});
52 | }
53 |
54 | var candles = this.state.candles;
55 |
56 | // If time is the same, remove last candle. A new one will be added.
57 | var lastTime = candles.map(c => c.dt)[candles.length-1];
58 | if(ohlcv.dt === lastTime) {
59 | candles.pop();
60 | }
61 |
62 | // Add received candle and sort all by time
63 | candles.push(ohlcv);
64 | candles = candles.sort((c1,c2)=>{
65 | return c1.millis - c2.millis
66 | });
67 |
68 | // Update the state for chart
69 | this.state.data[0].x = candles.map(c=>c.dt);
70 | this.state.data[0].y = candles.map(c=>c.c);
71 | var tmpData = this.state.data
72 |
73 | // To update plotly, we need to change then return it back
74 | this.setState({data: []});
75 | this.setState({data: tmpData});
76 | }
77 |
78 | onConnect(){
79 | // Subscribe to rabbit queue
80 | console.log('Subscribing to '+ this.candlesQueue);
81 | this.stompClient.subscribe(this.candlesQueue, this.onCandle.bind(this), {});
82 | }
83 |
84 | render() {
85 | return (
86 |
87 |
88 | Last candle: {this.state.lastCandle.d}, price: {this.state.lastCandle.c}
89 |
90 |
93 |
94 | );
95 | }
96 | }
97 | export default PriceChart;
--------------------------------------------------------------------------------
/www/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/pytrade/feed/Feed2Csv.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import logging
3 | import datetime as dt
4 | import os
5 | from collections import defaultdict
6 |
7 | import pandas as pd
8 | from pandas import DataFrame
9 | from feed.Feed import Feed
10 | from model.feed.Asset import Asset
11 |
12 |
13 | class Feed2Csv:
14 | """
15 | Receive ticks and level 2 and persist to csv
16 | """
17 | def __init__(self, feed: Feed, data_dir: str = './data'):
18 | self._logger = logging.getLogger(__name__)
19 | self._feed = feed
20 | self._feed.subscribe_feed(Asset("*","*"), self)
21 |
22 | # Dump periodically
23 | self._write_interval = dt.timedelta(seconds=30)
24 | self._data_dir = data_dir
25 | self._logger.info("Candles and level2 will be persisted each %s to %s", self._write_interval, self._data_dir)
26 | self._last_write_time = dt.datetime.min
27 | self._last_processed_times = defaultdict(lambda: pd.Timestamp.min)
28 | self.is_writing = False
29 |
30 | def write(self, df, tag):
31 | """
32 | Group dataframe by ticker and day. Write each group to separate file
33 | :param df: dataframe to group and write
34 | :param tag: text tag, will be appended to file name
35 | """
36 | if self.is_writing:
37 | self._logger.debug(f"Previous {tag} writing is still in progress, exiting.")
38 | return
39 | try:
40 | self.is_writing = True
41 | self._logger.debug(f"Writing {tag} to csv")
42 |
43 | # Get the data, not saved previously
44 | last_processed_time = self._last_processed_times[tag]
45 | df = df[df.index.get_level_values(0) > last_processed_time]
46 |
47 | # Append to csv if new data exists
48 | if not df.empty:
49 | df.groupby([df.index.get_level_values(0).dayofyear, df.index.get_level_values(1)]).apply(
50 | lambda df_daily: self._write_ticker_by_day(df_daily, tag))
51 | self._last_processed_times[tag] = df.index.get_level_values(0).max()
52 | else:
53 | self._logger.debug(f"{tag} is empty, nothing to write")
54 | except Exception as ex:
55 | self._logger.error(ex)
56 | finally:
57 | self.is_writing = False
58 |
59 | def _write_ticker_by_day(self, df: DataFrame, data_type):
60 | """
61 | Compose file name from ticker and
62 | :param df: Dataframe, all rows have the same day and ticker in index
63 | :param data_type: tag to use in file name
64 | """
65 | # Form file name
66 | date = df.first_valid_index()[0].date()
67 | ticker = df.first_valid_index()[1].replace('/','_')
68 | df.first_valid_index()[1].replace('/', '_')
69 | file_name = '%s_%s_%s.csv' % (ticker, data_type, date.strftime('%Y-%m-%d'))
70 | file_path = os.path.join(self._data_dir, file_name)
71 | self._logger.debug("Writing %s to %s", data_type, os.path.abspath(file_path))
72 | # Write to file
73 | df.to_csv(file_path, mode='a', header=False)
74 |
75 | def on_heartbeat(self):
76 | """
77 | Heartbeat received
78 | """
79 | self._logger.debug("Got heartbeat")
80 | # If time interval elapsed, write dataframe to csv
81 | if (dt.datetime.now() - self._last_write_time) < self._write_interval:
82 | return
83 | self._logger.debug("Writing to csv")
84 | self._last_write_time = dt.datetime.now()
85 | self.write(self._feed.level2, 'level2')
86 | self.write(self._feed.candles, 'candles')
87 | self.write(self._feed.quotes, 'quotes')
88 | self._last_write_time = dt.datetime.now()
89 |
--------------------------------------------------------------------------------
/pytrade/strategy/features/Level2Features.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import numpy as np
3 |
4 |
5 | class Level2Features:
6 | """
7 | Level2 feature engineering
8 | """
9 |
10 | def level2_buckets(self, level2: pd.DataFrame, l2size: int = 0, buckets: int = 20) -> pd.DataFrame:
11 | """
12 | Return dataframe with level2 feature columns. Colums are named "bucket"
13 | where n in a number of price interval and value is summary volumes inside this price.
14 | For ask price intervals number of buckets >0, for bid ones < 0
15 | level2: DataFrame with level2 tick columns: datetime, price, bid_vol, ask_vol
16 | level2 price and volume for each time
17 | """
18 | # Assign bucket number for each level2 item
19 | level2.set_index("datetime")
20 | level2 = self.assign_bucket(level2, l2size, buckets)
21 |
22 | # Pivot buckets to feature columns: bucket_1, bucket_2 etc. with summary bucket's volume as value.
23 | maxbucket = buckets // 2 - 1
24 | minbucket = -buckets // 2
25 | askfeatures = self.pivot_buckets(level2, 'ask_vol', 0, maxbucket)
26 | bidfeatures = self.pivot_buckets(level2, 'bid_vol', minbucket, -1)
27 |
28 | # Ask + bid buckets
29 | level2features = bidfeatures.merge(askfeatures, on='datetime')
30 | return level2features
31 |
32 | def assign_bucket(self, level2: pd.DataFrame, l2size: int = 0, buckets: int = 20) -> pd.DataFrame:
33 | """
34 | To each level2 item set it's bucket number.
35 | l2size: max-min price across all level2 snapshots
36 | buckets: split level2 snapshots to this number of items, calculate volume inside each bucket
37 | """
38 | # Calc middle price between ask and bid
39 | level2 = level2.set_index("datetime")
40 | askmin = level2[level2['ask_vol'].notna()].groupby('datetime')['price'].min().reset_index().set_index(
41 | "datetime")
42 | level2['price_min'] = askmin['price']
43 | bidmax = level2[level2['bid_vol'].notna()].groupby('datetime')['price'].max().reset_index().set_index(
44 | "datetime")
45 | level2['price_max'] = bidmax['price']
46 | level2['price_middle'] = (askmin['price'] + bidmax['price']) / 2
47 |
48 | # Assign a bucket number to each level2 item
49 | # scalar level2 size and bucket size
50 | if not l2size:
51 | l2size = level2.groupby('datetime')['price'].agg(np.ptp).reset_index()['price'].median()
52 | # 10 ask steps + 10 bid steps
53 | # buckets = 20
54 | bucketsize = l2size / buckets
55 |
56 | # If price is too out, set maximum possible bucket
57 | level2['bucket'] = (level2['price'] - level2['price_middle']) // bucketsize
58 | maxbucket = buckets // 2 - 1
59 | minbucket = -buckets // 2
60 | level2['bucket'] = level2['bucket'].clip(upper=maxbucket, lower=minbucket)
61 | return level2
62 |
63 | def pivot_buckets(self, level2: pd.DataFrame, vol_col_name: str, minbucket: int, maxbucket: int) -> pd.DataFrame:
64 | """
65 | Pivot dataframe to make bucket columns with volume values
66 | """
67 | # Calculate volume inside each group
68 | grouped = level2[level2['bucket'].between(minbucket, maxbucket)].groupby(['datetime', 'bucket'])[
69 | vol_col_name].sum().reset_index(level=1)
70 |
71 | grouped['bucket'] = grouped['bucket'].astype(int)
72 | features = grouped.reset_index().pivot_table(index='datetime', columns='bucket', values=vol_col_name)
73 | # Add absent buckets (rare case)
74 | for col in range(minbucket, maxbucket + 1):
75 | if col not in features.columns:
76 | features[col] = 0
77 | features = features[sorted(features)]
78 |
79 | features.columns = ['l2_bucket_' + str(col) for col in features.columns]
80 | return features
81 |
--------------------------------------------------------------------------------
/tests/connector/quik/test_WebQuikFeed.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from unittest import TestCase
3 |
4 | from pytrade.connector.quik.WebQuikFeed import WebQuikFeed
5 | from pytrade.model.feed.Asset import Asset
6 |
7 |
8 | class TestWebQuikFeed(TestCase):
9 | def test_level2_of(self):
10 | # Sample of level2. {'msgid': 21014, 'quotes': {'QJSIM¦SBER': {'lines': {'22806':
11 | # {'b': 234, 's': 0, 'by': 0, 'sy': 0}, '22841': {'b': 437, 's': 0, 'by': 0, 'sy': 0},
12 | # '22853': {'b': 60, 's': 0, 'by': 0, 'sy': 0}, '22878': {'b': 82, 's': 0, 'by': 0, 'sy': 0},
13 | # '22886': {'b': 138, 's': 0, 'by': 0, 'sy': 0}, '22895': {'b': 1, 's': 0, 'by': 0, 'sy': 0},...
14 | data = {1: {'b': 4, 's': 7, 'by': 0, 'sy': 0},
15 | 2: {'b': 5, 's': 8, 'by': 0, 'sy': 0},
16 | 3: {'b': 6, 's': 9, 'by': 0, 'sy': 0}}
17 | level2 = WebQuikFeed._level2_of(datetime(2021, 7, 23, 12, 51), Asset("code1", "sec1"), data)
18 | self.assertEqual(level2.dt, datetime(2021, 7, 23, 12, 51))
19 | self.assertEqual(level2.asset, Asset("code1", "sec1"))
20 | self.assertEqual([1, 2, 3], [item.price for item in level2.items])
21 | self.assertEqual([4, 5, 6], [item.bid_vol for item in level2.items])
22 | self.assertEqual([7, 8, 9], [item.ask_vol for item in level2.items])
23 |
24 | def test_quote_of_empty(self):
25 | data = {}
26 | quote = WebQuikFeed._quote_of(None, data)
27 | self.assertIsNotNone(quote.dt)
28 | self.assertIsNone(quote.bid)
29 | self.assertIsNone(quote.ask)
30 | self.assertIsNone(quote.last)
31 | self.assertIsNone(quote.last_change)
32 |
33 | def test_quote_of(self):
34 | data = {"bid": 1, "offer": 2, "last": 3, "lastchange": 4}
35 | quote = WebQuikFeed._quote_of("stock1¦asset1", data)
36 | self.assertIsNotNone(quote.dt)
37 | self.assertEqual(Asset("stock1", "asset1"), quote.asset)
38 | self.assertEqual(1, quote.bid)
39 | self.assertEqual(2, quote.ask)
40 | self.assertEqual(3, quote.last)
41 | self.assertEqual(4, quote.last_change)
42 |
43 | def test_ohlcv_of(self):
44 | data = {"d": "2019-10-01 10:02:00", "o": 1, "c": 2, "h": 3, "l": 4, "v": 5}
45 | ohlcv = WebQuikFeed._ohlcv_of("stock1¦asset1", data)
46 | self.assertEqual(ohlcv.asset, Asset("stock1","asset1"))
47 | self.assertEqual(ohlcv.dt, datetime(2019, 10, 1, 10, 2, 0))
48 | self.assertEqual(ohlcv.o, 1)
49 | self.assertEqual(ohlcv.h, 3)
50 | self.assertEqual(ohlcv.l, 4)
51 | self.assertEqual(ohlcv.c, 2)
52 | self.assertEqual(ohlcv.v, 5)
53 |
54 | def test_ticker_of_full_asset(self):
55 | ticker = WebQuikFeed._ticker_of(Asset("class1", "sec1"))
56 | self.assertEqual("class1|sec1", ticker)
57 |
58 | def test_ticker_of_None(self):
59 | ticker = WebQuikFeed._ticker_of(Asset(None, None))
60 | self.assertEqual("None|None", ticker)
61 |
62 | def test_ticker_of_empty(self):
63 | ticker = WebQuikFeed._ticker_of(Asset("", ""))
64 | self.assertEqual("|", ticker)
65 |
66 | def test_asset_of_empty(self):
67 | asset = WebQuikFeed._asset_of("")
68 | self.assertIsNone(asset)
69 |
70 | def test_asset_of_none(self):
71 | asset = WebQuikFeed._asset_of(None)
72 | self.assertIsNone(asset)
73 |
74 | def test_asset_of_3partsname(self):
75 | asset: Asset = WebQuikFeed._asset_of("QJSIM¦SBER¦0")
76 | self.assertEqual("QJSIM", asset.class_code)
77 | self.assertEqual("SBER", asset.sec_code)
78 |
79 | def test_asset_of_2partsname(self):
80 | asset: Asset = WebQuikFeed._asset_of("c1ode1¦a1sset1")
81 | self.assertEqual("c1ode1", asset.class_code)
82 | self.assertEqual("a1sset1", asset.sec_code)
83 |
84 | def test_asset_of_onlyname(self):
85 | asset: Asset = WebQuikFeed._asset_of("a1sset1")
86 | self.assertIsNone(asset.class_code)
87 | self.assertEqual("a1sset1", asset.sec_code)
88 |
--------------------------------------------------------------------------------
/pytrade/strategy/PeriodicalLearnStrategy.py:
--------------------------------------------------------------------------------
1 | # import talib as ta
2 | from datetime import *
3 | import logging
4 | import pandas as pd
5 |
6 | from broker.Broker import Broker
7 | from connector.CsvFeedConnector import CsvFeedConnector
8 | from feed.Feed import Feed
9 | from model.feed.Asset import Asset
10 | from model.feed.Level2 import Level2
11 | from model.feed.Ohlcv import Ohlcv
12 | from model.feed.Quote import Quote
13 | from strategy.features.Level2Features import Level2Features
14 | from strategy.features.PriceFeatures import PriceFeatures
15 | from strategy.features.TargetFeatures import TargetFeatures
16 |
17 | pd.options.display.width = 0
18 |
19 |
20 | class PeriodicalLearnStrategy:
21 | """
22 | Strategy based on periodical additional learning
23 | """
24 |
25 | def __init__(self, feed: Feed, broker: Broker, config):
26 | self._logger = logging.getLogger(__name__)
27 | self._feed = feed
28 | self._broker = broker
29 | self.asset = Asset(config['trade.asset.sec_class'], config['trade.asset.sec_code'])
30 | # self._last_big_learn_time = datetime.min
31 | self._last_learn_time = None
32 | # todo:: parameterise
33 | self._interval_big_learn = timedelta(seconds=10)
34 | self._interval_small_learn = timedelta(hours=2)
35 | self._csv_connector = CsvFeedConnector(config)
36 | self._feed.subscribe_feed(self.asset, self)
37 | self._logger.info(f"Strategy initialized with initial learn interval {self._interval_big_learn},"
38 | f" additional learn interval ${self._interval_small_learn}")
39 |
40 | def learn(self):
41 | _, quotes, level2 = self._csv_connector.read_csvs()
42 | quotes.set_index(["ticker"], append=True,inplace=True)
43 | level2.set_index(["ticker"], append=True,inplace=True)
44 | self.learn_on(quotes, level2)
45 |
46 | def learn_on(self, quotes: pd.DataFrame, level2: pd.DataFrame):
47 | self._logger.info("Starting feature engineering")
48 | level2_features = Level2Features().level2_buckets(level2)
49 | target_features = TargetFeatures().min_max_future(quotes, 5, 'min')
50 | price_features = PriceFeatures().prices(quotes)
51 | features = pd.merge_asof(price_features, level2_features, left_on="datetime", right_on="datetime",
52 | tolerance=pd.Timedelta("1 min"))
53 | self._logger.info("Completed feature engineering")
54 | return features, target_features
55 |
56 | def periodical_learn(self):
57 | """
58 | Learn on last data we have
59 | """
60 | self._logger.info("Starting periodical learn")
61 | self.learn_on(self._feed.quotes, self._feed.level2)
62 |
63 | # Set last learning time to the last quote time
64 | self._last_learn_time = self._feed.quotes.index[-1][0]
65 | self._logger.info(f"Completed periodical learn, last time: {self._last_learn_time}")
66 |
67 | def run(self):
68 | self._logger.info("Running")
69 | # Subscribe to receive feed for the asset
70 |
71 | def on_candle(self, ohlcv: Ohlcv):
72 | """
73 | Receive a new candle event from feed. self.feed.candles dataframe contains all candles including this one.
74 | """
75 | # Skip if too early for a new processing cycle
76 | self._logger.debug(f"Got new candle ohlcv={ohlcv}")
77 |
78 | def on_heartbeat(self):
79 | self._logger.debug(f"Got heartbeat")
80 | return
81 |
82 | def on_quote(self, quote: Quote):
83 | """
84 | Got a new quote. self.feed.quotes contains all quotes including this one
85 | """
86 | self._logger.debug(f"Got new quote: {quote}")
87 | if not self._last_learn_time:
88 | self._last_learn_time = quote.dt
89 | if (quote.dt - self._last_learn_time) >= self._interval_big_learn:
90 | self.periodical_learn()
91 |
92 | def on_level2(self, level2: Level2):
93 | """
94 | Got new level2 data. self.feed.level2 contains all level2 records including this one
95 | """
96 | self._logger.debug(f"Got new level2: {level2}")
97 | return
98 |
--------------------------------------------------------------------------------
/pytrade/backtest/Backtest.py:
--------------------------------------------------------------------------------
1 | import backtrader as bt
2 | import pandas as pd
3 |
4 |
5 | class TestStrategy(bt.Strategy):
6 |
7 | def log(self, txt, dt=None):
8 | ''' Logging function fot this strategy'''
9 | dt = dt or self.datas[0].datetime.date(0)
10 | print('%s, %s' % (dt.isoformat(), txt))
11 |
12 | def __init__(self):
13 | # Keep a reference to the "close" line in the data[0] dataseries
14 | self.dataclose = self.datas[0].close
15 |
16 | # To keep track of pending orders and buy price/commission
17 | self.order = None
18 | self.buyprice = None
19 | self.buycomm = None
20 |
21 | def notify_order(self, order):
22 | if order.status in [order.Submitted, order.Accepted]:
23 | # Buy/Sell order submitted/accepted to/by broker - Nothing to do
24 | return
25 |
26 | # Check if an order has been completed
27 | # Attention: broker could reject order if not enough cash
28 | if order.status in [order.Completed]:
29 | if order.isbuy():
30 | self.log(
31 | 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
32 | (order.executed.price,
33 | order.executed.value,
34 | order.executed.comm))
35 |
36 | self.buyprice = order.executed.price
37 | self.buycomm = order.executed.comm
38 | else: # Sell
39 | self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
40 | (order.executed.price,
41 | order.executed.value,
42 | order.executed.comm))
43 |
44 | self.bar_executed = len(self)
45 |
46 | elif order.status in [order.Canceled, order.Margin, order.Rejected]:
47 | self.log('Order Canceled/Margin/Rejected')
48 |
49 | self.order = None
50 |
51 | def notify_trade(self, trade):
52 | if not trade.isclosed:
53 | return
54 |
55 | self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
56 | (trade.pnl, trade.pnlcomm))
57 |
58 | def next(self):
59 | # Simply log the closing price of the series from the reference
60 | self.log('Close, %.2f' % self.dataclose[0])
61 |
62 | # Check if an order is pending ... if yes, we cannot send a 2nd one
63 | if self.order:
64 | return
65 |
66 | # Check if we are in the market
67 | if not self.position:
68 |
69 | # Not yet ... we MIGHT BUY if ...
70 | if self.dataclose[0] < self.dataclose[-1]:
71 | # current close less than previous close
72 |
73 | if self.dataclose[-1] < self.dataclose[-2]:
74 | # previous close less than the previous close
75 |
76 | # BUY, BUY, BUY!!! (with default parameters)
77 | self.log('BUY CREATE, %.2f' % self.dataclose[0])
78 |
79 | # Keep track of the created order to avoid a 2nd order
80 | self.order = self.buy()
81 |
82 | else:
83 |
84 | # Already in the market ... we might sell
85 | if len(self) >= (self.bar_executed + 5):
86 | # SELL, SELL, SELL!!! (with all possible default parameters)
87 | self.log('SELL CREATE, %.2f' % self.dataclose[0])
88 |
89 | # Keep track of the created order to avoid a 2nd order
90 | self.order = self.sell()
91 |
92 |
93 | # Read csv data into dataframe
94 | df = pd.read_csv('./data/RI.RTSI_180101_180313.csv', parse_dates={'time': ['', '