├── 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 |
Reply messages
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 |
Stock limits
45 | 46 | 47 | 48 | 49 | {Array.from(this.state.stockLimits.values()).map(v=> 50 | 51 | )} 52 |
assetcountprice
{v.scode}{v.cbal}{v.avg}
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 |