You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
546 lines
19 KiB
546 lines
19 KiB
from .ui_basic import CandlestickItem, VolumeItem
|
|
from ..common.constant import Interval
|
|
from ..data import database_manager
|
|
from ..common.utility import extract_full_symbol
|
|
from ..common.datastruct import Event
|
|
from ..data.data_board import BarGenerator
|
|
import sys
|
|
import numpy as np
|
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
|
from datetime import timedelta, datetime
|
|
import pyqtgraph as pg
|
|
|
|
sys.path.insert(0, "../..")
|
|
|
|
|
|
class MarketDataView(QtWidgets.QWidget):
|
|
tick_signal = QtCore.pyqtSignal(Event)
|
|
symbol_signal = QtCore.pyqtSignal(str)
|
|
|
|
def __init__(self, sym: str = ""):
|
|
""""""
|
|
super().__init__()
|
|
|
|
self.full_symbol = ""
|
|
self.init_ui()
|
|
self.register_event()
|
|
|
|
def init_ui(self):
|
|
self.datachart = QuotesChart(self.full_symbol)
|
|
self.orderbook = OrderBookWidget()
|
|
self.scroll = QtWidgets.QScrollArea()
|
|
self.scroll.setWidget(self.datachart)
|
|
self.scroll.setWidgetResizable(True)
|
|
|
|
hbox = QtWidgets.QHBoxLayout()
|
|
hbox.addWidget(self.scroll)
|
|
hbox.addWidget(self.orderbook)
|
|
self.setLayout(hbox)
|
|
|
|
def register_event(self):
|
|
""""""
|
|
self.tick_signal.connect(self.orderbook.tick_signal.emit)
|
|
self.tick_signal.connect(self.datachart.on_tick)
|
|
self.symbol_signal.connect(self.orderbook.symbol_signal.emit)
|
|
self.orderbook.symbol_signal.connect(self.datachart.reset)
|
|
self.orderbook.day_signal.connect(self.datachart.reload)
|
|
# self.symbol_signal.connect(self.datachart.reset)
|
|
|
|
|
|
class DateAxis(pg.AxisItem):
|
|
"""Axis for showing date data"""
|
|
|
|
def __init__(self, dates: dict, *args, **kwargs):
|
|
""""""
|
|
super().__init__(*args, **kwargs)
|
|
self.dates = dates
|
|
|
|
def tickStrings(self, values, scale, spacing):
|
|
""""""
|
|
strings = []
|
|
for v in values:
|
|
dt = self.dates.get(v, "")
|
|
strings.append(str(dt))
|
|
return strings
|
|
|
|
|
|
class DateAxis2(pg.AxisItem):
|
|
"""Axis for showing date data"""
|
|
|
|
def __init__(self, datalist: list, *args, **kwargs):
|
|
""""""
|
|
super().__init__(*args, **kwargs)
|
|
self.data = datalist
|
|
|
|
def tickStrings(self, values, scale, spacing):
|
|
""""""
|
|
strings = []
|
|
# 60*(self.data[0].datetime.hour - 9) + self.data[0].datetime.minute
|
|
xstart = 0
|
|
for value in values:
|
|
v = value - xstart
|
|
if v > len(self.data) - 1 or v < 0:
|
|
return strings
|
|
dt = self.data[int(v)].datetime
|
|
strings.append(dt.strftime('%H:%M\n %b-%d '))
|
|
return strings
|
|
|
|
|
|
class PriceAxis(pg.AxisItem):
|
|
def __init__(self):
|
|
super().__init__(orientation='right')
|
|
self.style.update({'textFillLimits': [(0, 0.8)]})
|
|
|
|
def tickStrings(self, vals, scale, spacing):
|
|
digts = max(0, np.ceil(-np.log10(spacing * scale)))
|
|
return [
|
|
('{:<8,.%df}' % digts).format(v).replace(',', ' ') for v in vals
|
|
]
|
|
|
|
|
|
class VolumeAxis(pg.AxisItem):
|
|
def __init__(self):
|
|
super().__init__(orientation='right')
|
|
self.style.update({'textFillLimits': [(0, 0.8)]})
|
|
|
|
def tickStrings(self, vals, scale, spacing):
|
|
digts = max(0, np.ceil(-np.log10(spacing * scale)))
|
|
return [
|
|
('{:<8,.%df}' % digts).format(v).replace(',', ' ') for v in vals
|
|
]
|
|
|
|
|
|
class OpenInterestAxis(pg.AxisItem):
|
|
def __init__(self):
|
|
super().__init__(orientation='left')
|
|
self.style.update({'textFillLimits': [(0, 0.8)]})
|
|
|
|
def tickStrings(self, vals, scale, spacing):
|
|
digts = max(0, np.ceil(-np.log10(spacing * scale)))
|
|
return [
|
|
('{:<8,.%df}' % digts).format(v).replace(',', ' ') for v in vals
|
|
]
|
|
|
|
|
|
CHART_MARGINS = (0, 0, 20, 10)
|
|
|
|
|
|
class QuotesChart(QtGui.QWidget):
|
|
signal = QtCore.pyqtSignal(Event)
|
|
|
|
long_pen = pg.mkPen('#006000')
|
|
long_brush = pg.mkBrush('#00ff00')
|
|
short_pen = pg.mkPen('#600000')
|
|
short_brush = pg.mkBrush('#ff0000')
|
|
|
|
zoomIsDisabled = QtCore.pyqtSignal(bool)
|
|
|
|
def __init__(self, symbol: str = ""):
|
|
super().__init__()
|
|
self.full_symbol = symbol
|
|
self.data = []
|
|
self.bg = BarGenerator(self.on_bar)
|
|
self.layout = QtGui.QVBoxLayout(self)
|
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
self.chart = None
|
|
self.charv = None
|
|
self.load_bar()
|
|
self.plot()
|
|
|
|
def reset(self, symbol: str):
|
|
# if not self.layout.isEmpty():
|
|
# self.layout.removeWidget(self.splitter)
|
|
# self.splitter.deleteLater()
|
|
# self.full_symbol = symbol
|
|
# self.bg = BarGenerator(self.on_bar)
|
|
# self.load_bar()
|
|
# self.plot()
|
|
self.full_symbol = symbol
|
|
self.bg = BarGenerator(self.on_bar)
|
|
self.load_bar()
|
|
self.klineitem.generatePicture()
|
|
self.volumeitem.generatePicture()
|
|
self.oicurve.setData([bar.open_interest for bar in self.data])
|
|
|
|
def plot(self):
|
|
self.xaxis = DateAxis2(self.data, orientation='bottom')
|
|
self.xaxis.setStyle(
|
|
tickTextOffset=7, textFillLimits=[(0, 0.80)], showValues=True
|
|
)
|
|
self.klineitem = CandlestickItem(self.data)
|
|
self.volumeitem = VolumeItem(self.data)
|
|
self.oicurve = pg.PlotCurveItem(
|
|
[bar.open_interest for bar in self.data], pen='w')
|
|
self.init_chart()
|
|
self.init_chart_item()
|
|
|
|
def load_bar(self, days: int = 1, interval: Interval = Interval.MINUTE):
|
|
symbol, exchange = extract_full_symbol(self.full_symbol)
|
|
end = datetime.now()
|
|
start = end - timedelta(days)
|
|
if start > end:
|
|
tmp = end
|
|
end = start
|
|
start = tmp
|
|
bars = database_manager.load_bar_data(
|
|
symbol=symbol,
|
|
exchange=exchange,
|
|
interval=interval,
|
|
start=start,
|
|
end=end,
|
|
)
|
|
self.data.clear()
|
|
self.data.extend(bars)
|
|
|
|
def reload(self, count=1):
|
|
self.load_bar(days=count)
|
|
self.klineitem.generatePicture()
|
|
self.volumeitem.generatePicture()
|
|
self.oicurve.setData([bar.open_interest for bar in self.data])
|
|
|
|
def on_bar(self, bar):
|
|
self.data.append(bar)
|
|
self.klineitem.on_bar(bar)
|
|
self.volumeitem.on_bar(bar)
|
|
self.oicurve.setData([abar.open_interest for abar in self.data])
|
|
# self.xaxis.on_bar(bar)
|
|
# self.tmax += 1
|
|
# self.pmax = max(self.pmax,bar.high_price)
|
|
# self.pmin = min(self.pmin,bar.low_price)
|
|
# self.chart.setLimits(
|
|
# xMin=self.tmin,
|
|
# xMax=self.tmax,
|
|
# minXRange=60,
|
|
# yMin=self.pmin * 0.95,
|
|
# yMax=self.pmax * 1.05,
|
|
# )
|
|
|
|
def on_tick(self, tickevent):
|
|
tick = tickevent.data
|
|
if tick.full_symbol == self.full_symbol:
|
|
self.bg.update_tick(tick)
|
|
|
|
def init_chart_item(self):
|
|
|
|
self.chart.addItem(self.klineitem)
|
|
# barf = self.data[0]
|
|
# bare = self.data[-1]
|
|
# self.tmin = 60*(barf.datetime.hour - 9) + barf.datetime.minute - 20
|
|
# self.tmax = 60*(bare.datetime.hour - 9) + bare.datetime.minute + 20
|
|
# self.pmax = 0
|
|
# self.pmin = 999999
|
|
# for bar in self.data:
|
|
# self.pmax = max(self.pmax,bar.high_price)
|
|
# self.pmin = min(self.pmin,bar.low_price)
|
|
# self.chart.setLimits(
|
|
# xMin=self.tmin,
|
|
# xMax=self.tmax,
|
|
# minXRange=60,
|
|
# yMin=self.pmin * 0.95,
|
|
# yMax=self.pmax * 1.05,
|
|
# )
|
|
self.chartv.addItem(self.volumeitem)
|
|
|
|
# self.chart.setCursor(QtCore.Qt.BlankCursor)
|
|
# self.chart.sigXRangeChanged.connect(self._update_yrange_limits)
|
|
|
|
# def _update_yrange_limits(self):
|
|
# vr = self.chart.viewRect()
|
|
# lbar, rbar = max(0,int(vr.left())), min(len(self.data),int(vr.right()))
|
|
# bars = self.data[lbar:rbar]
|
|
# pmax = 0
|
|
# pmin = 999999
|
|
# pmean = 0
|
|
# for bar in bars:
|
|
# pmax = max(pmax,bar.high_price)
|
|
# pmin = min(pmin,bar.low_price)
|
|
# pmean += bar.close_price
|
|
# pmean = pmean/(len(bars))
|
|
# ylow = pmin * 0.95
|
|
# yhigh = pmax * 1.05
|
|
# print(pmax-pmean)
|
|
# self.chart.setLimits(yMin=ylow, yMax=yhigh, minYRange= 0.5*abs(pmax-pmean))
|
|
# self.chart.setYRange(ylow, yhigh)
|
|
|
|
def init_chart(self):
|
|
self.splitter = QtGui.QSplitter(QtCore.Qt.Vertical)
|
|
self.splitter.setHandleWidth(0)
|
|
self.layout.addWidget(self.splitter)
|
|
self.chart = pg.PlotWidget(
|
|
parent=self.splitter,
|
|
axisItems={'bottom': self.xaxis, 'right': PriceAxis()},
|
|
enableMenu=True,
|
|
)
|
|
self.chart.getPlotItem().setContentsMargins(0, 0, 20, 0)
|
|
self.chart.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
|
self.chart.hideAxis('left')
|
|
self.chart.showAxis('right')
|
|
self.chart.showGrid(x=True, y=True)
|
|
|
|
self.chartv = pg.PlotWidget(
|
|
parent=self.splitter,
|
|
axisItems={'bottom': self.xaxis,
|
|
'right': VolumeAxis(), 'left': OpenInterestAxis()},
|
|
enableMenu=True,
|
|
)
|
|
self.chartv.getPlotItem().setContentsMargins(0, 0, 15, 15)
|
|
self.chartv.setFrameStyle(
|
|
QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
|
# self.chartv.hideAxis('left')
|
|
self.chartv.showAxis('left')
|
|
self.chartv.showAxis('right')
|
|
self.chartv.setXLink(self.chart)
|
|
|
|
self.chartoi = pg.ViewBox()
|
|
p1 = self.chartv.getPlotItem()
|
|
p1.scene().addItem(self.chartoi)
|
|
p1.getAxis('left').linkToView(self.chartoi)
|
|
self.chartoi.setXLink(p1)
|
|
p1.vb.sigResized.connect(self.updateViews)
|
|
|
|
self.chartoi.addItem(self.oicurve)
|
|
|
|
def updateViews(self):
|
|
p1 = self.chartv.getPlotItem()
|
|
p2 = self.chartoi
|
|
p2.setGeometry(p1.vb.sceneBoundingRect())
|
|
p2.linkedViewChanged(p1.vb, p2.XAxis)
|
|
|
|
|
|
class OrderBookWidget(QtWidgets.QWidget):
|
|
tick_signal = QtCore.pyqtSignal(Event)
|
|
symbol_signal = QtCore.pyqtSignal(str)
|
|
day_signal = QtCore.pyqtSignal(int)
|
|
|
|
def __init__(self):
|
|
""""""
|
|
super().__init__()
|
|
|
|
self.full_symbol = ""
|
|
self.init_ui()
|
|
self.register_event()
|
|
self.clear_label_text()
|
|
|
|
def init_ui(self):
|
|
self.symbol_line = QtWidgets.QLineEdit()
|
|
# self.symbol_line.setReadOnly(True)
|
|
self.symbol_line.returnPressed.connect(self.process_symbol)
|
|
|
|
self.change_label = self.create_label(alignment=QtCore.Qt.AlignRight)
|
|
self.open_label = self.create_label(alignment=QtCore.Qt.AlignRight)
|
|
self.low_label = self.create_label(alignment=QtCore.Qt.AlignRight)
|
|
self.high_label = self.create_label(alignment=QtCore.Qt.AlignRight)
|
|
|
|
bid_color = "rgb(255,174,201)"
|
|
ask_color = "rgb(160,255,160)"
|
|
|
|
self.uplimit_label = self.create_label()
|
|
|
|
self.bp1_label = self.create_label(bid_color)
|
|
self.bp2_label = self.create_label(bid_color)
|
|
self.bp3_label = self.create_label(bid_color)
|
|
self.bp4_label = self.create_label(bid_color)
|
|
self.bp5_label = self.create_label(bid_color)
|
|
|
|
self.bv1_label = self.create_label(
|
|
bid_color, alignment=QtCore.Qt.AlignRight)
|
|
self.bv2_label = self.create_label(
|
|
bid_color, alignment=QtCore.Qt.AlignRight)
|
|
self.bv3_label = self.create_label(
|
|
bid_color, alignment=QtCore.Qt.AlignRight)
|
|
self.bv4_label = self.create_label(
|
|
bid_color, alignment=QtCore.Qt.AlignRight)
|
|
self.bv5_label = self.create_label(
|
|
bid_color, alignment=QtCore.Qt.AlignRight)
|
|
|
|
self.ap1_label = self.create_label(ask_color)
|
|
self.ap2_label = self.create_label(ask_color)
|
|
self.ap3_label = self.create_label(ask_color)
|
|
self.ap4_label = self.create_label(ask_color)
|
|
self.ap5_label = self.create_label(ask_color)
|
|
|
|
self.av1_label = self.create_label(
|
|
ask_color, alignment=QtCore.Qt.AlignRight)
|
|
self.av2_label = self.create_label(
|
|
ask_color, alignment=QtCore.Qt.AlignRight)
|
|
self.av3_label = self.create_label(
|
|
ask_color, alignment=QtCore.Qt.AlignRight)
|
|
self.av4_label = self.create_label(
|
|
ask_color, alignment=QtCore.Qt.AlignRight)
|
|
self.av5_label = self.create_label(
|
|
ask_color, alignment=QtCore.Qt.AlignRight)
|
|
|
|
self.lplimit_lable = self.create_label()
|
|
self.lp_label = self.create_label()
|
|
|
|
self.size_label = self.create_label(alignment=QtCore.Qt.AlignRight)
|
|
self.last_volume = 0
|
|
form2 = QtWidgets.QFormLayout()
|
|
|
|
historylabel = self.create_label(alignment=QtCore.Qt.AlignCenter)
|
|
historylabel.setText('History Data')
|
|
form2.addRow(historylabel)
|
|
self.histbar_day = QtWidgets.QLineEdit()
|
|
self.histbar_day.setValidator(QtGui.QIntValidator())
|
|
self.histbar_day.setText('1')
|
|
self.histbar_day.returnPressed.connect(self.process_days)
|
|
self.last_days = 1
|
|
form2.addRow('Days', self.histbar_day)
|
|
self.intervalcombo = QtWidgets.QComboBox()
|
|
self.intervalcombo.addItems(["1m", "1h"])
|
|
form2.addRow('Interval', self.intervalcombo)
|
|
self.indicators = QtWidgets.QComboBox()
|
|
self.indicators.addItems(['ma', 'sma'])
|
|
form2.addRow('Indicator', self.indicators)
|
|
form2.addItem(QtWidgets.QSpacerItem(
|
|
40, 40, QtWidgets.QSizePolicy.Expanding))
|
|
titlelabel = self.create_label(alignment=QtCore.Qt.AlignCenter)
|
|
titlelabel.setText('OrderBook')
|
|
|
|
# pricelable = self.create_label(alignment=QtCore.Qt.AlignLeft)
|
|
# pricelable.setText('Price')
|
|
# volumelable = self.create_label(alignment=QtCore.Qt.AlignRight)
|
|
# volumelable.setText('Volume')
|
|
|
|
# verticalSpacer = QtWidgets.QSpacerItem(10, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
|
form2.addRow(titlelabel)
|
|
form2.addRow(self.symbol_line)
|
|
form2.addRow(self.change_label, self.open_label)
|
|
form2.addRow(self.low_label, self.high_label)
|
|
form2.addRow(self.uplimit_label)
|
|
form2.addRow(self.ap5_label, self.av5_label)
|
|
form2.addRow(self.ap4_label, self.av4_label)
|
|
form2.addRow(self.ap3_label, self.av3_label)
|
|
form2.addRow(self.ap2_label, self.av2_label)
|
|
form2.addRow(self.ap1_label, self.av1_label)
|
|
form2.addRow(self.lp_label, self.size_label)
|
|
form2.addRow(self.bp1_label, self.bv1_label)
|
|
form2.addRow(self.bp2_label, self.bv2_label)
|
|
form2.addRow(self.bp3_label, self.bv3_label)
|
|
form2.addRow(self.bp4_label, self.bv4_label)
|
|
form2.addRow(self.bp5_label, self.bv5_label)
|
|
form2.addRow(self.lplimit_lable)
|
|
|
|
# Overall layout
|
|
vbox = QtWidgets.QVBoxLayout()
|
|
vbox.addLayout(form2)
|
|
self.setLayout(vbox)
|
|
self.setFixedWidth(160)
|
|
# self.setFixedSize(160,500)
|
|
|
|
def create_label(self, color: str = "", alignment: int = QtCore.Qt.AlignLeft):
|
|
"""
|
|
Create label with certain font color.
|
|
"""
|
|
label = QtWidgets.QLabel()
|
|
if color:
|
|
label.setStyleSheet(f"color:{color}")
|
|
label.setAlignment(alignment)
|
|
return label
|
|
|
|
def register_event(self):
|
|
""""""
|
|
self.tick_signal.connect(self.process_tick_event)
|
|
self.symbol_signal.connect(self.set_full_symbol)
|
|
|
|
def process_tick_event(self, tickevent: Event):
|
|
""""""
|
|
tick = tickevent.data
|
|
if not tick:
|
|
return
|
|
if (tick.full_symbol != self.full_symbol):
|
|
return
|
|
self.lp_label.setText(str(tick.last_price))
|
|
self.open_label.setText(str(tick.open_price))
|
|
self.low_label.setText(str(tick.low_price))
|
|
self.high_label.setText(str(tick.high_price))
|
|
self.size_label.setText(str(tick.volume - self.last_volume))
|
|
self.last_volume = tick.volume
|
|
self.bp1_label.setText(str(tick.bid_price_1))
|
|
self.bv1_label.setText(str(tick.bid_volume_1))
|
|
self.ap1_label.setText(str(tick.ask_price_1))
|
|
self.av1_label.setText(str(tick.ask_volume_1))
|
|
self.uplimit_label.setText(str(tick.limit_up))
|
|
self.lplimit_lable.setText(str(tick.limit_down))
|
|
|
|
if tick.pre_close != 0.0:
|
|
r = (tick.last_price / tick.pre_close - 1) * 100
|
|
self.change_label.setText(f"{r:.2f}%")
|
|
|
|
if tick.depth == 5:
|
|
self.bp2_label.setText(str(tick.bid_price_2))
|
|
self.bv2_label.setText(str(tick.bid_volume_2))
|
|
self.ap2_label.setText(str(tick.ask_price_2))
|
|
self.av2_label.setText(str(tick.ask_volume_2))
|
|
|
|
self.bp3_label.setText(str(tick.bid_price_3))
|
|
self.bv3_label.setText(str(tick.bid_volume_3))
|
|
self.ap3_label.setText(str(tick.ask_price_3))
|
|
self.av3_label.setText(str(tick.ask_volume_3))
|
|
|
|
self.bp4_label.setText(str(tick.bid_price_4))
|
|
self.bv4_label.setText(str(tick.bid_volume_4))
|
|
self.ap4_label.setText(str(tick.ask_price_4))
|
|
self.av4_label.setText(str(tick.ask_volume_4))
|
|
|
|
self.bp5_label.setText(str(tick.bid_price_5))
|
|
self.bv5_label.setText(str(tick.bid_volume_5))
|
|
self.ap5_label.setText(str(tick.ask_price_5))
|
|
self.av5_label.setText(str(tick.ask_volume_5))
|
|
|
|
def process_symbol(self):
|
|
sym = self.symbol_line.text()
|
|
if sym:
|
|
self.symbol_signal.emit(sym)
|
|
|
|
def process_days(self):
|
|
days = int(self.histbar_day.text())
|
|
if days != self.last_days:
|
|
self.last_days = days
|
|
self.day_signal.emit(days)
|
|
|
|
def set_full_symbol(self, symbol: str):
|
|
"""
|
|
Set the tick depth data to monitor by full_symbol.
|
|
"""
|
|
|
|
# Update name line widget and clear all labels
|
|
self.full_symbol = symbol
|
|
self.symbol_line.setText(symbol)
|
|
self.clear_label_text()
|
|
|
|
def clear_label_text(self):
|
|
"""
|
|
Clear text on all labels.
|
|
"""
|
|
self.lp_label.setText("Last")
|
|
self.change_label.setText("Change")
|
|
self.lplimit_lable.setText('LowerLimit')
|
|
self.uplimit_label.setText('UpperLimit')
|
|
self.open_label.setText('Open')
|
|
self.low_label.setText('Low')
|
|
self.high_label.setText('High')
|
|
self.size_label.setText('Volume')
|
|
self.bv1_label.setText("")
|
|
self.bv2_label.setText("")
|
|
self.bv3_label.setText("")
|
|
self.bv4_label.setText("")
|
|
self.bv5_label.setText("")
|
|
|
|
self.av1_label.setText("")
|
|
self.av2_label.setText("")
|
|
self.av3_label.setText("")
|
|
self.av4_label.setText("")
|
|
self.av5_label.setText("")
|
|
|
|
self.bp1_label.setText("")
|
|
self.bp2_label.setText("")
|
|
self.bp3_label.setText("")
|
|
self.bp4_label.setText("")
|
|
self.bp5_label.setText("")
|
|
|
|
self.ap1_label.setText("")
|
|
self.ap2_label.setText("")
|
|
self.ap3_label.setText("")
|
|
self.ap4_label.setText("")
|
|
self.ap5_label.setText("")
|
|
|