由于vnpy系统升级之最新的3.0版本,python底层的对象继承机制发生变化,导致原来的一部分绘图部件因为多继承而发生初始化失败,无法使用,必须升级。
近期不少vnpy的会员朋友不断地私信我,反映这些绘图部件用不了了,因为本人最近忙于交易策略的开发,无暇顾及,实在是抽不出时间,请大家谅解!
现在问题已经解决,可以放心使用。
修改vnpy\chart\manager.py中的BarManager,为它添加一个函数:
def get_bar_idx(self,trade_dt:datetime) -> int: # hxxjava add
"""
get the index of a bar which the trade time belongs to.
return:
-1 : belongs to none
0,1,... : bar's index
"""
a1 = np.array(sorted(self._datetime_index_map.keys()))
a2 = a1 <= trade_dt
return np.sum(a2 == True) - 1
当然别忘了在该文件的引用部分添加下面的语句
import numpy as np # hxxjava add
from datetime import datetime
from typing import List, Tuple, Dict
from vnpy.trader.ui import create_qapp, QtCore, QtGui, QtWidgets
from pyqtgraph import ScatterPlotItem
import pyqtgraph as pg
import numpy as np
import talib
import copy
from vnpy.chart import ChartWidget, VolumeItem, CandleItem
from vnpy.chart.item import ChartItem
from vnpy.chart.manager import BarManager
from vnpy.trader.object import (
BarData,
OrderData,
TradeData
)
from vnpy.trader.object import Direction, Exchange, Interval, Offset, Status, Product, OptionType, OrderType
class BarItem(CandleItem):
""" 美国线 """
BAR_WIDTH = 0.3
def __init__(self, manager: BarManager):
""""""
super().__init__(manager)
self.bar_pen: QtGui.QPen = pg.mkPen(color="w", width=2)
self.bar_brush: QtGui.QBrush = pg.mkBrush(color="w")
def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
""""""
# Create objects
candle_picture = QtGui.QPicture()
painter = QtGui.QPainter(candle_picture)
# Set painter color
painter.setPen(self.bar_pen)
painter.setBrush(self.bar_brush)
open,high,low,close = bar.open_price,bar.high_price,bar.low_price,bar.close_price
painter.drawLine(QtCore.QPointF(ix - self.BAR_WIDTH, open),QtCore.QPointF(ix, open))
painter.drawLine(QtCore.QPointF(ix, high),QtCore.QPointF(ix, low))
painter.drawLine(QtCore.QPointF(ix + self.BAR_WIDTH, close),QtCore.QPointF(ix, close))
# Finish
painter.end()
return candle_picture
class LineItem(CandleItem):
""""""
def __init__(self, manager: BarManager):
""""""
super().__init__(manager)
self.white_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 255), width=1)
def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
""""""
last_bar = self._manager.get_bar(ix - 1)
# Create objects
picture = QtGui.QPicture()
painter = QtGui.QPainter(picture)
# Set painter color
painter.setPen(self.white_pen)
# Draw Line
end_point = QtCore.QPointF(ix, bar.close_price)
if last_bar:
start_point = QtCore.QPointF(ix - 1, last_bar.close_price)
else:
start_point = end_point
painter.drawLine(start_point, end_point)
# Finish
painter.end()
return picture
def get_info_text(self, ix: int) -> str:
""""""
text = ""
bar = self._manager.get_bar(ix)
if bar:
text = f"Close:{bar.close_price}"
return text
class SmaItem(CandleItem):
""""""
def __init__(self, manager: BarManager):
""""""
super().__init__(manager)
self.line_pen: QtGui.QPen = pg.mkPen(color=(100, 100, 255), width=2)
self.sma_window = 10
self.sma_data: Dict[int, float] = {}
def set_pen(self,pen:QtGui.QPen):
""" 设置绘图的笔 """
self.line_pen = pen
def set_sma_window(self,sma_window:int):
""" 设置Sma的窗口 """
self.sma_window = sma_window
def get_sma_value(self, ix: int) -> float:
""""""
if ix < 0:
return 0
# When initialize, calculate all rsi value
if not self.sma_data:
bars = self._manager.get_all_bars()
close_data = [bar.close_price for bar in bars]
sma_array = talib.SMA(np.array(close_data), self.sma_window)
for n, value in enumerate(sma_array):
self.sma_data[n] = value
# Return if already calcualted
if ix in self.sma_data:
return self.sma_data[ix]
# Else calculate new value
close_data = []
for n in range(ix - self.sma_window, ix + 1):
bar = self._manager.get_bar(n)
close_data.append(bar.close_price)
sma_array = talib.SMA(np.array(close_data), self.sma_window)
sma_value = sma_array[-1]
self.sma_data[ix] = sma_value
return sma_value
def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
""""""
sma_value = self.get_sma_value(ix)
last_sma_value = self.get_sma_value(ix - 1)
# Create objects
picture = QtGui.QPicture()
painter = QtGui.QPainter(picture)
# Set painter color
painter.setPen(self.line_pen)
# Draw Line
start_point = QtCore.QPointF(ix-1, last_sma_value)
end_point = QtCore.QPointF(ix, sma_value)
painter.drawLine(start_point, end_point)
# Finish
painter.end()
return picture
def get_info_text(self, ix: int) -> str:
""""""
if ix in self.sma_data:
sma_value = self.sma_data[ix]
text = f"SMA{self.sma_window} {sma_value:.1f}"
else:
text = "SMA{self.sma_window} -"
return text
class RsiItem(ChartItem):
""""""
def __init__(self, manager: BarManager):
""""""
super().__init__(manager)
self.white_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 255), width=1)
self.yellow_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 0), width=2)
self.rsi_window = 14
self.rsi_data: Dict[int, float] = {}
def get_rsi_value(self, ix: int) -> float:
""""""
if ix < 0:
return 50
# When initialize, calculate all rsi value
if not self.rsi_data:
bars = self._manager.get_all_bars()
close_data = [bar.close_price for bar in bars]
rsi_array = talib.RSI(np.array(close_data), self.rsi_window)
for n, value in enumerate(rsi_array):
self.rsi_data[n] = value
# Return if already calcualted
if ix in self.rsi_data:
return self.rsi_data[ix]
# Else calculate new value
close_data = []
for n in range(ix - self.rsi_window, ix + 1):
bar = self._manager.get_bar(n)
close_data.append(bar.close_price)
rsi_array = talib.RSI(np.array(close_data), self.rsi_window)
rsi_value = rsi_array[-1]
self.rsi_data[ix] = rsi_value
return rsi_value
def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
""""""
rsi_value = self.get_rsi_value(ix)
last_rsi_value = self.get_rsi_value(ix - 1)
# Create objects
picture = QtGui.QPicture()
painter = QtGui.QPainter(picture)
# Draw RSI line
painter.setPen(self.yellow_pen)
if np.isnan(last_rsi_value) or np.isnan(rsi_value):
# print(ix - 1, last_rsi_value,ix, rsi_value,)
pass
else:
end_point = QtCore.QPointF(ix, rsi_value)
start_point = QtCore.QPointF(ix - 1, last_rsi_value)
painter.drawLine(start_point, end_point)
# Draw oversold/overbought line
painter.setPen(self.white_pen)
painter.drawLine(
QtCore.QPointF(ix, 70),
QtCore.QPointF(ix - 1, 70),
)
painter.drawLine(
QtCore.QPointF(ix, 30),
QtCore.QPointF(ix - 1, 30),
)
# Finish
painter.end()
return picture
def boundingRect(self) -> QtCore.QRectF:
""""""
# min_price, max_price = self._manager.get_price_range()
rect = QtCore.QRectF(
0,
0,
len(self._bar_picutures),
100
)
return rect
def get_y_range( self, min_ix: int = None, max_ix: int = None) -> Tuple[float, float]:
""" """
return 0, 100
def get_info_text(self, ix: int) -> str:
""""""
if ix in self.rsi_data:
rsi_value = self.rsi_data[ix]
text = f"RSI {rsi_value:.1f}"
# print(text)
else:
text = "RSI -"
return text
def to_int(value: float) -> int:
""""""
return int(round(value, 0))
def adjust_range(in_range:Tuple[float, float])->Tuple[float, float]:
""" 将y方向的显示范围扩大到1.1 """
ret_range:Tuple[float, float]
diff = abs(in_range[0] - in_range[1])
ret_range = (in_range[0]-diff*0.05,in_range[1]+diff*0.05)
return ret_range
class MacdItem(ChartItem):
""""""
_values_ranges: Dict[Tuple[int, int], Tuple[float, float]] = {}
last_range:Tuple[int, int] = (-1,-1) # 最新显示K线索引范围
def __init__(self, manager: BarManager):
""""""
super().__init__(manager)
self.white_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 255), width=1)
self.yellow_pen: QtGui.QPen = pg.mkPen(color=(255, 255, 0), width=1)
self.red_pen: QtGui.QPen = pg.mkPen(color=(255, 0, 0), width=1)
self.green_pen: QtGui.QPen = pg.mkPen(color=(0, 255, 0), width=1)
self.short_window = 12
self.long_window = 26
self.M = 9
self.macd_data: Dict[int, Tuple[float,float,float]] = {}
def get_macd_value(self, ix: int) -> Tuple[float,float,float]:
""""""
if ix < 0:
return (0.0,0.0,0.0)
# When initialize, calculate all macd value
if not self.macd_data:
bars = self._manager.get_all_bars()
close_data = [bar.close_price for bar in bars]
diffs,deas,macds = talib.MACD(np.array(close_data),
fastperiod=self.short_window,
slowperiod=self.long_window,
signalperiod=self.M)
for n in range(0,len(diffs)):
self.macd_data[n] = (diffs[n],deas[n],macds[n])
# Return if already calcualted
if ix in self.macd_data:
return self.macd_data[ix]
# Else calculate new value
close_data = []
for n in range(ix-self.long_window-self.M+1, ix + 1):
bar = self._manager.get_bar(n)
close_data.append(bar.close_price)
diffs,deas,macds = talib.MACD(np.array(close_data),
fastperiod=self.short_window,
slowperiod=self.long_window,
signalperiod=self.M)
diff,dea,macd = diffs[-1],deas[-1],macds[-1]
self.macd_data[ix] = (diff,dea,macd)
return (diff,dea,macd)
def _draw_bar_picture(self, ix: int, bar: BarData) -> QtGui.QPicture:
""""""
macd_value = self.get_macd_value(ix)
last_macd_value = self.get_macd_value(ix - 1)
# # Create objects
picture = QtGui.QPicture()
painter = QtGui.QPainter(picture)
# # Draw macd lines
if np.isnan(macd_value[0]) or np.isnan(last_macd_value[0]):
# print("略过macd lines0")
pass
else:
end_point0 = QtCore.QPointF(ix, macd_value[0])
start_point0 = QtCore.QPointF(ix - 1, last_macd_value[0])
painter.setPen(self.white_pen)
painter.drawLine(start_point0, end_point0)
if np.isnan(macd_value[1]) or np.isnan(last_macd_value[1]):
# print("略过macd lines1")
pass
else:
end_point1 = QtCore.QPointF(ix, macd_value[1])
start_point1 = QtCore.QPointF(ix - 1, last_macd_value[1])
painter.setPen(self.yellow_pen)
painter.drawLine(start_point1, end_point1)
if not np.isnan(macd_value[2]):
if (macd_value[2]>0):
painter.setPen(self.red_pen)
painter.setBrush(pg.mkBrush(255,0,0))
else:
painter.setPen(self.green_pen)
painter.setBrush(pg.mkBrush(0,255,0))
painter.drawRect(QtCore.QRectF(ix-0.3,0,0.6,macd_value[2]))
else:
# print("略过macd lines2")
pass
painter.end()
return picture
def boundingRect(self) -> QtCore.QRectF:
""""""
min_y, max_y = self.get_y_range()
rect = QtCore.QRectF(
0,
min_y,
len(self._bar_picutures),
max_y
)
return rect
def get_y_range(self, min_ix: int = None, max_ix: int = None) -> Tuple[float, float]:
# 获得3个指标在y轴方向的范围
# hxxjava 修改,2020-6-29
# 当显示范围改变时,min_ix,max_ix的值不为None,当显示范围不变时,min_ix,max_ix的值不为None,
offset = max(self.short_window,self.long_window) + self.M - 1
if not self.macd_data or len(self.macd_data) < offset:
# print(f'(min_ix,max_ix){(min_ix,max_ix)} offset={offset},len(self.macd_data)={len(self.macd_data)}')
# hxxjava 修改,2021-5-8,因为升级vnpy,其依赖的pyqtgraph版本也升级了,原来为return 0,1
return -100, 100
# print("len of range dict:",len(self._values_ranges),",macd_data:",len(self.macd_data),(min_ix,max_ix))
if min_ix != None: # 调整最小K线索引
min_ix = max(min_ix,offset)
if max_ix != None: # 调整最大K线索引
max_ix = min(max_ix, len(self.macd_data)-1)
last_range = (min_ix,max_ix) # 请求的最新范围
if last_range == (None,None): # 当显示范围不变时
if self.last_range in self._values_ranges:
# 如果y方向范围已经保存
# 读取y方向范围
result = self._values_ranges[self.last_range]
# print("1:",self.last_range,result)
return adjust_range(result)
else:
# 如果y方向范围没有保存
# 从macd_data重新计算y方向范围
min_ix,max_ix = 0,len(self.macd_data)-1
macd_list = list(self.macd_data.values())[min_ix:max_ix + 1]
ndarray = np.array(macd_list)
max_price = np.nanmax(ndarray)
min_price = np.nanmin(ndarray)
# 保存y方向范围,同时返回结果
result = (min_price, max_price)
self.last_range = (min_ix,max_ix)
self._values_ranges[self.last_range] = result
# print("2:",self.last_range,result)
return adjust_range(result)
""" 以下为显示范围变化时 """
if last_range in self._values_ranges:
# 该范围已经保存过y方向范围
# 取得y方向范围,返回结果
result = self._values_ranges[last_range]
# print("3:",last_range,result)
return adjust_range(result)
# 该范围没有保存过y方向范围
# 从macd_data重新计算y方向范围
macd_list = list(self.macd_data.values())[min_ix:max_ix + 1]
ndarray = np.array(macd_list)
max_price = np.nanmax(ndarray)
min_price = np.nanmin(ndarray)
# 取得y方向范围,返回结果
result = (min_price, max_price)
self.last_range = last_range
self._values_ranges[self.last_range] = result
# print("4:",self.last_range,result)
return adjust_range(result)
def get_info_text(self, ix: int) -> str:
""" """
barscount = len(self._manager._bars) # hxxjava debug
if ix in self.macd_data:
diff,dea,macd = self.macd_data[ix]
words = [
f"diff {diff:.3f}"," ",
f"dea {dea:.3f}"," ",
f"macd {macd:.3f}",
f"barscount={ix,barscount}"
]
text = "\n".join(words)
else:
text = "diff - \ndea - \nmacd -"
return text
def tip_func(x,y,data):
""" """
return f"{data}"
class BaseScatter(pg.ScatterPlotItem):
""" """
def __init__(self, plot:pg.PlotItem,manager:BarManager,*args, **kargs):
""" """
super().__init__(args=args,kargs=kargs)
self.plot = plot
self.manager = manager
self.plot.addItem(self)
self.opts['hoverable'] = True
def hoverEvent(self, ev):
""" """
if self.opts['hoverable']:
old = self.data['hovered']
if ev.exit:
new = np.zeros_like(self.data['hovered'])
else:
new = self._maskAt(ev.pos())
if self._hasHoverStyle():
self.data['sourceRect'][old ^ new] = 0
self.data['hovered'] = new
self.updateSpots()
points = self.points()[new][::-1]
# Show information about hovered points in a tool tip
vb = self.getViewBox()
if vb is not None and self.opts['tip'] is not None:
cutoff = 10
# tip = [self.opts['tip'](x=pt.pos().x(), y=pt.pos().y(), data=pt.data())
tip = [tip_func(x=pt.pos().x(), y=pt.pos().y(), data=pt.data()) for pt in points[:cutoff]]
if len(points) > cutoff:
tip.append('({} others...)'.format(len(points) - cutoff))
vb.setToolTip('\n\n'.join(tip))
self.sigHovered.emit(self, points, ev)
class TradeItem(BaseScatter):
""" 成交单绘图部件 """
TRADE_COLOR_MAP = {
(Direction.LONG,Offset.OPEN):'red',
(Direction.LONG,Offset.CLOSE):'magenta',
(Direction.LONG,Offset.CLOSETODAY):'magenta',
(Direction.LONG,Offset.CLOSEYESTERDAY):'magenta',
(Direction.SHORT,Offset.OPEN):'green',
(Direction.SHORT,Offset.CLOSE):'yellow',
(Direction.SHORT,Offset.CLOSETODAY):'yellow',
(Direction.SHORT,Offset.CLOSEYESTERDAY):'yellow',
}
TRADE_COMMAND_MAP = {
(Direction.LONG,Offset.OPEN):'买开',
(Direction.LONG,Offset.CLOSE):'买平',
(Direction.LONG,Offset.CLOSETODAY):'买平今',
(Direction.LONG,Offset.CLOSEYESTERDAY):'买平昨',
(Direction.SHORT,Offset.OPEN):'卖开',
(Direction.SHORT,Offset.CLOSE):'卖平',
(Direction.SHORT,Offset.CLOSETODAY):'卖平今',
(Direction.SHORT,Offset.CLOSEYESTERDAY):'卖平昨',
}
def __init__(self, plot:pg.PlotItem,manager:BarManager):
""" """
super().__init__(plot=plot,manager=manager,size=15, pxMode=True,pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120))
self.trades : List = []
def _to_scatter_data(self,trade:TradeData):
""" """
idx = self.manager.get_bar_idx(trade.datetime)
if idx == -1:
return {}
bar:BarData = self.manager.get_bar(idx)
color = self.TRADE_COLOR_MAP[(trade.direction,trade.offset)]
size = 10
LL,HH = self.manager.get_price_range()
y_adjustment = (HH-LL) * 0.01
if trade.direction == Direction.LONG:
symbol = 't1'
y = bar.low_price - y_adjustment
else:
symbol = 't'
y = bar.high_price + y_adjustment
# pen = pg.mkPen(QtGui.QColor(color))
# brush = pg.mkBrush(QtGui.QColor(color))
scatter_data = {
"pos": (idx, y),
"size": size,
"pen": color,
"brush": color,
"symbol": symbol,
"data": "成交单:{},单号:{},指令:{},价格:{},手数:{},时间:{}".format(
trade.vt_symbol,
trade.vt_tradeid,
self.TRADE_COMMAND_MAP[(trade.direction,trade.offset)],
trade.price,trade.volume,
trade.datetime.strftime('%Y-%m-%d %H:%M:%S')
)
}
return scatter_data
def add_trades(self, trades: List[TradeData]):
""""""
# 将trade转换为scatter数据
# self.updated = False
self.trades.extend(trades)
spots = []
for trade in self.trades:
scatter = self._to_scatter_data(trade)
if not scatter:
continue
spots.append(scatter)
# self.clear()
# self.plot.removeItem(self)
self.setData(spots,hoverable=True)
def add_trade(self,trade:TradeData):
""" """
self.trades.append(trade)
spots = []
for trade in self.trades:
scatter = self._to_scatter_data(trade)
if not scatter:
continue
spots.append(scatter)
# self.clear()
# self.plot.removeItem(self)
self.setData(spots,hoverable=True)
class OrderItem(BaseScatter):
""" 成交单绘图部件 """
ORDER_COLOR_MAP = {
(Direction.LONG,Offset.OPEN):'red',
(Direction.LONG,Offset.CLOSE):'magenta',
(Direction.LONG,Offset.CLOSETODAY):'magenta',
(Direction.LONG,Offset.CLOSEYESTERDAY):'magenta',
(Direction.SHORT,Offset.OPEN):'green',
(Direction.SHORT,Offset.CLOSE):'yellow',
(Direction.SHORT,Offset.CLOSETODAY):'yellow',
(Direction.SHORT,Offset.CLOSEYESTERDAY):'yellow',
}
ORDER_COMMAND_MAP = {
(Direction.LONG,Offset.OPEN):'买开',
(Direction.LONG,Offset.CLOSE):'买平',
(Direction.LONG,Offset.CLOSETODAY):'买平今',
(Direction.LONG,Offset.CLOSEYESTERDAY):'买平昨',
(Direction.SHORT,Offset.OPEN):'卖开',
(Direction.SHORT,Offset.CLOSE):'卖平',
(Direction.SHORT,Offset.CLOSETODAY):'卖平今',
(Direction.SHORT,Offset.CLOSEYESTERDAY):'卖平昨',
}
def __init__(self, plot:pg.PlotItem,manager:BarManager):
""" """
super().__init__(plot=plot,manager=manager,size=15, pxMode=True,pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120))
self.orders : List[OrderData] = []
def _to_scatter_data(self,order:OrderData):
""" """
if not order.datetime:
return {}
idx = self.manager.get_bar_idx(order.datetime)
if idx == -1:
return {}
bar:BarData = self.manager.get_bar(idx)
color = self.ORDER_COLOR_MAP[(order.direction,order.offset)]
size = 10
LL,HH = self.manager.get_price_range()
y_adjustment = (HH-LL) * 0.02
if order.direction == Direction.LONG:
symbol = 'o'
y = bar.low_price - y_adjustment
else:
symbol = 'o'
y = bar.high_price + y_adjustment
# pen = pg.mkPen(QtGui.QColor(color))
# brush = pg.mkBrush(QtGui.QColor(color))
scatter_data = {
"pos": (idx, y),
"size": size,
"pen": color,
"brush": color,
"symbol": symbol,
"data": "委托单:{},单号:{},指令:{},价格:{},手数:{},时间:{}".format(
order.vt_symbol,
order.vt_orderid,
self.ORDER_COMMAND_MAP[(order.direction,order.offset)],
order.price,order.volume,
order.datetime.strftime('%Y-%m-%d %H:%M:%S')
)
}
return scatter_data
def add_orders(self, orders: List[OrderData]):
""""""
# 将trade转换为scatter数据
# self.updated = False
filter_orders = [order for order in orders if order.datetime is not None and order.traded > 0]
if not filter_orders:
return
self.orders.extend(filter_orders)
spots = []
for order in self.orders:
scatter = self._to_scatter_data(order)
if not scatter:
continue
spots.append(scatter)
print(f"spots={spots}")
# self.clear()
# self.plot.removeItem(self)
self.setData(spots,hoverable=True)
def add_order(self,order:OrderData):
""" """
if order.datetime is None or order.traded == 0:
return
self.orders.append(order)
spots = []
for order in self.orders:
scatter = self._to_scatter_data(order)
if not scatter:
continue
spots.append(scatter)
print(f"spots={spots}")
# self.clear()
# self.plot.removeItem(self)
self.setData(spots,hoverable=True)
创建OrderItem和TradeItem时,必须传递主图或者附图的plot和bar管理器BarManager,示例代码如下:
candle_plot = self.chart.get_plot('candle')
manager = self.chart._manager
self.trade_item:TradeItem = TradeItem(plot=candle_plot,manager=manager)
当十字光标移动到成交单图标时,如果当根K线上发生过多次成交,你可能只看见一个图标,但其实是有多个图标被绘制的,这反应在图中的光标提示中,如图所示:
看效果图:
尽管此时还是没有开盘,甚至还没有开始集合竞价,可是您的策略已经从on_tick()接口被推送了一个tick,而且该tick的时间不是当天下午的收盘,也不是您订阅该合约的时间 ! 我把这个tick打印出来了,请看:
TickData(
gateway_name='CTP',
symbol='TA205',
exchange=<Exchange.CZCE: 'CZCE'>,
datetime=datetime.datetime(2022, 4, 12, 20, 1, 18, 500000, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>),
name='精对苯二甲酸205',
volume=0,
turnover=0.0,
open_interest=441112.0,
last_price=6128.0,
last_volume=0,
limit_up=6374.0,
limit_down=5538.0,
open_price=0,
high_price=0,
low_price=0,
pre_close=6128.0,
bid_price_1=0,
bid_price_2=0,
bid_price_3=0,
bid_price_4=0,
bid_price_5=0,
ask_price_1=0,
ask_price_2=0,
ask_price_3=0,
ask_price_4=0,
ask_price_5=0,
bid_volume_1=0,
bid_volume_2=0,
bid_volume_3=0,
bid_volume_4=0,
bid_volume_5=0,
ask_volume_1=0,
ask_volume_2=0,
ask_volume_3=0,
ask_volume_4=0,
ask_volume_5=0,
localtime=None
)
很快开始集合竞价,在20:59的时候,策略可能又会收到一个包含开盘价的tick。
1分钟后是21:00,正式进入连续竞价阶段,策略又会收到等多的tick。
为了后面叙述的方便,我们把20:50时收到tick叫tick1,20:59时收到tick叫tick2。
假如CTA策略使用了30分钟K线,那么随着集合竞价结束,在21:00的时候,策略中的BarGeneraor对象,就会为您生成两个莫名其妙的30分钟K线:
启动策略不到10分钟时间,就已经虚多了2个30分钟K线。
CTA策略的初始化是由CtaEngine驱动的,其执行逻辑在vnpy_ctastrategy\engine的CtaEngine._init_strategy()中:
def _init_strategy(self, strategy_name: str):
"""
Init strategies in queue.
"""
strategy = self.strategies[strategy_name]
if strategy.inited:
self.write_log(f"{strategy_name}已经完成初始化,禁止重复操作")
return
self.write_log(f"{strategy_name}开始执行初始化")
# Call on_init function of strategy
self.call_strategy_func(strategy, strategy.on_init)
# Restore strategy data(variables)
data = self.strategy_data.get(strategy_name, None)
if data:
for name in strategy.variables:
value = data.get(name, None)
if value is not None:
setattr(strategy, name, value)
# Subscribe market data
contract = self.main_engine.get_contract(strategy.vt_symbol)
if contract:
req = SubscribeRequest(
symbol=contract.symbol, exchange=contract.exchange)
self.main_engine.subscribe(req, contract.gateway_name)
else:
self.write_log(f"行情订阅失败,找不到合约{strategy.vt_symbol}", strategy)
# Put event to update init completed status.
strategy.inited = True
self.put_strategy_event(strategy)
self.write_log(f"{strategy_name}初始化完成")
_init_strategy()执行过程是先为策略加载历史数据,再订阅策略交易合约的行情。
要想解决问题,就必须问题的根源在哪里?
因为订阅合约行情执行的是CtpMdApi的subscribe():
def subscribe(self, req: SubscribeRequest) -> None:
"""订阅行情"""
if self.login_status:
self.subscribeMarketData(req.symbol)
self.subscribed.add(req.symbol)
self.subscribeMarketData(req.symbol)只要是首次订阅,接口都会立即从OnRtnDepthMarketData推送1个该合约最新的深度行情通知,而时间是:
///最后修改时间
TThostFtdcTimeType UpdateTime;
///最后修改毫秒
TThostFtdcMillisecType UpdateMillisec;
这里的最后修改时间和最后修改毫秒本应该是该合约最后交易的时间,也可能是交易所行情服务器中CTP行情接口重新打开的时间!这就是为什么我们开动tick1的时间是2022-4-12 20:1:18.500000的原因。
TA205.CZCE在每个交易日的集合竞价时段的第4分钟会产生一个集合竞价tick。
你可能会说这个没有毛病,从20:30~21:00,确实是可以生成一个30分钟K线,为什么它不可以只包含一个tick呢?
这么说也过得去,可是问题是咱们在加载其他历史数据的时候,无论我们使用米筐、tushare或者什么其他第三方历史数据时,加载的1分钟K线,从来都没有这样的数据。
或者我们把策略产生的30分钟K线与通达信、大智慧或者文华6等软件生成的30分钟K线比较一下,它们都是没有出现这第二个30分钟K线情况的。从这种种也可以看出来这个tick的处理是不对的,tick2必须归入到21:00~21:30。
struct CThostFtdcDepthMarketDataField
{
///交易日
TThostFtdcDateType TradingDay;
///合约代码
TThostFtdcInstrumentIDType InstrumentID;
///交易所代码
TThostFtdcExchangeIDType ExchangeID;
///合约在交易所的代码
TThostFtdcExchangeInstIDType ExchangeInstID;
///最新价
TThostFtdcPriceType LastPrice;
///上次结算价
TThostFtdcPriceType PreSettlementPrice;
///昨收盘
TThostFtdcPriceType PreClosePrice;
///昨持仓量
TThostFtdcLargeVolumeType PreOpenInterest;
///今开盘
TThostFtdcPriceType OpenPrice;
///最高价
TThostFtdcPriceType HighestPrice;
///最低价
TThostFtdcPriceType LowestPrice;
///数量
TThostFtdcVolumeType Volume;
///成交金额
TThostFtdcMoneyType Turnover;
///持仓量
TThostFtdcLargeVolumeType OpenInterest;
///今收盘
TThostFtdcPriceType ClosePrice;
///本次结算价
TThostFtdcPriceType SettlementPrice;
///涨停板价
TThostFtdcPriceType UpperLimitPrice;
///跌停板价
TThostFtdcPriceType LowerLimitPrice;
///昨虚实度
TThostFtdcRatioType PreDelta;
///今虚实度
TThostFtdcRatioType CurrDelta;
///最后修改时间
TThostFtdcTimeType UpdateTime;
///最后修改毫秒
TThostFtdcMillisecType UpdateMillisec;
///申买价一
TThostFtdcPriceType BidPrice1;
///申买量一
TThostFtdcVolumeType BidVolume1;
///申卖价一
TThostFtdcPriceType AskPrice1;
///申卖量一
TThostFtdcVolumeType AskVolume1;
///申买价二
TThostFtdcPriceType BidPrice2;
///申买量二
TThostFtdcVolumeType BidVolume2;
///申卖价二
TThostFtdcPriceType AskPrice2;
///申卖量二
TThostFtdcVolumeType AskVolume2;
///申买价三
TThostFtdcPriceType BidPrice3;
///申买量三
TThostFtdcVolumeType BidVolume3;
///申卖价三
TThostFtdcPriceType AskPrice3;
///申卖量三
TThostFtdcVolumeType AskVolume3;
///申买价四
TThostFtdcPriceType BidPrice4;
///申买量四
TThostFtdcVolumeType BidVolume4;
///申卖价四
TThostFtdcPriceType AskPrice4;
///申卖量四
TThostFtdcVolumeType AskVolume4;
///申买价五
TThostFtdcPriceType BidPrice5;
///申买量五
TThostFtdcVolumeType BidVolume5;
///申卖价五
TThostFtdcPriceType AskPrice5;
///申卖量五
TThostFtdcVolumeType AskVolume5;
///当日均价
TThostFtdcPriceType AveragePrice;
///业务日期
TThostFtdcDateType ActionDay;
};
其中 UpdateMillisec 为最后修改毫秒,int型
错误和不合适之处已经改正,见注释:
def onRtnDepthMarketData(self, data: dict) -> None:
"""行情数据推送"""
# 过滤没有时间戳的异常行情数据
if not data["UpdateTime"]:
return
# 过滤还没有收到合约数据前的行情推送
symbol: str = data["InstrumentID"]
contract: ContractData = symbol_contract_map.get(symbol, None)
if not contract:
return
# 对大商所的交易日字段取本地日期
if not data["ActionDay"] or contract.exchange == Exchange.DCE:
# 这里废了那么大的劲,却使用了一个更新滞后的变量,属实不应该
# self.current_date是由定时器几秒更新一次,
# 对于一些跨夜品种,会导致几秒钟的tick的日期错误
# date_str: str = self.current_date
date_str: str = datetime.now().strftime("%Y%m%d") # hxxjava change
else:
date_str: str = data["ActionDay"]
# 这里不好,为什么要故意降低接口的时间精度,放着毫秒不要而费劲地变化为0.1秒精度?
# timestamp: str = f"{date_str} {data['UpdateTime']}.{int(data['UpdateMillisec']/100)}"
timestamp: str = f"{date_str} {data['UpdateTime']}." + str(data['UpdateMillisec']*1000).zfill(6) # hxxjava edit
dt: datetime = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f")
dt: datetime = CHINA_TZ.localize(dt)
tick: TickData = TickData(
symbol=symbol,
exchange=contract.exchange,
datetime=dt,
name=contract.name,
volume=data["Volume"],
turnover=data["Turnover"],
open_interest=data["OpenInterest"],
last_price=adjust_price(data["LastPrice"]),
limit_up=data["UpperLimitPrice"],
limit_down=data["LowerLimitPrice"],
open_price=adjust_price(data["OpenPrice"]),
high_price=adjust_price(data["HighestPrice"]),
low_price=adjust_price(data["LowestPrice"]),
pre_close=adjust_price(data["PreClosePrice"]),
bid_price_1=adjust_price(data["BidPrice1"]),
ask_price_1=adjust_price(data["AskPrice1"]),
bid_volume_1=data["BidVolume1"],
ask_volume_1=data["AskVolume1"],
gateway_name=self.gateway_name
)
if data["BidVolume2"] or data["AskVolume2"]:
tick.bid_price_2 = adjust_price(data["BidPrice2"])
tick.bid_price_3 = adjust_price(data["BidPrice3"])
tick.bid_price_4 = adjust_price(data["BidPrice4"])
tick.bid_price_5 = adjust_price(data["BidPrice5"])
tick.ask_price_2 = adjust_price(data["AskPrice2"])
tick.ask_price_3 = adjust_price(data["AskPrice3"])
tick.ask_price_4 = adjust_price(data["AskPrice4"])
tick.ask_price_5 = adjust_price(data["AskPrice5"])
tick.bid_volume_2 = data["BidVolume2"]
tick.bid_volume_3 = data["BidVolume3"]
tick.bid_volume_4 = data["BidVolume4"]
tick.bid_volume_5 = data["BidVolume5"]
tick.ask_volume_2 = data["AskVolume2"]
tick.ask_volume_3 = data["AskVolume3"]
tick.ask_volume_4 = data["AskVolume4"]
tick.ask_volume_5 = data["AskVolume5"]
self.gateway.on_tick(tick)
先厘清大思路,后面逐步完成。
vnpy系统自带了一个BarGenerator,它可以帮助我们生成1分钟,n分钟,n小时,日周期的K线,也叫bar。可是除了1分钟比较完美之外,有很多问题。它在读取历史数据、回测的时候多K线的处理和实盘却有不一样的效果。具体的问题我已经在解决vnpy 2.9.0版本的BarGenerator产生30分钟Bar的错误!这个帖子中做过尝试,但也不是很成功。因为系统的BarGenerator靠时间窗口与1分钟bar的时间分钟关系来决定是否该新建和结束一个bar,这个有问题。于是我改用对1分钟bar进行计数来决定是否该新建和结束一个bar,这也是有不可靠的问题,遇到行情比较清淡的时候,可能有的分钟就没有1分钟bar产生,这是完全有可能的!
K线几乎是绝大部分交易策略分析的基础,除非你从事的是极高频交易,否则你就得用它。可是如果你连生成一个稳健可靠的K线都不能够保证,那么运行在K线基础上的指标及由此产生的交易信号就无从谈起,K线错了,它们就是错误的,以此为依据做出点交易指令有可能是南辕北辙,所以必须解决它!
K线不是交易所发布的,它有很多种产生机制。其对齐方式、表现形式多种多样。关于K线的分类本人在以往的帖子中做出过比较详细的说明,有兴趣的读者可以去我以往的帖子中查看,这里就不再赘述。
市面上的绝大部分软件如通达信、大智慧、文华财经等软件,除非用户特别设定,他们最常提供给用户的K线多是日内对齐等交易时长K线。常用是一定是有道理的,因为它们已经为广大用户和投资者所接受。
1)什么是日内对齐等交易时长K线?
它具有这些特点:以每日开盘为起点,每根K线都包含相同交易时间的数据,跳过中间的休市时间,直至当前交易日的收盘,收盘不足n分钟也就是K线。实盘中,每日的第一个n分钟K线含集合竞价的那个tick数据。
2)为什么这种K线能够被普遍接受?
为它尽可能地保证一个交易日内的所有K线所表达的意义内容上是一致的,它们包含相等的交易时长。这非常重要,因为你把一个5分钟时长的K线与一个30分钟时长的K线放在一起谈论是没有意义的。但是如果为了保证K线在交易时长上的一致性,让n分钟K线跨日的话也是不太合理,因为这跨日,跨周末时间太长,这中间会发生什么意外事情,可能会产生出非常巨大的幅度大K线,掩盖了隔日跳空的行情变化,这对解读行情是不利的。当然n日的K线日跨日的,但是它是n个交易日的K线融合而成的,不过其融合的每个日K线也是对齐各自的日开盘的。
另外日内对齐等交易时长K线还有一个好处,那就是你以任何之前的时间为起点,在读取历史数据重新生成该日的n分钟K线的时候,得到的改日的K线是一致的。举个例子,如果我们的CTA策略在init()中常常是这么一句:
self.load_bar(20) # 先加载20日历史1分钟数据
这么简单的一句,包含着很多你意识不到的变化——你今天运行策略和明天运行你的策略,其中的历史数据的范围发生了变化,也就是说加载数据的起点变了。如果我们合成的K线的对齐方式不采用日内对齐的话,而采用对齐加载时间起点的话,你今天、明天加载出来之前的某日的K线就可能完全是不同的。而采用日内对齐等交易时长的K线则不存在这个问题。
3)需要知道合约的交易时间段
既然要对齐每日开盘,还有跳过各个休市时间,还要知道收市时间,那么我们就知道生成这种K线必须知道其所表达合约或对象的交易时间段,交易时间段中包含了这些信息,不知道这些信息,BarGenerator就不知道如何生成这种bar。这是必须的!
目前vnpy系统中的是没有合约的交易时间段的。到哪里获取合约的交易时间段的呢?
1) 它与合约相关,应该到保存合约的数据类ContractData中去找,没有找到。
2) 是否可以提供接口,从交易所获得,这个也是比较基础的数据。于是到CTP接口中(我使用的是CTP接口,您也许不一样) ,在最新版本的CTP接口文档中也没有找到任何与交易时间段相关的信息,绝望!
解决方法:
打开vnpy.trader.datafeed.py文件为Datafeed的基类BaseDatafeed扩展下面的接口
class BaseDatafeed(ABC):
"""
Abstract datafeed class for connecting to different datafeed.
"""
def init(self) -> bool:
"""
Initialize datafeed service connection.
"""
pass
def update_all_trading_hours(self) -> bool: # hxxjava add
""" 更新所有合约的交易时间段到trading_hours.json文件中 """
pass
def load_all_trading_hours(self) -> dict: # hxxjava add
""" 从trading_hours.json文件中读取所有合约的交易时间段 """
pass
def query_bar_history(self, req: HistoryRequest) -> Optional[List[BarData]]:
"""
Query history bar data.
"""
pass
def query_tick_history(self, req: HistoryRequest) -> Optional[List[TickData]]:
"""
Query history tick data.
"""
pass
其中的trading_hours.json文件我会在后面的文章中做详细的介绍。有了它我们才能展开其他的设计。
在vnpy_rqdata\rqdata_datafeed.py中增加下面的代码
from datetime import timedelta,date # hxxjava add
def update_all_trading_hours(self) -> bool: # hxxjava add
""" 更新所有合约的交易时间段到trading_hours.json文件中 """
if not self.inited:
self.init()
if not self.inited:
return False
ths_dict = load_json(self.trading_hours_file)
# instruments = all_instruments(type=['Future','Stock','Index','Spot'])
trade_hours = {}
for stype in ['Future','Stock','Index','Fund','Spot']:
instruments = all_instruments(type=[stype])
# print(f"{stype} instruments count={len(instruments)}")
for idx,inst in instruments.iterrows():
# 获取每个最新发布的合约的建议时间段
if ('trading_hours' not in inst) or not(isinstance(inst.trading_hours,str)):
# 跳过没有交易时间段或者交易时间段无效的合约
continue
inst_name = inst.trading_code if stype == 'Future' else inst.order_book_id
inst_name = inst_name.upper()
if inst_name.find('.') < 0:
inst_name += '.' + inst.exchange
if inst_name not in ths_dict:
str_trading_hours = inst.trading_hours
# 把'01-'或'31-'者替换成'00-'或'30-'
suffix_pair = [('1-','0-'),('6-','5-')]
for s1,s2 in suffix_pair:
str_trading_hours = str_trading_hours.replace(s1,s2)
# 如果原来没有,提取出来
trade_hours[inst_name] = {"name": inst.symbol,"trading_hours": str_trading_hours}
# print(f"trade_hours old count {len(ths_dict)},append count={len(trade_hours)}")
if trade_hours:
ths_dict.update(trade_hours)
save_json(self.trading_hours_file,ths_dict)
return True
def load_all_trading_hours(self) -> dict: # hxxjava add
""" 从trading_hours.json文件中读取所有合约的交易时间段 """
json_file = get_file_path(self.trading_hours_file)
if not json_file.exists():
return {}
else:
return load_json(self.trading_hours_file)
在vnpy\trader\engine.py中:
from .datafeed import get_datafeed # hxxjava add
def get_trading_hours(self,vt_symbol:str) -> str: # hxxjava add
""" get vt_symbol's trading hours """
ths = self.all_trading_hours.get(vt_symbol.upper(),"")
return ths["trading_hours"] if ths else ""
因为无论你运行vnpy中的哪个app,你都会启动main_engine,无需绕弯子就可以得到这些信息,而我们的用户策略中都包含各自策略的引擎,这样就方便获取交易时间段信息。
如CTA策略中包含cta_engine,而cta_engine它的成员就包含main_engine。那么在策略中执行类似下面的语句就可以获取您交易品种的交易时间段信息:
trading_hours = self.cta_engine.main_engine.get_trading_hours(selt.vt_symbol)
如PortFolioStrategy策略中包含strategy_engine,而strategy_engine它的成员就包含main_engine。那么在策略中执行类似下面的语句就可以获取多个交易品种的交易时间段信息:
trading_hours_list = [self.cta_engine.main_engine.get_trading_hours(vt_symbol) for vt_symbol in self.vt_symbols]
是不是很方便呢?
vnpy 3.0的启动界面中已经集成了一个叫“投研”的功能,其实它是jupyter lab,启动之后输入下面的代码:
# 测试update_all_trading_hours()函数和load_all_trading_hours()
from vnpy.trader.datafeed import get_datafeed
df = get_datafeed()
df.init()
df.update_all_trading_hours() # 更新所有合约的交易时间段到本地文件中
ths = df.load_all_trading_hours() # 从本地文件中读取所有合约的交易时间段
当然您可以在vnpy的trader中主界面的菜单中增加一项,方便您在需要的时候执行下面语句。不过这个更新交易时间段的功能并不需要频繁执行,手动也就够了,记得就好。
经过上面步骤3.4.4,您就在本地得到了一个trading_hours.json文件,该文件在您的用户目录下的.vntrader\中,其内容如下:
{
"A0303.DCE": {
"name": "豆一0303",
"trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
},
"A0305.DCE": {
"name": "豆一0305",
"trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
},
"A0307.DCE": {
"name": "豆一0307",
"trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
},
"A0309.DCE": {
"name": "豆一0309",
"trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
},
"A0311.DCE": {
"name": "豆一0311",
"trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
},
"A0401.DCE": {
"name": "豆一0401",
"trading_hours": "21:00-23:00,09:00-10:15,10:30-11:30,13:30-15:00"
},
... ...
}
观察其格式,在你没有米筐数据接口或者这里没有的合约,您也可以手动输入合约交易时间段信息。
按照程序中算法,这个文件文件中一共包含约16500多个合约的交易时间段信息。可以覆盖国内金融市场几乎全部都产品,但是不包括金融二次衍生品期权。
为什么没有期权交易时间段信息,因为不需要。期权合约有其对应的标的物,从其名称和编号就可以解析出来。期权合约的交易时间段其和标的物的交易时间段是完全相同的,因此不需要保存到该文件中。
上周升级到了vnpy 2.9.0版本,编写了个策略,用到了30分钟Bar。
self.dir_bg = BarGenerator(on_bar = self.on_bar,window = 30,
on_window_bar = self.on_30m_bar,interval = Interval.MINUTE)
那个意思就是创建一个30分钟bar合成器。
策略的on_30m_bar()是这样的,先打印出来看看:
def on_30m_bar(self, bar: BarData):
"""
收到方向周期的K线
"""
print(f"{self.strategy_name}收到30分钟周期K线{bar}")
结果杯具了:
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 21, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=225506.0, turnover=10790057010.0, open_interest=1921172.0, open_price=4788.0, high_price=4808.0, low_price=4762.0, close_price=4777.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 21, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=179987.0, turnover=8570390020.0, open_interest=1903437.0, open_price=4778.0, high_price=4778.0, low_price=4751.0, close_price=4760.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 22, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=99381.0, turnover=4723786710.0, open_interest=1905948.0, open_price=4760.0, high_price=4766.0, low_price=4743.0, close_price=4746.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 23, 22, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=83782.0, turnover=3985661600.0, open_interest=1904511.0, open_price=4745.0, high_price=4767.0, low_price=4744.0, close_price=4763.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 9, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=99253.0, turnover=4714720470.0, open_interest=1916969.0, open_price=4763.0, high_price=4766.0, low_price=4738.0, close_price=4748.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 9, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=85886.0, turnover=4076563050.0, open_interest=1930796.0, open_price=4750.0, high_price=4760.0, low_price=4735.0, close_price=4736.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 10, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=491666.0, turnover=23080991050.0, open_interest=1982231.0, open_price=4736.0, high_price=4741.0, low_price=4660.0, close_price=4665.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 11, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=279000.0, turnover=13059711390.0, open_interest=2005223.0, open_price=4666.0, high_price=4713.0, low_price=4654.0, close_price=4676.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 13, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=271595.0, turnover=12701729900.0, open_interest=2021599.0, open_price=4664.0, high_price=4709.0, low_price=4648.0, close_price=4673.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 14, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=266018.0, turnover=12358486960.0, open_interest=2082998.0, open_price=4673.0, high_price=4674.0, low_price=4622.0, close_price=4623.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 14, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=175873.0, turnover=8162467860.0, open_interest=2081475.0, open_price=4624.0, high_price=4654.0, low_price=4624.0, close_price=4637.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 21, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=312119.0, turnover=14487421790.0, open_interest=2043795.0, open_price=4635.0, high_price=4664.0, low_price=4613.0, close_price=4655.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 21, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=106717.0, turnover=4970359120.0, open_interest=2025130.0, open_price=4656.0, high_price=4666.0, low_price=4648.0, close_price=4655.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 22, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=60255.0, turnover=2807077210.0, open_interest=2011503.0, open_price=4657.0, high_price=4665.0, low_price=4652.0, close_price=4659.0)
GsjyDemo2收到30分钟周期K线BarData(gateway_name='RQ', symbol='rb2205', exchange=<Exchange.SHFE: 'SHFE'>, datetime=datetime.datetime(2022, 2, 24, 22, 30, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>), interval=None, volume=184047.0, turnover=8524883500.0, open_interest=1989335.0, open_price=4660.0, high_price=4661.0, low_price=4608.0, close_price=4614.0)
找到BarGenerator的错误了:
def update_bar_minute_window(self, bar: BarData) -> None:
""""""
# If not inited, create window bar object
if not self.window_bar:
dt = bar.datetime.replace(second=0, microsecond=0)
self.window_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=dt,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price
)
# Otherwise, update high/low price into window bar
else:
self.window_bar.high_price = max(
self.window_bar.high_price,
bar.high_price
)
self.window_bar.low_price = min(
self.window_bar.low_price,
bar.low_price
)
# Update close price/volume/turnover into window bar
self.window_bar.close_price = bar.close_price
self.window_bar.volume += bar.volume
self.window_bar.turnover += bar.turnover
self.window_bar.open_interest = bar.open_interest
Check if window bar completed
# 这里错误了,用当前1分钟到分钟数+1与30取模来决定一个30分钟K线是否结束,
# 先推送已合成bar,在生成下一个新的30分钟bar。
# 可是10:15-10:30是休市时间段,永远也等不到10:29分钟到那个1分钟bar,所以只能在10:59符合条件,
# 因此这个30分钟bar实际上包含了45分钟到交易数据,错误!!!
if not (bar.datetime.minute + 1) % self.window:
self.on_window_bar(self.window_bar)
self.window_bar = None
问题分析清楚了,就不再解释怎么修改了,直接上修改的BarGenerator完整代码吧。
BarGenerator在vnpy.trader.utility中,拷贝过去替换就OK了。
测试过了,和文华6产生的30分钟K线一模一样。
如果想知道哪里修改了,查找 # hxxjava就可以找到修改处。
vnpy/trader/utility.py的前面添加引用:
from datetime import timedelta
BarGenerator的修改如下:
class BarGenerator:
"""
For:
1. generating 1 minute bar data from tick data
2. generating x minute bar/x hour bar data from 1 minute data
Notice:
1. for x minute bar, x must be able to divide 60: 2, 3, 5, 6, 10, 15, 20, 30
2. for x hour bar, x can be any number
"""
def __init__(
self,
on_bar: Callable,
window: int = 0,
on_window_bar: Callable = None,
interval: Interval = Interval.MINUTE,
daily_close_time:str = "15:00"
):
"""Constructor"""
self.bar: BarData = None
self.on_bar: Callable = on_bar
self.interval: Interval = interval
self.interval_count: int = 0
self.hour_bar: BarData = None
self.window: int = window
self.count_for_window : int = 0 # hxxjava add
self.window_bar: BarData = None
self.on_window_bar: Callable = on_window_bar
self.last_tick: TickData = None
self.daily_close_time = daily_close_time
def update_tick(self, tick: TickData) -> None:
"""
Update new tick data into generator.
"""
new_minute = False
# Filter tick data with 0 last price
if not tick.last_price:
return
# Filter tick data with older timestamp
if self.last_tick and tick.datetime < self.last_tick.datetime:
return
if not self.bar:
new_minute = True
elif (
(self.bar.datetime.minute != tick.datetime.minute)
or (self.bar.datetime.hour != tick.datetime.hour)
):
self.bar.datetime = self.bar.datetime.replace(
second=0, microsecond=0
)
self.on_bar(self.bar)
new_minute = True
if new_minute:
self.bar = BarData(
symbol=tick.symbol,
exchange=tick.exchange,
interval=Interval.MINUTE,
datetime=tick.datetime,
gateway_name=tick.gateway_name,
open_price=tick.last_price,
high_price=tick.last_price,
low_price=tick.last_price,
close_price=tick.last_price,
open_interest=tick.open_interest
)
else:
self.bar.high_price = max(self.bar.high_price, tick.last_price)
if tick.high_price > self.last_tick.high_price:
self.bar.high_price = max(self.bar.high_price, tick.high_price)
self.bar.low_price = min(self.bar.low_price, tick.last_price)
if tick.low_price < self.last_tick.low_price:
self.bar.low_price = min(self.bar.low_price, tick.low_price)
self.bar.close_price = tick.last_price
self.bar.open_interest = tick.open_interest
self.bar.datetime = tick.datetime
if self.last_tick:
volume_change = tick.volume - self.last_tick.volume
self.bar.volume += max(volume_change, 0)
turnover_change = tick.turnover - self.last_tick.turnover
self.bar.turnover += max(turnover_change, 0)
self.last_tick = tick
def update_bar(self, bar: BarData) -> None:
"""
Update 1 minute bar into generator
"""
if self.interval == Interval.MINUTE:
self.update_bar_minute_window(bar)
else:
self.update_bar_hour_window(bar)
def update_bar_minute_window(self, bar: BarData) -> None:
""""""
# If not inited, create window bar object
if not self.window_bar:
dt = bar.datetime.replace(second=0, microsecond=0)
self.window_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=dt,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price
)
# Otherwise, update high/low price into window bar
else:
self.window_bar.high_price = max(
self.window_bar.high_price,
bar.high_price
)
self.window_bar.low_price = min(
self.window_bar.low_price,
bar.low_price
)
# Update close price/volume/turnover into window bar
self.window_bar.close_price = bar.close_price
self.window_bar.volume += bar.volume
self.window_bar.turnover += bar.turnover
self.window_bar.open_interest = bar.open_interest
# Check if window bar completed
# if not (bar.datetime.minute + 1) % self.window:
# self.on_window_bar(self.window_bar)
# self.window_bar = None
# hxxjava add start
h,m = self.daily_close_time.split(':')
today_close_time = bar.datetime.replace(hour=int(h),minute=int(m),second=0,microsecond=0)
enter_next_day = bar.datetime + timedelta(minutes=1) == today_close_time
if self.count_for_window + 1 == self.window or enter_next_day:
self.on_window_bar(self.window_bar)
self.window_bar = None
if enter_next_day:
self.count_for_window = 0
else:
self.count_for_window += 1
self.count_for_window %= self.window
# hxxjava add end
def update_bar_hour_window(self, bar: BarData) -> None:
""""""
# If not inited, create window bar object
if not self.hour_bar:
dt = bar.datetime.replace(minute=0, second=0, microsecond=0)
self.hour_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=dt,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price,
close_price=bar.close_price,
volume=bar.volume,
turnover=bar.turnover,
open_interest=bar.open_interest
)
return
finished_bar = None
# If minute is 59, update minute bar into window bar and push
if bar.datetime.minute == 59:
self.hour_bar.high_price = max(
self.hour_bar.high_price,
bar.high_price
)
self.hour_bar.low_price = min(
self.hour_bar.low_price,
bar.low_price
)
self.hour_bar.close_price = bar.close_price
self.hour_bar.volume += bar.volume
self.hour_bar.turnover += bar.turnover
self.hour_bar.open_interest = bar.open_interest
finished_bar = self.hour_bar
self.hour_bar = None
# If minute bar of new hour, then push existing window bar
elif bar.datetime.hour != self.hour_bar.datetime.hour:
finished_bar = self.hour_bar
dt = bar.datetime.replace(minute=0, second=0, microsecond=0)
self.hour_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=dt,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price,
close_price=bar.close_price,
volume=bar.volume,
turnover=bar.turnover,
open_interest=bar.open_interest
)
# Otherwise only update minute bar
else:
self.hour_bar.high_price = max(
self.hour_bar.high_price,
bar.high_price
)
self.hour_bar.low_price = min(
self.hour_bar.low_price,
bar.low_price
)
self.hour_bar.close_price = bar.close_price
self.hour_bar.volume += bar.volume
self.hour_bar.turnover += bar.turnover
self.hour_bar.open_interest = bar.open_interest
# Push finished window bar
if finished_bar:
self.on_hour_bar(finished_bar)
def on_hour_bar(self, bar: BarData) -> None:
""""""
if self.window == 1:
self.on_window_bar(bar)
else:
if not self.window_bar:
self.window_bar = BarData(
symbol=bar.symbol,
exchange=bar.exchange,
datetime=bar.datetime,
gateway_name=bar.gateway_name,
open_price=bar.open_price,
high_price=bar.high_price,
low_price=bar.low_price
)
else:
self.window_bar.high_price = max(
self.window_bar.high_price,
bar.high_price
)
self.window_bar.low_price = min(
self.window_bar.low_price,
bar.low_price
)
self.window_bar.close_price = bar.close_price
self.window_bar.volume += bar.volume
self.window_bar.turnover += bar.turnover
self.window_bar.open_interest = bar.open_interest
self.interval_count += 1
if not self.interval_count % self.window:
self.interval_count = 0
self.on_window_bar(self.window_bar)
self.window_bar = None
def generate(self) -> Optional[BarData]:
"""
Generate the bar data and call callback immediately.
"""
bar = self.bar
if self.bar:
bar.datetime = bar.datetime.replace(second=0, microsecond=0)
self.on_bar(bar)
self.bar = None
return bar
本人修改原则是n分钟bar按照日内对齐的原则,即:
注意到BarGenerator的构造函数多了个daily_close_time参数,字符串类型,默认值为"15:00"。
例如:
self.bg30m = BarGenerator(on_bar = self.on_bar,window = 30,on_window_bar = self.on_30m_bar,interval = Interval.MINUTE) # 默认15:00收市
但是如果有些例如国债等品种,它的收市时间不是15:00,则需要在特别传参,在写作交易策略的时候,可以给出代表收市时间的字符串参数,供创建实例的时候传递给该参数。虽然麻烦了一丢丢,但是已经可以算得上是够方便的啦!
例如:
self.bg30m = BarGenerator(on_bar = self.on_bar,window = 30,
on_window_bar = self.on_30m_bar,
interval = Interval.MINUTE,
daily_close_time= "16:00" )
self.bg30m = BarGenerator(on_bar = self.on_bar,window = 30,
on_window_bar = self.on_30m_bar,
interval = Interval.MINUTE,
daily_close_time= "5:00" )
(贴子标题不小心打错字了,无法修改 ,抱歉!!!)
首先用VSCode创建新文档,把它保存为扩展名为md的文档,这就表示它是一个markdown文档了。
说明:
- 虽然vn.py官网支持markdown语法,但是支持的还不够全面(也许是咱不熟悉),我编写的演示文档有许多地方还是不可在vn.py官网,所以我把不可以直接被显示的地方贴在显示效果之前,您可以把这些代码复制到VSCode中,在markdown相关插件齐全的情况下,是可以成功显示的。
- 例如后面Tek数学公式可mermaid绘图语法的演示,分成实现代码和显示效果,您可以复制到VSCode中,参考示例的写法,修改成符合自己要求的各种图。
无序列表
标题的写法:Markdown标题由 ‘#’ 开始:
# —— 一级标题
## —— 二级标题
### —— 三级标题
#### —— 四级标题
##### —— 五级标题
###### —— 六级标题
其中侧边栏上半部分显示的是当前工作区文件夹下的文件,包括 Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。
这里是引用演示:
> 1st reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。
>> 2nd reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。
>>> third level reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。
显示效果:
1st reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。2nd reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。third level reference
Markdown 文件和素材。下半部分可以展开「Outline」视图,即大纲,可以根据当前正在编辑的 Markdown 文件显示其各级标题的大纲目录。
下面是python代码示例:
python
import time
# Quick,count to ten!for i in range( 10):
# ( but not *too* quick)time.sleep(0.5)
for i in range(1,100):
print(i)
这是行内的代码:x = this.count
,它是嵌入到文字中的。
无序列表
有序列表
字体修饰(加重、斜体、删除)
加重显示 值得一提的是斜体字,
VS Code 的 Markdown 预览默认渲染的是当前你正在编辑的文档,不过如果你希望将某个文档的预览渲染锁定不动,可以通过「Markdown: Toggle Preview Locking」调出一个锁定某个文档的预览界面。
···
InLine math equations:$\omega = d\phi / dt$. Display math should get its own line like so:
$$I = \int \rho R^{2} dV$$
$$ 2+3 = 5 $$
$x+y=z^2$
$ax+by=cz$
$2x+3y=4z$
···
显示:
用代码插入图片示例 !

显示:
剪贴板图片插入示例 !
- 先找到自己需要的界面,将图形复制到剪贴板
- 在VSCode中编辑的md文档需要插入图片的位置,点击鼠标右键,选择“Markdown Paste”或者Ctrl+Alt+V快捷键,进入下面的图片命名环节,可以默认,也可以另外输入新名称。
- 完成上面的两步后,md文档就被自动添加了类似下面的语句:

,当然它和手工输入是同样的显示效果。
```mermaid
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John,how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts<br/>prevail...
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!|
John-->Alice:我爱你!
```
显示:
```mermaid
sequenceDiagram
Note right of A: 倒霉, 碰到B了
A->B: Hello B, how are you ?
note left of B: 倒霉,碰到A了
B-->A: Fine, thx, and you?
note over A,B: 快点溜,太麻烦了
A->>B: I'm fine too.
note left of B: 快点打发了A
B-->>A: Great!
note right of A: 溜之大吉
A-xB: Wait a moment
loop Look B every minute
A->>B: look B, go?
B->>A: let me go?
end
B--xA: I'm off, byte
note right of A: 太好了, 他走了
```
```mermaid
sequenceDiagram
Alice->>Bob: Hello Bob, how are you?
alt is sick
Bob->>Alice:not so good :(
else is well
Bob->>Alice:good
end
opt Extra response
Bob->>Alice:Thanks for asking
end
```
```mermaid
sequenceDiagram
# 通过设定参与者(participant)的顺序控制展示顺序
participant B
participant A
Note right of A: 倒霉, 碰到B了
A->B: Hello B, how are you ?
note left of B: 倒霉,碰到A了
B-->A: Fine, thx, and you?
note over A,B:快点溜,太麻烦了。。。
A->>B: I'm fine too.
note left of B: 快点打发了A
B-->>A: Great!
note right of A: 溜之大吉
A-xB: Wait a moment
loop Look B every minute
A->>B: look B, go?
B->>A: let me go?
end
B--xA: I'm off, byte
note right of A: 太好了, 他走了
```
```mermaid
sequenceDiagram
# 通过设定参与者(participants)的顺序控制展示模块顺序
participant Alice
participant Bob
participant John
Alice->John:Hello John,how are you?
loop Healthcheck
John->John:Fight against hypochondria
end
Note right of John:Rational thoughts <br/>prevail... John-->Alice:Great!
John->Bob: How about you?
Bob-->John: good!
```
```mermaid
gantt
dateFormat YYYY-MM-DD
section S1
T1: 2014-01-01, 9d
section S2
T2: 2014-01-11, 9d
section S3
T3: 2014-01-02, 9d
```
```mermaid
gantt
dateFormat YYYY-MM-DD
title Adding GANTT diagram functionality to mermaid
section A section
Completed task :done, des1, 2014-01-06,2014-01-08
Active task :active, des2, 2014-01-09, 3d
Future task : des3, after des2, 5d
Future task2 : des4, after des3, 5d
section Critical tasks
Completed task in the critical line :crit, done, 2014-01-06,24h
Implement parser and jison :crit, done, after des1, 2d
Create tests for parser :crit, active, 3d
Future task in critical line :crit, 5d
Create tests for renderer :2d
Add to mermaid :1d
section Documentation
Describe gantt syntax :active, a1, after des1, 3d
Add gantt diagram to demo page :after a1 , 20h
Add another diagram to demo page :doc1, after a1 , 48h
section Last section
Describe gantt syntax :after doc1, 3d
Add gantt diagram to demo page : 20h
Add another diagram to demo page : 48h
```
```mermaid
classDiagram
Class01 <|-- AveryLongClass : Cool
Class03 *-- Class04
Class05 o-- Class06
Class07 .. Class08
Class09 --> C2 : Where am i?
Class09 --* C3
Class09 --|> Class07
Class07 : equals()
Class07 : Object[] elementData
Class01 : size()
Class01 : int chimp
Class01 : int gorilla
Class08 <--> C2: Cool label
```
```mermaid
pie
"Dogs" : 386
"Cats" : 85
"Rats" : 15
```
```mermaid
erDiagram
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE-ITEM : contains
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
```
```mermaid
journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
Do work: 1: Me, Cat
section Go home
Go downstairs: 5: Me
Sit down: 5: Me
```
本文给您介绍了这些文档元素:
- 各级标题(1~6级)
- 文本修饰(字体、颜色、加粗、斜体、删除线...)
- 无序列表
- 有序列表
- 程序代码(行内和独立)
- 引用
- LaTex数学表达式(行内和独立)
- 表格(包括对齐方式)
- 图片(手工插入和自动插入)
- 超链接
- 各种Mermaid图
- 序列图
- 流程图
- 甘特图
- 类图
- 饼图
- 实体
- 旅游图
掌握了上述的Markdown语法,您可以轻松编写自己的Markdown文档了!
overview.start = min(bars[0].datetime, overview.start)
overview.end = max(bars[-1].datetime, overview.end)
它会把DbBarData中相同symbol、exchange和interval的所有bar的生成一个数据总揽overview,
并且overview的起止时间分别为这些bar最早时间戳和最晚时间戳——无论这些bars是由几个时间段构成的。
也就是说,如果您曾经下载过过螺纹rb2201.SHFE的日线数据:
这三次下载后,rb2201.SHFE的日线数据的BarOverview的起止时间:2021-1-16~2021-12-22,而实际上数据库中是不存在2021-5-17~2021-11-30,除非我们查询明细,否则我们不知道它们还缺少哪些日期的K线数据!
我觉得对于下载的时间段重叠的可以合并成为一个BarOverview,目前vnpy在这点上的处理就很好。
而对于两个没有任何重叠时间段的两段bars是不应该合并的,因为这会引起误导!仔细地研究下数据管理模块的界面,就会明白我的意思了。
在jupyter notebook中,我编写了下面的一段下载合约K线数据的代码:
from datetime import datetime,date,timedelta
from vnpy.trader.utility import extract_vt_symbol
from vnpy.trader.constant import Exchange,Interval
from vnpy.trader.object import HistoryRequest
from vnpy.trader.database import get_database
from vnpy.trader.datafeed import get_datafeed
df = get_datafeed()
end = datetime.now()
start = end.replace(month=11)
start,end
req = HistoryRequest(symbol='rb2201',exchange=Exchange.SHFE,start=start,end=end,interval=Interval.DAILY)
# 语句1
bars = df.query_bar_history(req)
# 语句2
db.save_bar_data(bars)
# 语句3
for bar in bars:
print(bar)
执行上面代码,当执行到语句3的时候,提示:
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-11-f72a667896f4> in <module>
1 for bar in bars:
----> 2 print(bar)
D:\ProgramFiles\vnstudio\lib\dataclasses.py in __repr__(self)
AttributeError: 'BarData' object has no attribute 'gateway_name'
如果把语句2注释掉,执行语句3的时候有没有任何问题,可以把从米筐接口读取的rb2201.SHFE的bar全部打印出来。
由于本人使用的是mysql数据库,所以需要对vnpy_mysql目录下的mysql_database.py进行修改,使用其他数据库的用户其实也有同样的问题。解决方法也是一样的,不再每个都说。
方法:
打开vnpy_mysql\mysql_database.py,在文件头部添加下面的语句:
import copy # hxxjava
对下面两个函数进行如下的修改,save_bar_data()和save_tick_data()都有同样的问题,修改之处语句在函数中注明了,错误原因是在遍历保存bars和ticks的过程中,因为数据库的要求,改变了每个bar和tick的 __dict__ 字典,保存动作做完了以后,列表bars和列表ticks的每个bar和tick已经被改得面目全非了,里面的gateway_name已经没有了。
修改过的代码后面有 # hxxjava change注释标记。
修改后的代码如下:
def save_bar_data(self, bars: List[BarData]) -> bool:
"""保存K线数据"""
# 读取主键参数
bar = bars[0]
symbol = bar.symbol
exchange = bar.exchange
interval = bar.interval
# 将BarData数据转换为字典,并调整时区
data = []
for bar in bars:
bar.datetime = convert_tz(bar.datetime)
d = copy.copy(bar.__dict__) # hxxjava change
d["exchange"] = d["exchange"].value
d["interval"] = d["interval"].value
d.pop("gateway_name")
d.pop("vt_symbol")
data.append(d)
# 使用upsert操作将数据更新到数据库中
with self.db.atomic():
for c in chunked(data, 50):
DbBarData.insert_many(c).on_conflict_replace().execute()
# 更新K线汇总数据
overview: DbBarOverview = DbBarOverview.get_or_none(
DbBarOverview.symbol == symbol,
DbBarOverview.exchange == exchange.value,
DbBarOverview.interval == interval.value,
)
if not overview:
overview = DbBarOverview()
overview.symbol = symbol
overview.exchange = exchange.value
overview.interval = interval.value
overview.start = bars[0].datetime
overview.end = bars[-1].datetime
overview.count = len(bars)
else:
overview.start = min(bars[0].datetime, overview.start)
overview.end = max(bars[-1].datetime, overview.end)
s: ModelSelect = DbBarData.select().where(
(DbBarData.symbol == symbol)
& (DbBarData.exchange == exchange.value)
& (DbBarData.interval == interval.value)
)
overview.count = s.count()
overview.save()
return True
def save_tick_data(self, ticks: List[TickData]) -> bool:
"""保存TICK数据"""
# 将TickData数据转换为字典,并调整时区
data = []
for tick in ticks:
tick.datetime = convert_tz(tick.datetime)
d = copy.copy(tick.__dict__) # hxxjava change
d["exchange"] = d["exchange"].value
d.pop("gateway_name")
d.pop("vt_symbol")
data.append(d)
# 使用upsert操作将数据更新到数据库中
with self.db.atomic():
for c in chunked(data, 50):
DbTickData.insert_many(c).on_conflict_replace().execute()
return True
详细见 报单流控、查询流控和会话数控制,这里不再赘述,有兴趣的读者可以去看看。
让我们看看下面的ctp_gateway的td_api.send_order()代码:
def send_order(self, req: OrderRequest) -> str:
"""委托下单"""
# .... 这里省略
self.reqid += 1
self.reqOrderInsert(ctp_req, self.reqid) # 这里有问题
orderid: str = f"{self.frontid}_{self.sessionid}_{self.order_ref}"
order: OrderData = req.create_order_data(orderid, self.gateway_name)
self.gateway.on_order(order)
return order.vt_orderid
上面代码中self.reqOrderInsert(ctp_req, self.reqid)就没有考虑报单流控,它有4个返回值:
0,代表成功。
-1,表示网络连接失败;
-2,表示未处理请求超过许可数;
-3,表示每秒发送请求数超过许可数。
其中两个是有关流控的,如果是-2,-3我们是可以采用类似等待、重试、队列等方法的,可是都没有。
这导致的问题是如果是返回了-2,-3,那么,报单是没有成功的,上层app还以为已经成功了,也不会采用什么其它逻辑来补救。
CTP交易接口一定的报单和查询必须符合CTP流控规则,默认FTD报文流控如果是每秒6次,报单和查询都会形成FTD报文,查询流控为1秒1次,报单流控最大是每秒6次,假如当前秒已经有了1次查询,那么报单的流控就只有5次了。
如果报单超越了当前的流控,那么报单坑定是失败的,交易服务器是不会接受报单请求的,无论你是开仓还是平仓,CTP交易接口根本没有成交的可能!
这样就造成我们以为发出去的符合条件成交条件委托是成功的,可是却是石沉大海,没有了下文。
实际的危害可能是:机会来了无法入场,灾难来临无法逃离 !
改造的主要思路是:只要没有违反流控,直接快速执行报单请求和查询请求,如遇违反流控,让CTP网关缓冲报单请求和查询请求,延后执行。
本次分享的ctp_gateway比当前vnpy系统的ctp_gateway具备更为丰富的接口,其中包括:
为了提高接口推送消息的能力,在ctp_gateway登录交易接口时,先连续不停地接收并解码合约状态、交易所保证金和合约信息这三个数量巨大的推送数据,然后分别一次性推送这些数据到系统的消息引擎中。这些消息接收端应该一次性地对这些消息进行处理。
class HedgeType(Enum):
"""
投机/套保/备兑类型 hxxjava add
"""
SEPCULATION = '1' #"投机"
ARBITRAGE = '2' #"套利"
HEDGE = '3' #"套保"
MARKETMAKER = '5' #"做市商"
SPECHEDGE = '6' # 第一腿投机第二腿套保 大商所专用
HEDGESPEC = '7' # 第一腿套保第二腿投机 大商所专用
class InstrumentStatus(Enum):
"""
合约交易状态类型 hxxjava debug
"""
BEFORE_TRADING = "开盘前"
NO_TRADING = "非交易"
CONTINOUS = "连续交易"
AUCTION_ORDERING = "集合竞价报单"
AUCTION_BALANCE = "集合竞价价格平衡"
AUCTION_MATCH = "集合竞价撮合"
CLOSE = "收盘"
# 有效交易状态
VALID_TRADE_STATUSES = [
InstrumentStatus.CONTINOUS,
InstrumentStatus.AUCTION_ORDERING,
InstrumentStatus.AUCTION_BALANCE,
InstrumentStatus.AUCTION_MATCH
]
# 集合竞价交易状态
AUCTION_STATUS = [
InstrumentStatus.AUCTION_ORDERING,
InstrumentStatus.AUCTION_BALANCE,
InstrumentStatus.AUCTION_MATCH
]
class StatusEnterReason(Enum):
"""
品种进入交易状态原因类型 hxxjava debug
"""
AUTOMATIC = "自动切换"
MANUAL = "手动切换"
FUSE = "熔断"
class MarginPriceType(Enum):
"""
保证金价格类型
"""
# 昨结算价
PRE_SETTLEMENT_PRICE = '1'
# 最新价
SETTLEMENT_PRICE = '2'
# 成交均价
$ERAGE_PRICE = '3'
# 开仓价
OPEN_PRICE = '4'
class AlgorithmType(Enum):
"""
盈亏算法类型
"""
# 浮盈浮亏都计算
ALL = '1'
# 浮盈不计,浮亏计
ONLY_LOST ='2'
# 浮盈计,浮亏不计
ONLY_GAIN = '3'
# 浮盈浮亏都不计算
NONE = '4'
class IncludeCloseProfitType(Enum):
"""
是否包含平仓盈利类型
"""
# 包含平仓盈利
INCLUDE = '0'
# 不包含平仓盈利
NOT_INCLUDE = '2'
class OptionRoyaltyPriceType(Enum):
"""
期权权利金价格类型类型
"""
# 昨结算价
PRE_SETTLEMENT_PRICE = '1'
# 开仓价
OPEN_PRICE = '4'
# 最新价与昨结算价较大值
MAX_PRE_SETTLEMENT_PRICE = '5'
class IdCardType(Enum):
""" $类型 """
# 组织机构代码
EID = '0'
# 中国公民$
IDCard = '1'
# $
OfficerIDCard = '2'
# $
PoliceIDCard = '3'
# $
SoldierIDCard = '4'
# 户口簿
HouseholdRegister = '5'
# $
Passport = '6'
# 台胞证
TaiwanCompatriotIDCard = '7'
# 回乡证
HomeComingCard = '8'
# 营业执照号
LicenseNo = '9'
# 税务登记号/当地纳税ID
TaxNo = 'A'
# 港澳居民来往内地通行证
HMMainlandTravelPermit = 'B'
# 台湾居民来往大陆通行证
TwMainlandTravelPermit = 'C'
# $
DrivingLicense = 'D'
# 当地社保ID
SocialID = 'F'
# 当地$
LocalID = 'G'
# 商业登记证
BusinessRegistration = 'H'
# 港澳永久性居民$
HKMCIDCard = 'I'
# 人行开户许可证
AccountsPermits = 'J'
# 外国人永久居留证
FrgPrmtRdCard = 'K'
# 资管产品备案函
CptMngPrdLetter = 'L'
# 统一社会信用代码
UniformSocialCreditCode = 'N'
# 机构成立证明文件
CorporationCertNo = 'O'
# 其他$
OtherCard = 'x'
class ProductClass(Enum):
# 期货
FUTURES = '1'
# 期货期权
OPTIONS = '2'
# 组合
COMBINATION = '3'
# 即期
SPOT = '4'
# 期转现
EFP = '5'
# 现货期权
SPOT_OPTION = '6'
# TAS合约
TAS = '7'
# 金属指数
MI = 'I'
因为本文的内容比较长,一共分为5个帖子,其他内容在下方帖子中 ... ...
一个交易所可能有多个柜台连接,比如CTP柜台、恒牛柜台、金仕达柜台,不管来自哪个柜台,是不是报单编号和成交编号在一个交易 日内,在交易所内是唯一的吗?
报单编号是唯一的, 但是成交编号是不唯一的,因为撮合的双方,成交编号是一致的。也就是说一个报单可能在撮合过程中被拆分为多个成交单,交易所赋予成交单的相同的成交编号会被推送给多空双方,同时还包含了个不相同的报单编号。
另外,因为自成交情况存在,所以在写程序时需要注意,用成交方向+成交编号才能确定笔成交记录。
修改vnpy\trader\event.py,添加如下内容:
EVENT_ORIGIN_TICK = "eOriginTick." # 原始tick消息
EVENT_AUCTION_TICK = "eAuctionTick." # 集合竞价tick消息
EVENT_STATUS = "eStatus." # 交易状态消息
EVENT_STATUS_END = "eStatusEnd." # 交易状态结束消息
修改vnpy\trader\constant.py,添加如下内容:
class InstrumentStatus(Enum):
"""
合约交易状态类型 hxxjava debug
"""
BEFORE_TRADING = "开盘前"
NO_TRADING = "非交易"
CONTINOUS = "连续交易"
AUCTION_ORDERING = "集合竞价报单"
AUCTION_BALANCE = "集合竞价价格平衡"
AUCTION_MATCH = "集合竞价撮合"
CLOSE = "收盘"
# 有效交易状态
VALID_TRADE_STATUSES = [
InstrumentStatus.CONTINOUS,
InstrumentStatus.AUCTION_ORDERING,
InstrumentStatus.AUCTION_BALANCE,
InstrumentStatus.AUCTION_MATCH
]
# 集合竞价交易状态
AUCTION_STATUS = [
InstrumentStatus.AUCTION_ORDERING,
InstrumentStatus.AUCTION_BALANCE,
InstrumentStatus.AUCTION_MATCH
]
class StatusEnterReason(Enum):
"""
品种进入交易状态原因类型 hxxjava debug
"""
AUTOMATIC = "自动切换"
MANUAL = "手动切换"
FUSE = "熔断"
修改vnpy\trader\object.py,添加如下内容:
3.1 在文件的前面添加这样的内容:
from .constant import InstrumentStatus,StatusEnterReason
3.2 在文件的后面添加下面的内容:
def left_alphas(instr:str):
""" get lefe alphas of a string """
ret_str = ''
for s in instr:
if s.isalpha():
ret_str += s
else:
break
return ret_str
@dataclass
class StatusData(BaseData):
"""
hxxjava debug
"""
symbol:str
exchange : Exchange
settlement_group_id : str = ""
instrument_status : InstrumentStatus = None
trading_segment_sn : int = None
enter_time : str = ""
enter_reason : StatusEnterReason = StatusEnterReason.AUTOMATIC
exchange_inst_id : str = ""
def __post_init__(self):
""" """
self.vt_symbol = f"{self.symbol}.{self.exchange.value}"
def belongs_to(self,vt_symbol:str):
symbol,exchange_str = vt_symbol.split(".")
instrument = left_alphas(symbol).upper()
return (self.symbol.upper() == instrument) and (self.exchange.value == exchange_str)
修改vnpy\trader\gateway.py,添加下面内容:
添加引用部分
from .event import EVENT_ORIGIN_TICK,EVENT_STATUS, EVENT_STATUS_END
from .object import StatusData, # hxxjava debug
这样修改on_tick():
def on_tick(self, tick: TickData) -> None:
"""
Tick event push.
Tick event of a specific vt_symbol is also pushed.
"""
# self.on_event(EVENT_TICK, tick)
# self.on_event(EVENT_TICK + tick.vt_symbol, tick)
self.on_event(EVENT_ORIGIN_TICK, tick)
添加下面的两个函数:
def on_status(self, status: StatusData) -> None: # hxxjava debug
"""
Instrument Status event push.
"""
self.on_event(EVENT_STATUS, status)
self.on_event(EVENT_STATUS + status.vt_symbol, status)
def on_status_end(self, stats: List[StatusData]) -> None: # hxxjava debug
"""
Instrument Status list event push.
"""
self.on_event(EVENT_STATUS_END, stats)
修改vnpy_ctp\gateway\ctp_gateway.py,步骤如下:
from vnpy.trader.constant import InstrumentStatus,StatusEnterReason
from vnpy.trader.object import StatusData, # hxxjava debug
def __init__(self, gateway: CtpGateway) -> None:
"""构造函数"""
super().__init__()
self.gateway: CtpGateway = gateway
self.gateway_name: str = gateway.gateway_name
self.reqid: int = 0
self.order_ref: int = 0
self.connect_status: bool = False
self.login_status: bool = False
self.auth_status: bool = False
self.login_failed: bool = False
self.contract_inited: bool = False
self.userid: str = ""
self.password: str = ""
self.brokerid: str = ""
self.auth_code: str = ""
self.appid: str = ""
self.frontid: int = 0
self.sessionid: int = 0
self.inited = False # hxxjava add
self.status_data: List[dict] = [] # hxxjava add
self.order_data: List[dict] = []
self.trade_data: List[dict] = []
self.positions: Dict[str, PositionData] = {}
self.sysid_orderid_map: Dict[str, str] = {}
添加下面的两个函数:
def onRtnInstrumentStatus(self,data:dict):
"""
当接收到合约品种状态信息 # hxxjava debug
"""
if not self.contract_inited:
self.status_data.append(data)
return
status = self.extractInstrumentStatus(data)
self.gateway.on_status(status)
def extractInstrumentStatus(self,data:dict): # hxxjava add
""" 提取合约品种状态信息 """
return StatusData(
symbol = data["InstrumentID"],
exchange = EXCHANGE_CTP2VT[data["ExchangeID"]],
settlement_group_id = data["SettlementGroupID"],
instrument_status = INSTRUMENTSTATUS_CTP2VT[data["InstrumentStatus"]],
trading_segment_sn = data["TradingSegmentSN"],
enter_time = data["EnterTime"],
enter_reason = ENTERREASON_CTP2VT[data["EnterReason"]],
exchange_inst_id = data["ExchangeInstID"],
gateway_name=self.gateway_name
)
def onRspQryInstrument(self, data: dict, error: dict, reqid: int, last: bool) -> None:
"""合约查询回报"""
product: Product = PRODUCT_CTP2VT.get(data["ProductClass"], None)
if product:
contract: ContractData = ContractData(
symbol=data["InstrumentID"],
exchange=EXCHANGE_CTP2VT[data["ExchangeID"]],
name=data["InstrumentName"],
product=product,
size=data["VolumeMultiple"],
pricetick=data["PriceTick"],
# hxxjava add start
max_market_order_volume=data["MaxMarketOrderVolume"],
min_market_order_volume=data["MinMarketOrderVolume"],
max_limit_order_volume=data["MaxLimitOrderVolume"],
min_limit_order_volume=data["MinLimitOrderVolume"],
open_date=data["OpenDate"],
expire_date=data["ExpireDate"],
is_trading=data["IsTrading"],
long_margin_ratio=data["LongMarginRatio"],
short_margin_ratio=data["ShortMarginRatio"],
# hxxjava add end
gateway_name=self.gateway_name
)
# 期权相关
if contract.product == Product.OPTION:
# 移除郑商所期权产品名称带有的C/P后缀
if contract.exchange == Exchange.CZCE:
contract.option_portfolio = data["ProductID"][:-1]
else:
contract.option_portfolio = data["ProductID"]
contract.option_underlying = data["UnderlyingInstrID"]
contract.option_type = OPTIONTYPE_CTP2VT.get(data["OptionsType"], None)
contract.option_strike = data["StrikePrice"]
contract.option_index = str(data["StrikePrice"])
contract.option_expiry = datetime.strptime(data["ExpireDate"], "%Y%m%d")
self.gateway.on_contract(contract)
symbol_contract_map[contract.symbol] = contract
if last:
self.contract_inited = True
self.gateway.write_log("合约信息查询成功")
# self.gateway.write_log(f"收到{len(symbol_contract_map)}条合约信息")
self.gateway.write_log(f"提取{len(self.status_data)}条状态信息")
if self.status_data:
statuses = []
for data in self.status_data:
statuses.append(self.extractInstrumentStatus(data))
self.gateway.on_status_end(statuses)
self.status_data.clear()
# self.gateway.write_log(f"提取{len(self.order_data)}条委托单信息")
for data in self.order_data:
self.onRtnOrder(data)
self.order_data.clear()
# self.gateway.write_log(f"提取{len(self.trade_data)}条成交单信息")
for data in self.trade_data:
self.onRtnTrade(data)
self.trade_data.clear()
self.inited = True
修改vnpy\trader\engine.py,OmsEngine的代码如下:
class OmsEngine(BaseEngine):
"""
Provides order management system function for VN Trader.
"""
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
""""""
super(OmsEngine, self).__init__(main_engine, event_engine, "oms")
self.ticks: Dict[str, TickData] = {}
self.orders: Dict[str, OrderData] = {}
self.trades: Dict[str, TradeData] = {}
self.positions: Dict[str, PositionData] = {}
self.accounts: Dict[str, AccountData] = {}
self.contracts: Dict[str, ContractData] = {}
self.quotes: Dict[str, QuoteData] = {}
self.active_orders: Dict[str, OrderData] = {}
self.active_quotes: Dict[str, QuoteData] = {}
self.auction_ticks: Dict[str, List[TickData]] = {} # hxxjava 集合竞价tick字典,每个品种一个列表
self.statuses:Dict[str,StatusData] = {} # hxxjava add
self.add_function()
self.register_event()
def add_function(self) -> None:
"""Add query function to main engine."""
self.main_engine.get_tick = self.get_tick
self.main_engine.get_order = self.get_order
self.main_engine.get_trade = self.get_trade
self.main_engine.get_position = self.get_position
self.main_engine.get_account = self.get_account
self.main_engine.get_contract = self.get_contract
self.main_engine.get_quote = self.get_quote
self.main_engine.get_all_ticks = self.get_all_ticks
self.main_engine.get_all_orders = self.get_all_orders
self.main_engine.get_all_trades = self.get_all_trades
self.main_engine.get_all_positions = self.get_all_positions
self.main_engine.get_all_accounts = self.get_all_accounts
self.main_engine.get_all_contracts = self.get_all_contracts
self.main_engine.get_all_quotes = self.get_all_quotes
self.main_engine.get_all_active_orders = self.get_all_active_orders
self.main_engine.get_all_active_qutoes = self.get_all_active_quotes
self.main_engine.get_status = self.get_status # hxxjava add
def register_event(self) -> None:
""""""
self.event_engine.register(EVENT_TICK, self.process_tick_event)
self.event_engine.register(EVENT_ORDER, self.process_order_event)
self.event_engine.register(EVENT_TRADE, self.process_trade_event)
self.event_engine.register(EVENT_POSITION, self.process_position_event)
self.event_engine.register(EVENT_ACCOUNT, self.process_account_event)
self.event_engine.register(EVENT_CONTRACT, self.process_contract_event)
self.event_engine.register(EVENT_QUOTE, self.process_quote_event)
self.event_engine.register(EVENT_ORIGIN_TICK, self.process_origin_tick_event) # hxxjava add
self.event_engine.register(EVENT_STATUS, self.process_status_event) # hxxjava add
self.event_engine.register(EVENT_STATUS_END, self.process_status_end) # hxxjava add
def process_origin_tick_event(self, event: Event) -> None:
""""""
tick: TickData = event.data
# 得到tick合约的当前交易状态
vt_symbol = get_vt_instrument(tick.vt_symbol)
status = self.statuses.get(vt_symbol,None)
if status:
if status.instrument_status in AUCTION_STATUS:
# 收到了集合竞价时段tick
# 保存集合竞价tick到品种列表
if vt_symbol not in self.auction_ticks:
self.auction_ticks[vt_symbol] = []
self.auction_ticks[vt_symbol].append(tick)
# 发出集合竞价tick消息
self.event_engine.put(Event(EVENT_AUCTION_TICK,tick))
print(f"集合竞价状态={status} 收到tick={tick}")
elif status.instrument_status == InstrumentStatus.CONTINOUS:
# 其他时段的tick
self.event_engine.put(Event(EVENT_TICK,tick))
self.event_engine.put(Event(EVENT_TICK + tick.vt_symbol,tick))
else:
# 按说应该不存在这种情况
self.event_engine.put(Event(EVENT_TICK,tick))
self.event_engine.put(Event(EVENT_TICK + tick.vt_symbol,tick))
def process_tick_event(self, event: Event) -> None:
""""""
tick: TickData = event.data
self.ticks[tick.vt_symbol] = tick
def process_order_event(self, event: Event) -> None:
""""""
order: OrderData = event.data
self.orders[order.vt_orderid] = order
def process_trade_event(self, event: Event) -> None:
""""""
trade: TradeData = event.data
self.trades[trade.vt_tradeid] = trade
print(f"process_trade_event:{trade}")
def process_position_event(self, event: Event) -> None:
""""""
position: PositionData = event.data
self.positions[position.vt_positionid] = position
def process_account_event(self, event: Event) -> None:
""""""
account: AccountData = event.data
self.accounts[account.vt_accountid] = account
def process_contract_event(self, event: Event) -> None:
""""""
contract: ContractData = event.data
self.contracts[contract.vt_symbol] = contract
def process_status_event(self, event: Event) -> None: # hxxjava add
""" 交易状态通知消息处理 """
status:StatusData = event.data
# print(f"process_status_event {status}")
vt_symbol = status.vt_symbol
pre_status = self.statuses.get(vt_symbol,None)
self.statuses[vt_symbol] = status
if pre_status and pre_status.instrument_status in AUCTION_STATUS \
and status.instrument_status == InstrumentStatus.CONTINOUS:
# 当从状态为集合竞价状态进入连续竞价状态之时,
# 把所有集合竞价的tick时间变换为连续竞价开始,
# 然后重新把tick发送到系统的信息循环之中
ticks = self.auction_ticks.get(vt_symbol,[])
for t in ticks:
tick:TickData = copy(t)
hh,mm,ss = status.enter_time.split(':')
tick0 = copy(tick)
tick.datetime = tick.datetime.replace(hour=int(hh),minute=int(mm),second=int(ss),microsecond=0)
print(f"集合竞价{tick0} 集合竞价后{tick}")
self.event_engine.put(Event(EVENT_TICK,tick))
self.ticks.clear()
def process_status_end(self, event: Event) -> None: # hxxjava add
""" 交易状态通知消息处理 """
statuses:List[StatusData] = event.data
for status in statuses:
self.statuses[status.vt_symbol] = status
print(f"status:{status}")
def process_quote_event(self, event: Event) -> None:
""""""
quote: QuoteData = event.data
self.quotes[quote.vt_quoteid] = quote
# If quote is active, then update data in dict.
if quote.is_active():
self.active_quotes[quote.vt_quoteid] = quote
# Otherwise, pop inactive quote from in dict
elif quote.vt_quoteid in self.active_quotes:
self.active_quotes.pop(quote.vt_quoteid)
def get_tick(self, vt_symbol: str) -> Optional[TickData]:
"""
Get latest market tick data by vt_symbol.
"""
return self.ticks.get(vt_symbol, None)
def get_order(self, vt_orderid: str) -> Optional[OrderData]:
"""
Get latest order data by vt_orderid.
"""
return self.orders.get(vt_orderid, None)
def get_trade(self, vt_tradeid: str) -> Optional[TradeData]:
"""
Get trade data by vt_tradeid.
"""
return self.trades.get(vt_tradeid, None)
def get_position(self, vt_positionid: str) -> Optional[PositionData]:
"""
Get latest position data by vt_positionid.
"""
return self.positions.get(vt_positionid, None)
def get_account(self, vt_accountid: str) -> Optional[AccountData]:
"""
Get latest account data by vt_accountid.
"""
return self.accounts.get(vt_accountid, None)
def get_contract(self, vt_symbol: str) -> Optional[ContractData]:
"""
Get contract data by vt_symbol.
"""
return self.contracts.get(vt_symbol, None)
def get_quote(self, vt_quoteid: str) -> Optional[QuoteData]:
"""
Get latest quote data by vt_orderid.
"""
return self.quotes.get(vt_quoteid, None)
def get_all_ticks(self) -> List[TickData]:
"""
Get all tick data.
"""
return list(self.ticks.values())
def get_all_orders(self) -> List[OrderData]:
"""
Get all order data.
"""
return list(self.orders.values())
def get_all_trades(self) -> List[TradeData]:
"""
Get all trade data.
"""
return list(self.trades.values())
def get_all_positions(self) -> List[PositionData]:
"""
Get all position data.
"""
return list(self.positions.values())
def get_all_accounts(self) -> List[AccountData]:
"""
Get all account data.
"""
return list(self.accounts.values())
def get_all_contracts(self) -> List[ContractData]:
"""
Get all contract data.
"""
return list(self.contracts.values())
def get_all_quotes(self) -> List[QuoteData]:
"""
Get all quote data.
"""
return list(self.quotes.values())
def get_status(self,vt_symbol:str) -> Optional[StatusData]: # hxxjava add
"""
Get the vt_symbol's status data.
"""
return self.statuses.get(vt_symbol,None)
def get_all_statuses(self) -> List[StatusData]: # hxxjava add
"""
Get the vt_symbol's status data.
"""
return self.statuses.values()
def get_all_active_orders(self, vt_symbol: str = "") -> List[OrderData]:
"""
Get all active orders by vt_symbol.
If vt_symbol is empty, return all active orders.
"""
if not vt_symbol:
return list(self.active_orders.values())
else:
active_orders = [
order
for order in self.active_orders.values()
if order.vt_symbol == vt_symbol
]
return active_orders
def get_all_active_quotes(self, vt_symbol: str = "") -> List[QuoteData]:
"""
Get all active quotes by vt_symbol.
If vt_symbol is empty, return all active qutoes.
"""
if not vt_symbol:
return list(self.active_quotes.values())
else:
active_quotes = [
quote
for quote in self.active_quotes.values()
if quote.vt_symbol == vt_symbol
]
return active_quotes
修改vnpy\trader\ui\widget.py,内容如下:
"""
Basic widgets for VN Trader.
"""
import csv
import platform
from enum import Enum
from typing import Any, Dict
from copy import copy
from tzlocal import get_localzone
from PyQt5 import QtCore, QtGui, QtWidgets, Qt
import importlib_metadata
import vnpy
from vnpy.event import Event, EventEngine
from ..constant import Direction, Exchange, Offset, OrderType
from ..engine import MainEngine
from ..event import (
EVENT_QUOTE,
EVENT_AUCTION_TICK,
EVENT_TICK,
EVENT_TRADE,
EVENT_ORDER,
EVENT_POSITION,
EVENT_ACCOUNT,
EVENT_STRATEGY_ACCOUNT, # hxxjava
EVENT_LOG
)
from ..object import OrderRequest, SubscribeRequest, PositionData
from ..utility import load_json, save_json, get_digits
from ..setting import SETTING_FILENAME, SETTINGS
COLOR_LONG = QtGui.QColor("red")
COLOR_SHORT = QtGui.QColor("green")
COLOR_BID = QtGui.QColor(255, 174, 201)
COLOR_ASK = QtGui.QColor(160, 255, 160)
COLOR_BLACK = QtGui.QColor("black")
class BaseCell(QtWidgets.QTableWidgetItem):
"""
General cell used in tablewidgets.
"""
def __init__(self, content: Any, data: Any):
""""""
super(BaseCell, self).__init__()
self.setTextAlignment(QtCore.Qt.AlignCenter)
self.set_content(content, data)
def set_content(self, content: Any, data: Any) -> None:
"""
Set text content.
"""
self.setText(str(content))
self._data = data
def get_data(self) -> Any:
"""
Get data object.
"""
return self._data
class EnumCell(BaseCell):
"""
Cell used for showing enum data.
"""
def __init__(self, content: str, data: Any):
""""""
super(EnumCell, self).__init__(content, data)
def set_content(self, content: Any, data: Any) -> None:
"""
Set text using enum.constant.value.
"""
if content:
super(EnumCell, self).set_content(content.value, data)
class DirectionCell(EnumCell):
"""
Cell used for showing direction data.
"""
def __init__(self, content: str, data: Any):
""""""
super(DirectionCell, self).__init__(content, data)
def set_content(self, content: Any, data: Any) -> None:
"""
Cell color is set according to direction.
"""
super(DirectionCell, self).set_content(content, data)
if content is Direction.SHORT:
self.setForeground(COLOR_SHORT)
else:
self.setForeground(COLOR_LONG)
class BidCell(BaseCell):
"""
Cell used for showing bid price and volume.
"""
def __init__(self, content: Any, data: Any):
""""""
super(BidCell, self).__init__(content, data)
self.setForeground(COLOR_BID)
class AskCell(BaseCell):
"""
Cell used for showing ask price and volume.
"""
def __init__(self, content: Any, data: Any):
""""""
super(AskCell, self).__init__(content, data)
self.setForeground(COLOR_ASK)
class PnlCell(BaseCell):
"""
Cell used for showing pnl data.
"""
def __init__(self, content: Any, data: Any):
""""""
super(PnlCell, self).__init__(content, data)
def set_content(self, content: Any, data: Any) -> None:
"""
Cell color is set based on whether pnl is
positive or negative.
"""
super(PnlCell, self).set_content(content, data)
if str(content).startswith("-"):
self.setForeground(COLOR_SHORT)
else:
self.setForeground(COLOR_LONG)
class DateTimeCell(BaseCell):
"""
Cell used for showing time string from datetime object.
"""
local_tz = get_localzone()
def __init__(self, content: Any, data: Any):
""""""
super(DateTimeCell, self).__init__(content, data)
def set_content(self, content: Any, data: Any) -> None:
""""""
if content is None:
return
content = content.astimezone(self.local_tz)
timestamp = content.strftime("%Y-%m-%d %H:%M:%S")
millisecond = int(content.microsecond / 1000)
if millisecond:
timestamp = f"{timestamp}.{millisecond}"
self.setText(timestamp)
self._data = data
class TimeCell(BaseCell):
"""
Cell used for showing time string from datetime object.
"""
local_tz = get_localzone()
def __init__(self, content: Any, data: Any):
""""""
super(TimeCell, self).__init__(content, data)
def set_content(self, content: Any, data: Any) -> None:
""""""
if content is None:
return
content = content.astimezone(self.local_tz)
timestamp = content.strftime("%H:%M:%S")
millisecond = int(content.microsecond / 1000)
if millisecond:
timestamp = f"{timestamp}.{millisecond}"
else:
timestamp = f"{timestamp}.000"
self.setText(timestamp)
self._data = data
class MsgCell(BaseCell):
"""
Cell used for showing msg data.
"""
def __init__(self, content: str, data: Any):
""""""
super(MsgCell, self).__init__(content, data)
self.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
class BaseMonitor(QtWidgets.QTableWidget):
"""
Monitor data update in VN Trader.
"""
event_type: str = ""
data_key: str = ""
sorting: bool = False
headers: Dict[str, dict] = {}
signal: QtCore.pyqtSignal = QtCore.pyqtSignal(Event)
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
""""""
super(BaseMonitor, self).__init__()
self.main_engine: MainEngine = main_engine
self.event_engine: EventEngine = event_engine
self.cells: Dict[str, dict] = {}
self.init_ui()
self.load_setting()
self.register_event()
def __del__(self) -> None:
""""""
self.save_setting()
def init_ui(self) -> None:
""""""
self.init_table()
self.init_menu()
def init_table(self) -> None:
"""
Initialize table.
"""
self.setColumnCount(len(self.headers))
labels = [d["display"] for d in self.headers.values()]
self.setHorizontalHeaderLabels(labels)
self.verticalHeader().setVisible(False)
self.setEditTriggers(self.NoEditTriggers)
self.setAlternatingRowColors(True)
self.setSortingEnabled(self.sorting)
def init_menu(self) -> None:
"""
Create right click menu.
"""
self.menu = QtWidgets.QMenu(self)
resize_action = QtWidgets.QAction("调整列宽", self)
resize_action.triggered.connect(self.resize_columns)
self.menu.addAction(resize_action)
save_action = QtWidgets.QAction("保存数据", self)
save_action.triggered.connect(self.save_csv)
self.menu.addAction(save_action)
def register_event(self) -> None:
"""
Register event handler into event engine.
"""
if self.event_type:
self.signal.connect(self.process_event)
if type(self.event_type) == list: # hxxjava debug
for ev in self.event_type:
self.event_engine.register(ev, self.signal.emit)
# print(f"已经订阅 {ev} 消息")
else:
self.event_engine.register(self.event_type, self.signal.emit)
def process_event(self, event: Event) -> None:
"""
Process new data from event and update into table.
"""
# Disable sorting to prevent unwanted error.
if self.sorting:
self.setSortingEnabled(False)
# Update data into table.
data = event.data
if not self.data_key:
self.insert_new_row(data)
else:
key = data.__getattribute__(self.data_key)
if key in self.cells:
self.update_old_row(data)
else:
self.insert_new_row(data)
# Enable sorting
if self.sorting:
self.setSortingEnabled(True)
def insert_new_row(self, data: Any):
"""
Insert a new row at the top of table.
"""
self.insertRow(0)
row_cells = {}
for column, header in enumerate(self.headers.keys()):
setting = self.headers[header]
content = data.__getattribute__(header)
cell = setting["cell"](content, data)
self.setItem(0, column, cell)
if setting["update"]:
row_cells[header] = cell
if self.data_key:
key = data.__getattribute__(self.data_key)
self.cells[key] = row_cells
def update_old_row(self, data: Any) -> None:
"""
Update an old row in table.
"""
key = data.__getattribute__(self.data_key)
row_cells = self.cells[key]
for header, cell in row_cells.items():
content = data.__getattribute__(header)
cell.set_content(content, data)
def resize_columns(self) -> None:
"""
Resize all columns according to contents.
"""
self.horizontalHeader().resizeSections(QtWidgets.QHeaderView.ResizeToContents)
def save_csv(self) -> None:
"""
Save table data into a csv file
"""
path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "保存数据", "", "CSV(*.csv)")
if not path:
return
with open(path, "w") as f:
writer = csv.writer(f, lineterminator="\n")
headers = [d["display"] for d in self.headers.values()]
writer.writerow(headers)
for row in range(self.rowCount()):
if self.isRowHidden(row):
continue
row_data = []
for column in range(self.columnCount()):
item = self.item(row, column)
if item:
row_data.append(str(item.text()))
else:
row_data.append("")
writer.writerow(row_data)
def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None:
"""
Show menu with right click.
"""
self.menu.popup(QtGui.QCursor.pos())
def save_setting(self) -> None:
""""""
settings = QtCore.QSettings(self.__class__.__name__, "custom")
settings.setValue("column_state", self.horizontalHeader().saveState())
def load_setting(self) -> None:
""""""
settings = QtCore.QSettings(self.__class__.__name__, "custom")
column_state = settings.value("column_state")
if isinstance(column_state, QtCore.QByteArray):
self.horizontalHeader().restoreState(column_state)
self.horizontalHeader().setSortIndicator(-1, QtCore.Qt.AscendingOrder)
class TickMonitor(BaseMonitor):
"""
Monitor for tick data.
"""
# event_type = EVENT_TICK
event_type = [EVENT_TICK,EVENT_AUCTION_TICK]
data_key = "vt_symbol"
sorting = True
headers = {
"symbol": {"display": "代码", "cell": BaseCell, "update": False},
"exchange": {"display": "交易所", "cell": EnumCell, "update": False},
"name": {"display": "名称", "cell": BaseCell, "update": True},
"last_price": {"display": "最新价", "cell": BaseCell, "update": True},
"volume": {"display": "成交量", "cell": BaseCell, "update": True},
"open_price": {"display": "开盘价", "cell": BaseCell, "update": True},
"high_price": {"display": "最高价", "cell": BaseCell, "update": True},
"low_price": {"display": "最低价", "cell": BaseCell, "update": True},
"bid_price_1": {"display": "买1价", "cell": BidCell, "update": True},
"bid_volume_1": {"display": "买1量", "cell": BidCell, "update": True},
"ask_price_1": {"display": "卖1价", "cell": AskCell, "update": True},
"ask_volume_1": {"display": "卖1量", "cell": AskCell, "update": True},
"datetime": {"display": "时间", "cell": TimeCell, "update": True},
"gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
}
class LogMonitor(BaseMonitor):
"""
Monitor for log data.
"""
event_type = EVENT_LOG
data_key = ""
sorting = False
headers = {
"time": {"display": "时间", "cell": TimeCell, "update": False},
"msg": {"display": "信息", "cell": MsgCell, "update": False},
"gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
}
class TradeMonitor(BaseMonitor):
"""
Monitor for trade data.
"""
event_type = EVENT_TRADE
data_key = "tradeid" # hxxjava chanage
sorting = True
headers: Dict[str, dict] = {
"tradeid": {"display": "成交号 ", "cell": BaseCell, "update": False},
"orderid": {"display": "委托号", "cell": BaseCell, "update": False},
"symbol": {"display": "代码", "cell": BaseCell, "update": False},
"exchange": {"display": "交易所", "cell": EnumCell, "update": False},
"direction": {"display": "方向", "cell": DirectionCell, "update": False},
"offset": {"display": "开平", "cell": EnumCell, "update": False},
"price": {"display": "价格", "cell": BaseCell, "update": False},
"volume": {"display": "数量", "cell": BaseCell, "update": False},
"datetime": {"display": "时间", "cell": DateTimeCell, "update": False},
"gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
# "reference": {"display": "策略", "cell": BaseCell, "update": False},
}
class OrderMonitor(BaseMonitor):
"""
Monitor for order data.
"""
event_type = EVENT_ORDER
data_key = "vt_orderid"
sorting = True
headers: Dict[str, dict] = {
"orderid": {"display": "委托号", "cell": BaseCell, "update": False},
"reference": {"display": "来源", "cell": BaseCell, "update": False},
"symbol": {"display": "代码", "cell": BaseCell, "update": False},
"exchange": {"display": "交易所", "cell": EnumCell, "update": False},
"type": {"display": "类型", "cell": EnumCell, "update": False},
"direction": {"display": "方向", "cell": DirectionCell, "update": False},
"offset": {"display": "开平", "cell": EnumCell, "update": False},
"price": {"display": "价格", "cell": BaseCell, "update": False},
"volume": {"display": "总数量", "cell": BaseCell, "update": True},
"traded": {"display": "已成交", "cell": BaseCell, "update": True},
"status": {"display": "状态", "cell": EnumCell, "update": True},
"datetime": {"display": "时间", "cell": DateTimeCell, "update": True},
"gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
}
def init_ui(self):
"""
Connect signal.
"""
super(OrderMonitor, self).init_ui()
self.setToolTip("双击单元格撤单")
self.itemDoubleClicked.connect(self.cancel_order)
def cancel_order(self, cell: BaseCell) -> None:
"""
Cancel order if cell double clicked.
"""
order = cell.get_data()
req = order.create_cancel_request()
self.main_engine.cancel_order(req, order.gateway_name)
class PositionMonitor(BaseMonitor):
"""
Monitor for position data.
"""
event_type = EVENT_POSITION
data_key = "vt_positionid"
sorting = True
headers = {
"symbol": {"display": "代码", "cell": BaseCell, "update": False},
"exchange": {"display": "交易所", "cell": EnumCell, "update": False},
"direction": {"display": "方向", "cell": DirectionCell, "update": False},
"volume": {"display": "数量", "cell": BaseCell, "update": True},
"yd_volume": {"display": "昨仓", "cell": BaseCell, "update": True},
"frozen": {"display": "冻结", "cell": BaseCell, "update": True},
"price": {"display": "均价", "cell": BaseCell, "update": True},
"pnl": {"display": "盈亏", "cell": PnlCell, "update": True},
"gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
}
class AccountMonitor(BaseMonitor):
"""
Monitor for account data.
"""
event_type = EVENT_ACCOUNT
data_key = "vt_accountid"
sorting = True
headers = {
"accountid": {"display": "账号", "cell": BaseCell, "update": False},
"balance": {"display": "余额", "cell": BaseCell, "update": True},
"frozen": {"display": "冻结", "cell": BaseCell, "update": True},
"available": {"display": "可用", "cell": BaseCell, "update": True},
"gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
}
class QuoteMonitor(BaseMonitor):
"""
Monitor for quote data.
"""
event_type = EVENT_QUOTE
data_key = "vt_quoteid"
sorting = True
headers: Dict[str, dict] = {
"quoteid": {"display": "报价号", "cell": BaseCell, "update": False},
"reference": {"display": "来源", "cell": BaseCell, "update": False},
"symbol": {"display": "代码", "cell": BaseCell, "update": False},
"exchange": {"display": "交易所", "cell": EnumCell, "update": False},
"bid_offset": {"display": "买开平", "cell": EnumCell, "update": False},
"bid_volume": {"display": "买量", "cell": BidCell, "update": False},
"bid_price": {"display": "买价", "cell": BidCell, "update": False},
"ask_price": {"display": "卖价", "cell": AskCell, "update": False},
"ask_volume": {"display": "卖量", "cell": AskCell, "update": False},
"ask_offset": {"display": "卖开平", "cell": EnumCell, "update": False},
"status": {"display": "状态", "cell": EnumCell, "update": True},
"datetime": {"display": "时间", "cell": TimeCell, "update": True},
"gateway_name": {"display": "接口", "cell": BaseCell, "update": False},
}
def init_ui(self):
"""
Connect signal.
"""
super().init_ui()
self.setToolTip("双击单元格撤销报价")
self.itemDoubleClicked.connect(self.cancel_quote)
def cancel_quote(self, cell: BaseCell) -> None:
"""
Cancel quote if cell double clicked.
"""
quote = cell.get_data()
req = quote.create_cancel_request()
self.main_engine.cancel_quote(req, quote.gateway_name)
class StrategyAccountMonitor(BaseMonitor): # hxxjava add
"""
Monitor for account data.
"""
event_type = EVENT_STRATEGY_ACCOUNT
data_key = "strategy_name"
sorting = True
headers = {
"strategy_name": {"display": "策略", "cell": BaseCell, "update": False},
"capital": {"display": "本金", "cell": BaseCell, "update": True},
"money": {"display": "权益", "cell": BaseCell, "update": True},
"margin": {"display": "保证金", "cell": BaseCell, "update": True},
"available": {"display": "可用资金", "cell": BaseCell, "update": True},
"commission": {"display": "手续费", "cell": BaseCell, "update": True},
}
class ConnectDialog(QtWidgets.QDialog):
"""
Start connection of a certain gateway.
"""
def __init__(self, main_engine: MainEngine, gateway_name: str):
""""""
super().__init__()
self.main_engine: MainEngine = main_engine
self.gateway_name: str = gateway_name
self.filename: str = f"connect_{gateway_name.lower()}.json"
self.widgets: Dict[str, QtWidgets.QWidget] = {}
self.init_ui()
def init_ui(self) -> None:
""""""
self.setWindowTitle(f"连接{self.gateway_name}")
# Default setting provides field name, field data type and field default value.
default_setting = self.main_engine.get_default_setting(
self.gateway_name)
# Saved setting provides field data used last time.
loaded_setting = load_json(self.filename)
# Initialize line edits and form layout based on setting.
form = QtWidgets.QFormLayout()
for field_name, field_value in default_setting.items():
field_type = type(field_value)
if field_type == list:
widget = QtWidgets.QComboBox()
widget.addItems(field_value)
if field_name in loaded_setting:
saved_value = loaded_setting[field_name]
ix = widget.findText(saved_value)
widget.setCurrentIndex(ix)
else:
widget = QtWidgets.QLineEdit(str(field_value))
if field_name in loaded_setting:
saved_value = loaded_setting[field_name]
widget.setText(str(saved_value))
if "密码" in field_name:
widget.setEchoMode(QtWidgets.QLineEdit.Password)
if field_type == int:
validator = QtGui.QIntValidator()
widget.setValidator(validator)
form.addRow(f"{field_name} <{field_type.__name__}>", widget)
self.widgets[field_name] = (widget, field_type)
button = QtWidgets.QPushButton("连接")
button.clicked.connect(self.connect)
form.addRow(button)
self.setLayout(form)
def connect(self) -> None:
"""
Get setting value from line edits and connect the gateway.
"""
setting = {}
for field_name, tp in self.widgets.items():
widget, field_type = tp
if field_type == list:
field_value = str(widget.currentText())
else:
try:
field_value = field_type(widget.text())
except ValueError:
field_value = field_type()
setting[field_name] = field_value
save_json(self.filename, setting)
self.main_engine.connect(setting, self.gateway_name)
self.accept()
class TradingWidget(QtWidgets.QWidget):
"""
General manual trading widget.
"""
signal_tick = QtCore.pyqtSignal(Event)
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
""""""
super().__init__()
self.main_engine: MainEngine = main_engine
self.event_engine: EventEngine = event_engine
self.vt_symbol: str = ""
self.price_digits: int = 0
self.init_ui()
self.register_event()
def init_ui(self) -> None:
""""""
self.setFixedWidth(300)
# Trading function area
exchanges = self.main_engine.get_all_exchanges()
self.exchange_combo = QtWidgets.QComboBox()
self.exchange_combo.addItems([exchange.value for exchange in exchanges])
self.symbol_line = QtWidgets.QLineEdit()
self.symbol_line.returnPressed.connect(self.set_vt_symbol)
self.name_line = QtWidgets.QLineEdit()
self.name_line.setReadOnly(True)
self.direction_combo = QtWidgets.QComboBox()
self.direction_combo.addItems(
[Direction.LONG.value, Direction.SHORT.value])
self.offset_combo = QtWidgets.QComboBox()
self.offset_combo.addItems([offset.value for offset in Offset])
self.order_type_combo = QtWidgets.QComboBox()
self.order_type_combo.addItems(
[order_type.value for order_type in OrderType])
double_validator = QtGui.QDoubleValidator()
double_validator.setBottom(0)
self.price_line = QtWidgets.QLineEdit()
self.price_line.setValidator(double_validator)
self.volume_line = QtWidgets.QLineEdit()
self.volume_line.setValidator(double_validator)
self.gateway_combo = QtWidgets.QComboBox()
self.gateway_combo.addItems(self.main_engine.get_all_gateway_names())
self.price_check = QtWidgets.QCheckBox()
self.price_check.setToolTip("设置价格随行情更新")
send_button = QtWidgets.QPushButton("委托")
send_button.clicked.connect(self.send_order)
cancel_button = QtWidgets.QPushButton("全撤")
cancel_button.clicked.connect(self.cancel_all)
grid = QtWidgets.QGridLayout()
grid.addWidget(QtWidgets.QLabel("交易所"), 0, 0)
grid.addWidget(QtWidgets.QLabel("代码"), 1, 0)
grid.addWidget(QtWidgets.QLabel("名称"), 2, 0)
grid.addWidget(QtWidgets.QLabel("方向"), 3, 0)
grid.addWidget(QtWidgets.QLabel("开平"), 4, 0)
grid.addWidget(QtWidgets.QLabel("类型"), 5, 0)
grid.addWidget(QtWidgets.QLabel("价格"), 6, 0)
grid.addWidget(QtWidgets.QLabel("数量"), 7, 0)
grid.addWidget(QtWidgets.QLabel("接口"), 8, 0)
grid.addWidget(self.exchange_combo, 0, 1, 1, 2)
grid.addWidget(self.symbol_line, 1, 1, 1, 2)
grid.addWidget(self.name_line, 2, 1, 1, 2)
grid.addWidget(self.direction_combo, 3, 1, 1, 2)
grid.addWidget(self.offset_combo, 4, 1, 1, 2)
grid.addWidget(self.order_type_combo, 5, 1, 1, 2)
grid.addWidget(self.price_line, 6, 1, 1, 1)
grid.addWidget(self.price_check, 6, 2, 1, 1)
grid.addWidget(self.volume_line, 7, 1, 1, 2)
grid.addWidget(self.gateway_combo, 8, 1, 1, 2)
grid.addWidget(send_button, 9, 0, 1, 3)
grid.addWidget(cancel_button, 10, 0, 1, 3)
# Market depth display area
bid_color = "rgb(255,174,201)"
ask_color = "rgb(160,255,160)"
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.lp_label = self.create_label()
self.return_label = self.create_label(alignment=QtCore.Qt.AlignRight)
form = QtWidgets.QFormLayout()
form.addRow(self.ap5_label, self.av5_label)
form.addRow(self.ap4_label, self.av4_label)
form.addRow(self.ap3_label, self.av3_label)
form.addRow(self.ap2_label, self.av2_label)
form.addRow(self.ap1_label, self.av1_label)
form.addRow(self.lp_label, self.return_label)
form.addRow(self.bp1_label, self.bv1_label)
form.addRow(self.bp2_label, self.bv2_label)
form.addRow(self.bp3_label, self.bv3_label)
form.addRow(self.bp4_label, self.bv4_label)
form.addRow(self.bp5_label, self.bv5_label)
# Overall layout
vbox = QtWidgets.QVBoxLayout()
vbox.addLayout(grid)
vbox.addLayout(form)
self.setLayout(vbox)
def create_label(
self,
color: str = "",
alignment: int = QtCore.Qt.AlignLeft
) -> QtWidgets.QLabel:
"""
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) -> None:
""""""
self.signal_tick.connect(self.process_tick_event)
self.event_engine.register(EVENT_TICK, self.signal_tick.emit)
def process_tick_event(self, event: Event) -> None:
""""""
tick = event.data
if tick.vt_symbol != self.vt_symbol:
return
price_digits = self.price_digits
self.lp_label.setText(f"{tick.last_price:.{price_digits}f}")
self.bp1_label.setText(f"{tick.bid_price_1:.{price_digits}f}")
self.bv1_label.setText(str(tick.bid_volume_1))
self.ap1_label.setText(f"{tick.ask_price_1:.{price_digits}f}")
self.av1_label.setText(str(tick.ask_volume_1))
if tick.pre_close:
r = (tick.last_price / tick.pre_close - 1) * 100
self.return_label.setText(f"{r:.2f}%")
if tick.bid_price_2:
self.bp2_label.setText(f"{tick.bid_price_2:.{price_digits}f}")
self.bv2_label.setText(str(tick.bid_volume_2))
self.ap2_label.setText(f"{tick.ask_price_2:.{price_digits}f}")
self.av2_label.setText(str(tick.ask_volume_2))
self.bp3_label.setText(f"{tick.bid_price_3:.{price_digits}f}")
self.bv3_label.setText(str(tick.bid_volume_3))
self.ap3_label.setText(f"{tick.ask_price_3:.{price_digits}f}")
self.av3_label.setText(str(tick.ask_volume_3))
self.bp4_label.setText(f"{tick.bid_price_4:.{price_digits}f}")
self.bv4_label.setText(str(tick.bid_volume_4))
self.ap4_label.setText(f"{tick.ask_price_4:.{price_digits}f}")
self.av4_label.setText(str(tick.ask_volume_4))
self.bp5_label.setText(f"{tick.bid_price_5:.{price_digits}f}")
self.bv5_label.setText(str(tick.bid_volume_5))
self.ap5_label.setText(f"{tick.ask_price_5:.{price_digits}f}")
self.av5_label.setText(str(tick.ask_volume_5))
if self.price_check.isChecked():
self.price_line.setText(f"{tick.last_price:.{price_digits}f}")
def set_vt_symbol(self) -> None:
"""
Set the tick depth data to monitor by vt_symbol.
"""
symbol = str(self.symbol_line.text())
if not symbol:
return
# Generate vt_symbol from symbol and exchange
exchange_value = str(self.exchange_combo.currentText())
vt_symbol = f"{symbol}.{exchange_value}"
if vt_symbol == self.vt_symbol:
return
self.vt_symbol = vt_symbol
# Update name line widget and clear all labels
contract = self.main_engine.get_contract(vt_symbol)
if not contract:
self.name_line.setText("")
gateway_name = self.gateway_combo.currentText()
else:
self.name_line.setText(contract.name)
gateway_name = contract.gateway_name
# Update gateway combo box.
ix = self.gateway_combo.findText(gateway_name)
self.gateway_combo.setCurrentIndex(ix)
# Update price digits
self.price_digits = get_digits(contract.pricetick)
self.clear_label_text()
self.volume_line.setText("")
self.price_line.setText("")
# Subscribe tick data
req = SubscribeRequest(
symbol=symbol, exchange=Exchange(exchange_value)
)
self.main_engine.subscribe(req, gateway_name)
def clear_label_text(self) -> None:
"""
Clear text on all labels.
"""
self.lp_label.setText("")
self.return_label.setText("")
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("")
def send_order(self) -> None:
"""
Send new order manually.
"""
symbol = str(self.symbol_line.text())
if not symbol:
QtWidgets.QMessageBox.critical(self, "委托失败", "请输入合约代码")
return
volume_text = str(self.volume_line.text())
if not volume_text:
QtWidgets.QMessageBox.critical(self, "委托失败", "请输入委托数量")
return
volume = float(volume_text)
price_text = str(self.price_line.text())
if not price_text:
price = 0
else:
price = float(price_text)
req = OrderRequest(
symbol=symbol,
exchange=Exchange(str(self.exchange_combo.currentText())),
direction=Direction(str(self.direction_combo.currentText())),
type=OrderType(str(self.order_type_combo.currentText())),
volume=volume,
price=price,
offset=Offset(str(self.offset_combo.currentText())),
reference="ManualTrading"
)
gateway_name = str(self.gateway_combo.currentText())
self.main_engine.send_order(req, gateway_name)
def cancel_all(self) -> None:
"""
Cancel all active orders.
"""
order_list = self.main_engine.get_all_active_orders()
for order in order_list:
req = order.create_cancel_request()
self.main_engine.cancel_order(req, order.gateway_name)
def update_with_cell(self, cell: BaseCell) -> None:
""""""
data = cell.get_data()
self.symbol_line.setText(data.symbol)
self.exchange_combo.setCurrentIndex(
self.exchange_combo.findText(data.exchange.value)
)
self.set_vt_symbol()
if isinstance(data, PositionData):
if data.direction == Direction.SHORT:
direction = Direction.LONG
elif data.direction == Direction.LONG:
direction = Direction.SHORT
else: # Net position mode
if data.volume > 0:
direction = Direction.SHORT
else:
direction = Direction.LONG
self.direction_combo.setCurrentIndex(
self.direction_combo.findText(direction.value)
)
self.offset_combo.setCurrentIndex(
self.offset_combo.findText(Offset.CLOSE.value)
)
self.volume_line.setText(str(abs(data.volume)))
class ActiveOrderMonitor(OrderMonitor):
"""
Monitor which shows active order only.
"""
def process_event(self, event) -> None:
"""
Hides the row if order is not active.
"""
super(ActiveOrderMonitor, self).process_event(event)
order = event.data
row_cells = self.cells[order.vt_orderid]
row = self.row(row_cells["volume"])
if order.is_active():
self.showRow(row)
else:
self.hideRow(row)
class ContractManager(QtWidgets.QWidget):
"""
Query contract data available to trade in system.
"""
headers: Dict[str, str] = {
"vt_symbol": "本地代码",
"symbol": "代码",
"exchange": "交易所",
"name": "名称",
"product": "合约分类",
"size": "合约乘数",
"pricetick": "价格跳动",
"min_volume": "最小委托量",
"gateway_name": "交易接口",
}
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
super().__init__()
self.main_engine: MainEngine = main_engine
self.event_engine: EventEngine = event_engine
self.init_ui()
def init_ui(self) -> None:
""""""
self.setWindowTitle("合约查询")
self.resize(1000, 600)
self.filter_line = QtWidgets.QLineEdit()
self.filter_line.setPlaceholderText("输入合约代码或者交易所,留空则查询所有合约")
self.button_show = QtWidgets.QPushButton("查询")
self.button_show.clicked.connect(self.show_contracts)
labels = []
for name, display in self.headers.items():
label = f"{display}\n{name}"
labels.append(label)
self.contract_table = QtWidgets.QTableWidget()
self.contract_table.setColumnCount(len(self.headers))
self.contract_table.setHorizontalHeaderLabels(labels)
self.contract_table.verticalHeader().setVisible(False)
self.contract_table.setEditTriggers(self.contract_table.NoEditTriggers)
self.contract_table.setAlternatingRowColors(True)
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(self.filter_line)
hbox.addWidget(self.button_show)
vbox = QtWidgets.QVBoxLayout()
vbox.addLayout(hbox)
vbox.addWidget(self.contract_table)
self.setLayout(vbox)
def show_contracts(self) -> None:
"""
Show contracts by symbol
"""
flt = str(self.filter_line.text())
all_contracts = self.main_engine.get_all_contracts()
if flt:
contracts = [
contract for contract in all_contracts if flt in contract.vt_symbol
]
else:
contracts = all_contracts
self.contract_table.clearContents()
self.contract_table.setRowCount(len(contracts))
for row, contract in enumerate(contracts):
for column, name in enumerate(self.headers.keys()):
value = getattr(contract, name)
if isinstance(value, Enum):
cell = EnumCell(value, contract)
else:
cell = BaseCell(value, contract)
self.contract_table.setItem(row, column, cell)
self.contract_table.resizeColumnsToContents()
class AboutDialog(QtWidgets.QDialog):
"""
About VN Trader.
"""
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
""""""
super().__init__()
self.main_engine: MainEngine = main_engine
self.event_engine: EventEngine = event_engine
self.init_ui()
def init_ui(self) -> None:
""""""
self.setWindowTitle("关于VN Trader")
text = f"""
By Traders, For Traders.
License:MIT
Website:www.vnpy.com
Github:www.github.com/vnpy/vnpy
vn.py - {vnpy.__version__}
Python - {platform.python_version()}
PyQt5 - {Qt.PYQT_VERSION_STR}
NumPy - {importlib_metadata.version("numpy")}
pandas - {importlib_metadata.version("pandas")}
RQData - {importlib_metadata.version("rqdatac")}
"""
label = QtWidgets.QLabel()
label.setText(text)
label.setMinimumWidth(500)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(label)
self.setLayout(vbox)
class GlobalDialog(QtWidgets.QDialog):
"""
Start connection of a certain gateway.
"""
def __init__(self):
""""""
super().__init__()
self.widgets: Dict[str, Any] = {}
self.init_ui()
def init_ui(self) -> None:
""""""
self.setWindowTitle("全局配置")
self.setMinimumWidth(800)
settings = copy(SETTINGS)
settings.update(load_json(SETTING_FILENAME))
# Initialize line edits and form layout based on setting.
form = QtWidgets.QFormLayout()
for field_name, field_value in settings.items():
field_type = type(field_value)
widget = QtWidgets.QLineEdit(str(field_value))
form.addRow(f"{field_name} <{field_type.__name__}>", widget)
self.widgets[field_name] = (widget, field_type)
button = QtWidgets.QPushButton("确定")
button.clicked.connect(self.update_setting)
form.addRow(button)
scroll_widget = QtWidgets.QWidget()
scroll_widget.setLayout(form)
scroll_area = QtWidgets.QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setWidget(scroll_widget)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(scroll_area)
self.setLayout(vbox)
def update_setting(self) -> None:
"""
Get setting value from line edits and update global setting file.
"""
settings = {}
for field_name, tp in self.widgets.items():
widget, field_type = tp
value_text = widget.text()
if field_type == bool:
if value_text == "True":
field_value = True
else:
field_value = False
else:
field_value = field_type(value_text)
settings[field_name] = field_value
QtWidgets.QMessageBox.information(
self,
"注意",
"全局配置的修改需要重启VN Trader后才会生效!",
QtWidgets.QMessageBox.Ok
)
save_json(SETTING_FILENAME, settings)
self.accept()
未完待续 ... ...
期货交易结算主要有两种方式:
1、平仓盈亏(逐笔对冲)=开仓价与平仓价之差×手数×交易单位
2、浮动盈亏(持仓盈亏)=当日结算价与开仓价之差×手数×交易单位
3、当日结存(逐笔对冲)=上日结存(逐笔对冲)+ 当日存取合计 + 平仓盈亏 (逐笔对冲)-当日手续费
4、客户权益(逐笔对冲)=当日结存(逐笔对冲)+ 浮动盈亏(持仓盈亏)
1、平仓盈亏(逐日盯市)=平当日仓盈亏+平历史仓盈亏
(1)平当日仓盈亏=当日开仓价与平仓价之差×手数×交易单位
(2)平历史仓盈亏=平仓价与昨日结算价之差×手数×交易单位
2、持仓盈亏(逐日盯市)=持当日仓盈亏+持历史仓盈亏
(1)持当日仓盈亏=当日结算价与当日开仓价之差×手数×交易单位
(2)持历史仓盈亏=当日结算价与昨日结算价之差×手数×交易单位
1、逐日盯市每天都对客户持有的合约进行盈亏计算,并且按照风控水平的设置判断客户账户的风险,如果出现爆仓或者穿仓,实时对用户进行风险通知或者风险处置。
2、因此每天接口只对当前交易日的历史委托单、成交单、当前持仓和账户信息进行推送,如果当前交易日的没有委托、成交操作,那么当前交易日的历史委托单、成交单记录没有任何内容推送。即使有当前持仓,当前交易日之前的历史委托单、成交单也不会被推送给客户端。
3、当前vnpy就是按照大部分接口都支持结算方式,但单逐日盯市更适合交易所和期货公司使用,而非交易者。
1、一笔交易是自从客户对某个合约建仓开始,到该合约的持仓量为零结束;
2、逐笔对冲的成本价会因为加减仓而发生变化;
3、逐笔对冲结算需要记录该笔交易过程中的所有成交单,才能够计算其盈亏曲线,它可以在逐日盯市结算的基础上进行改造得到;
4、可以很好地描述交易策略的交易过程和账户的权益变化情况;
5、更适合交易者和交易策略使用。
把下面的代码保持为vnpy\usertools\trade_line.py:
'''
Author: hxxjava
Date: 2021-06-24 19:57:32
LastEditTime: 2021-07-05 21:23:04
LastEditors: Please set LastEditors
Description:
'''
from dataclasses import dataclass
from typing import Callable
from vnpy.trader.constant import Direction,Offset
from vnpy.event import EventEngine,Event
from copy import deepcopy
EVENT_PRICE_CHANGE = "ePriceChange." # 价格变化消息
@dataclass
class PriceData:
"""
价格变化数据定义
"""
sponsor:str # 发起者
price_name:str # 价格名称
price:float # 价格
@dataclass
class TradeCommand:
"""
交易指令数据定义
"""
trade_line_name:str
sponsor:str # 发起者
price_name:str # 价格名称
direction:Direction
offset:Offset
price:float
payup:float
trade_num:int
class TradeLine():
"""
名称:交易线。按照设定的条件,发出交易指令。相当于预警下单线。
"""
def __init__(self,event_engine:EventEngine,tc:TradeCommand,condition:str,on_trade_command: Callable,excuted:bool=False):
self.event_engine = event_engine
self.tc = tc
self.condition = condition
self.price = None
self.excuted = excuted
self.on_trade_command = on_trade_command
self.register_event()
def register_event(self):
""" 注册消息 """
self.event_engine.register(EVENT_PRICE_CHANGE,self.process_pricechange_event)
def process_pricechange_event(self,event:Event):
""" """
pc:PriceData = event.data
if self.tc.sponsor == pc.sponsor and self.tc.price_name == pc.price_name:
self.check_price(pc.price)
def check_price(self,price:float):
""" """
if self.excuted:
self.price = price
return
if self.condition == ">":
# 大于触发价
if price > self.tc.price:
self.send_trade_event(price)
elif self.condition == "<":
# 大于触发价
if price < self.tc.price:
self.send_trade_event(price)
elif self.condition == ">=":
# 大于触发价
if price >= self.tc.price:
self.send_trade_event(price)
elif self.condition == "<=":
# 大于触发价
if price <= self.tc.price:
self.send_trade_event(price)
elif self.condition == "^":
# 上穿触发价
if self.price is not None:
if self.price <= self.tc.price < price:
self.send_trade_event(price)
elif self.condition == "v":
# 下穿触发价
if self.price is not None:
if self.price >= self.tc.price > price:
self.send_trade_event(price)
self.price = price
def send_trade_event(self,price:float):
""" """
if self.excuted:
return
tc = deepcopy(self.tc) # 简单,不模棱两可
# tc.price=price # 注释掉,直接用交易线的价格
self.excuted = True
# 执行交易指令
self.on_trade_command(tc)
def set_emit_price(self,price:float):
""" 设置交易线触发价格 """
self.tc.price = price
def set_excuted(self,excuted:bool):
""" 设置交易线是否交易过标志 """
self.excuted = excuted
def reset(self):
""" 复位交易线 """
self.price = None
self.excuted = False
def __repr__(self) -> str:
out_str = "TradeLine=({},condition={},on_trade_command={},price={},excuted={})".\
format(
self.tc,
self.condition,
self.on_trade_command,
self.price,
self.excuted
)
return out_str
下面是一个测试用的价差交易的策略例子,只为可以演示交易线的创建、触发价格更新和最新价格的更新和交易动作的执行,并不表示该策略可以盈利。
把下面代码保存为boll_arbitrage_strategy.py,保存到vnpy_spreadtrading\stategies目录下,就可以创建价差策略来观察trade_line的运作机制。
""" """
from datetime import datetime
from typing import List,Dict,Tuple,Union
from vnpy.event.engine import Event, EventEngine
from os import close, name, write
from typing import Dict
from vnpy.trader.constant import Direction, Offset,InstrumentStatus
from numpy import nan
import talib
from vnpy.trader.utility import BarGenerator, ArrayManager
from vnpy_spreadtrading.base import LegData
from vnpy_spreadtrading import (
SpreadStrategyTemplate,
SpreadAlgoTemplate,
SpreadData,
OrderData,
TradeData,
TickData,
BarData
)
from vnpy.usertools.trade_line import (
EVENT_PRICE_CHANGE,
PriceData,
TradeCommand,
TradeLine
)
class BollArbitrageStrategy(SpreadStrategyTemplate):
"""
利用Keltner通道进行套利的一种价差交易
"""
author = "hxxjava"
bar_window = 5 # K线周期
boll_window = 26 # BOLL参数1
max_steps = 4 # 最大开仓次数
min_step = 2 # 最小开仓距离
step_pos = 1 # 每步开仓数量
payup = 10
interval = 5
spread_pos = 0.0 # 当前价差数量
boll_mid = nan # BOLL中轨
step = nan # 开仓步长
parameters = [
"bar_window",
"boll_window",
"max_steps",
"min_step",
"step_pos",
"payup",
"interval"
]
variables = [
"boll_mid",
"step",
"spread_pos",
]
def __init__(
self,
strategy_engine,
strategy_name: str,
spread: SpreadData,
setting: dict
):
""""""
super().__init__(
strategy_engine, strategy_name, spread, setting
)
self.event_engine:EventEngine = self.strategy_engine.event_engine
self.bg = BarGenerator(self.on_spread_bar,self.bar_window,self.on_xmin_spread_bar)
self.am = ArrayManager(size=60)
self.pre_spread_pos = self.spread_pos
self.trade_lines:Dict[str,TradeLine] = {}
self.init_trade_lines()
self.bars:List[BarData] = []
def on_init(self):
"""
Callback when strategy is inited.
"""
self.write_log("策略初始化")
self.load_bar(days=10,callback=self.on_spread_bar)
# self.load_tick(days=1)
def on_start(self):
"""
Callback when strategy is started.
"""
# 开启响应的所有交易线的交易功能
self.write_log("策略启动")
self.adjust_trade_lines_emitprice()
self.set_trade_lines_excuted("all_lines",False)
self.adjust_trade_lines_excute()
def on_stop(self):
"""
Callback when strategy is stopped.
"""
self.write_log("策略停止")
self.put_event()
def on_spread_data(self):
"""
Callback when spread price is updated.
"""
tick = self.get_spread_tick()
self.on_spread_tick(tick)
def on_spread_tick(self, tick: TickData):
"""
Callback when new spread tick data is generated.
"""
self.bg.update_tick(tick)
if self.trading:
pc = PriceData(sponsor=self.strategy_name,price_name=self.spread_name,price=tick.last_price)
self.event_engine.put(Event(EVENT_PRICE_CHANGE,data=pc))
def on_spread_bar(self,bar:BarData):
"""
Callback when 1 min spread bar data is generated.
"""
# print(f"on_spread_bar bar={bar}")
self.bg.update_bar(bar)
def on_xmin_spread_bar(self, bar: BarData):
"""
Callback when x min spread bar data is generated.
"""
self.bars.append(bar)
if len(self.bars) > 100:
self.bars.pop(0)
self.am.update_bar(bar)
if not self.am.inited:
return
# self.boll_mid = self.am.ma(self.boll_window)
self.boll_mid = talib.MA(self.am.close,self.boll_window)[-1]
# BOLL通道
std = self.am.std(self.boll_window,array=True)
temp = talib.MA(std,5)
self.step = 2*temp[-1]
self.adjust_trade_lines_emitprice()
print(f"boll_mid,step=({self.boll_mid,self.step})")
self.put_event()
def on_spread_pos(self):
"""
Callback when spread position is updated.
"""
self.spread_pos = self.get_spread_pos()
if self.spread_pos == 0:
if self.pre_spread_pos>0:
# 复位做多交易线和空平交易线交易标志
self.set_trade_lines_excuted("below_lines",False)
if self.pre_spread_pos<0:
# 复位做多交易线和空平交易线交易标志
self.set_trade_lines_excuted("above_lines",False)
if self.pre_spread_pos == 0:
if self.spread_pos>0:
# 当持有多仓时,使能平多仓交易线
self.set_trade_lines_excuted("above_mid",False)
if self.spread_pos<0:
# 当持有空仓时,使能平多仓交易线
self.set_trade_lines_excuted("below_mid",False)
self.pre_spread_pos = self.spread_pos
self.put_event()
def init_trade_lines(self):
"""
init all trade lines
"""
for step in range(self.min_step,self.min_step+self.max_steps):
line_name = f"trade_line{step}"
tc = TradeCommand(trade_line_name=line_name,
sponsor=self.strategy_name,
price_name=self.spread_name,
direction=Direction.SHORT,
offset=Offset.OPEN,
price=0.0,trade_num=self.step_pos,payup=self.payup)
trade_line = TradeLine(
event_engine = self.event_engine,
condition=">=",
tc = tc,on_trade_command=self.on_trade_command,
excuted=True)
self.trade_lines[line_name] = trade_line
line_name = f"trade_line{-step}"
tc = TradeCommand(trade_line_name=line_name,
sponsor=self.strategy_name,
price_name=self.spread_name,
direction=Direction.LONG,
offset=Offset.OPEN,
price=0.0,trade_num=self.step_pos,payup=self.payup)
trade_line = TradeLine(
event_engine = self.event_engine,
condition="<=",
tc = tc,on_trade_command=self.on_trade_command,
excuted=True)
self.trade_lines[line_name] = trade_line
line_name = f"below_mid"
tc = TradeCommand(trade_line_name=line_name,
sponsor=self.strategy_name,
price_name=self.spread_name,
direction=Direction.LONG,
offset=Offset.CLOSE,
price=0.0,trade_num=-1,payup=self.payup)
trade_line = TradeLine(
event_engine = self.event_engine,
condition="v", # 中轨之下
tc = tc,on_trade_command=self.on_trade_command,
excuted=False if self.spread_pos < 0 else True)
self.trade_lines[line_name] = trade_line
line_name = f"above_mid"
tc = TradeCommand(trade_line_name=line_name,
sponsor=self.strategy_name,
price_name=self.spread_name,
direction=Direction.SHORT,
offset=Offset.CLOSE,
price=0.0,trade_num=-1,payup=self.payup)
trade_line = TradeLine(
event_engine = self.event_engine,
condition="^", # 中轨之上
tc = tc,on_trade_command=self.on_trade_command,
excuted=False if self.spread_pos > 0 else True)
self.trade_lines[line_name] = trade_line
def on_trade_command(self,tc:TradeCommand):
"""
process trade command
"""
if not self.trading:
return
# print(f"excute trade command :{self.get_spread_tick()} {tc}")
if nan in [self.boll_mid,self.step]:
return
if tc.direction == Direction.LONG:
volume = tc.trade_num
if tc.trade_line_name == "below_mid":
# 中轨下全部清空仓
if not self.spread_pos < 0:
self.write_log("无空仓可平!")
return
volume = abs(self.spread_pos)
if volume == 0:
self.write_log("做多0$错误!")
return
next_vol = abs(self.spread_pos+volume)
if not next_vol <= self.max_steps*self.step_pos:
self.write_log("再做多将超过最大仓位")
return
algoid = self.start_long_algo(
price=tc.price,
volume=volume,
payup=tc.payup,
offset=tc.offset,
interval=self.interval)
print(f"executed start_long_algo : {algoid}")
elif tc.direction == Direction.SHORT:
volume = tc.trade_num
if tc.trade_line_name == "above_mid":
# 中轨下全部清多仓
if not self.spread_pos > 0:
self.write_log("无空仓可平!")
return
volume = abs(self.spread_pos)
if volume == 0:
self.write_log("做空0$错误!")
return
next_vol = abs(self.spread_pos-volume)
if not next_vol <= self.max_steps*self.step_pos:
self.write_log("再做空将超过最大仓位")
return
algoid = self.start_short_algo(
price=tc.price,
volume=volume,
payup=tc.payup,
offset=tc.offset,
interval=self.interval)
print(f"executed start_short_algo : {algoid}")
def adjust_trade_lines_emitprice(self):
""" 调整各个交易线的触发价格 """
for step in range(self.min_step,self.min_step+self.max_steps):
# 调整做空交易线的触发价格
line_name = f"trade_line{step}"
trade_line = self.trade_lines.get(line_name,None)
if trade_line:
trade_line.set_emit_price(self.boll_mid+step*self.step)
# 调整做多交易线的触发价格
line_name = f"trade_line{-step}"
trade_line = self.trade_lines.get(line_name,None)
if trade_line:
trade_line.set_emit_price(self.boll_mid-step*self.step)
# 调整上中轨线和下中轨线的触发价格
for line_name in ["above_mid","below_mid"]:
trade_line = self.trade_lines.get(line_name,None)
if trade_line:
trade_line.set_emit_price(self.boll_mid)
def set_trade_lines_excuted(self,select:str,excuted:bool):
"""
设置两套多交易线的执行标志
select = "all_lines":设置所有交易线的执行标志
"above_lines":设置所有做空交易线的执行标志
"below_lines":设置所有做多交易线的执行标志
"below_mid":设置平空仓交易线的执行标志
"above_mid":设置平多仓交易线的执行标志
excuted = True:已执行;False:未执行
"""
for step in range(self.min_step,self.min_step+self.max_steps):
if select in ["above_lines","all_lines"]:
# 做空交易线
line_name = f"trade_line{step}"
trade_line = self.trade_lines.get(line_name,None)
if trade_line:
trade_line.set_excuted(excuted)
if select in ["below_lines","all_lines"]:
# 做多交易线
line_name = f"trade_line{-step}"
trade_line = self.trade_lines.get(line_name,None)
if trade_line:
trade_line.set_excuted(excuted)
if select in ["below_mid","all_lines"]:
# 下中轨线
trade_line = self.trade_lines.get("below_mid",None)
if trade_line:
trade_line.set_excuted(excuted)
if select in ["above_mid","all_lines"]:
# 上中轨线
trade_line = self.trade_lines.get("above_mid",None)
if trade_line:
trade_line.set_excuted(excuted)
def adjust_trade_lines_excute(self):
""" 调整各个交易线的执行标志 """
if self.spread_pos == 0:
self.set_trade_lines_excuted("above_lines",False)
self.set_trade_lines_excuted("below_lines",False)
self.set_trade_lines_excuted("above_mid",True)
self.set_trade_lines_excuted("below_mid",True)
elif self.spread_pos > 0:
self.set_trade_lines_excuted("above_lines",True)
self.set_trade_lines_excuted("above_mid",True)
self.set_trade_lines_excuted("below_mid",False)
elif self.spread_pos < 0:
self.set_trade_lines_excuted("below_lines",True)
self.set_trade_lines_excuted("above_mid",False)
self.set_trade_lines_excuted("below_mid",True)
def on_spread_algo(self, algo: SpreadAlgoTemplate):
"""
Callback when algo status is updated.
"""
if not algo.is_active():
print(f"algoid = {algo.algoid}")
def on_order(self, order: OrderData):
"""
Callback when order status is updated.
"""
print(f"{self.spread_name} {order}")
def on_trade(self, trade: TradeData):
"""
Callback when new trade data is received.
"""
print(f"{self.spread_name} {trade}")
慢体会它用法,您也许会有惊喜发现!
【注:以下的内容来自SFIT的官方文档《综合交易平台结算平台业务操作手册》,上传以下的几个帖子的目的是:通过对《综合交易平台结算平台业务操作手册》中对费率的设置的研究,让大家明白手续费率和保证金率的概念和计算方法,进而正确地计算出交易中发生的手续费和保证金,以及为了计算交易手续费和保证金所需要的参数有哪些。】
手续费设置本平台分为交易所手续费率设置和投资者手续费率设置,主要是对期货合约的手续费率按照交易所规定和公司要求进行设置,分为开仓、平仓、平今、结算、交割、移仓等6种手续费率,其中结算、移仓手续费率并未启用,不需要设置。每种手续费率,以按金额、按手数等2种方式取和收取手续费。记按金额手续费率为R金额、按手数手续费率为R手数,手续费C手续费的计算公式如下:
C手续费=R金额×成交金额+R手数×成交手数
对于新合约的上市,需要设置交易所手续费率,在flex柜台中进行新增、修改、删除、查询操作,交易所手续费设置,主要用于设置交易所手续费率,以计算每笔成交的上交手续费,分为按产品和合约设置,同一产品和该产品的合约,合约优先级大于产品。在结算柜台中的“手续费率”目录下,“交易所手续费率设置”“交易所手续费率查询”菜单可以进行交易所手续费率的新增、修改和查询操作。手续费率设置后重新结算,就按新设置的手续费率计算手续费,盘中设置手续费率是否实时上场。
Flex柜台界面操作:
“费率设置->手续费率->交易所手续费率设置->新增即可”
查询:选择交易所、产品/合约,若均为空的话,则是对所有产品和合约进行查询;
新增:选择交易所;选择产品/合约,合约的优先级高于产品;对开仓、平仓、平今、结算、交割四个手续费率进行添加,结算、移仓手续费率并未启用,不需要设置,按金额、按手数等2种方式取和收取手续费;
修改:选中需要修改的记录,点击“修改”按钮,进入“交易所手续费率(修改)”界面,可以修改费率,不能修改交易所和产品/合约;
删除:选中需要删除的记录,即可进行删除操作。
投资者手续费设置,主要用于设置投资者手续费率,以计算每笔成交的投资者手续费。在结算柜台中的“手续费率”目录下,“投资者手续费率设置”菜单可以进行投资者手续费率的新增、修改和查询操作。投资者手续费率分为公司标准、模板、单一投资者,按照所述对象范围的粒度大小,这3种情况对应的保证金率设置存在如下的优先级关系:
单一投资者>模板>公司标准
系统按照投资者范围优先级的先后关系,三种情况设置分为按品种和按合约。对某一个投资者手续费率生效的优先级别是单一投资者>手续费率模板>公司标准,同一产品和该产品的合约,合约优先级别大于产品,系统依次去寻找投资者的手续费率设置,直到找到相应设置为止。
手续费率设置后重新结算,就按新设置的手续费率计算手续费,盘中设置手续费率是实时上场。手续费率实时同步的设置在flex柜台界面的操作流程:
“费率设置->手续费率盘中同步参数设置->选择“是”->点击确认”。
针对投资者手续费率设置中,分为三种情况,一、设置所有投资者手续费率,二、设置手续费率模板手续费率,三、 设置单一投资者手续费率,单一投资者设置与公司标准设置相对比较简单,下面主要介绍一新开户投资者模板手续费率设置步骤。
对投资者手续费率进行批量调整时,可以通过手续费率模型来实现。可以通过创建一个模型A实现对当前投资者手续费率数据进行备份,同时将数据复制到一个新模型B,如果手续费进行批量调整时可以对模型B进行批量修改,激活启用,那么投资者手续费率即按照模型B来进行收取;若过段时间手续费率需要进行恢复以前设置,则激活模型A即可。
手续费率模型应用步骤如下:
在flex中界面操作:
▶“ 费率设置->手续费率模型管理->手续费率模型-> 点击新增->填写手续费率模型代码、名称、选择交易所和产品、备注->从当前数据创建或确认”
注:若点击“确认”,手续费率需要到“模型投资者手续费率”中进行添加设置,添加过程中是与所创建模型是相对应的,包括:交易所得选择、产品/合约的选择;若是“从当前数据创建”,则所创建模型即为当前手续费率数据的设置;注:当交易所为空时,表示该手续费率模型的适用范围为所有交易所所有产品,激活时将覆盖所有手续费率;创建模型时,当交易所不为空,产品为空时,表示该手续费率模型的适用范围为特定交易所所有产品,激活时将覆盖特定交易所的所有产品;当交易所、产品都不为空时,表示该手续费率模型的适用范围为特定交易所特定产品,激活时将覆盖特定交易所特定产品的手续费率。
▶“费率设置-> 手续费率模型管理->模型手续费率->新增->填写模型代码、选择交易所、产品/合约、投资者范围、添加开仓、平仓、平今、交割手续费率(按金额和按手数) ->点击确认按钮”
查询:可以按照模型代码、交易所、产品/合约、投资者范围、组织架构进行查询,若为空,则查询全部;
新增:添加模型手续费率的设置;
修改:只能修改相关费率字段,不能修改费率模板、交易所、产品/合约;
删除:选中记录,点击“删除”按钮即可;
批量修改:可以实现对公司范围、模板、单一投资者进行批量修改,可分为:绝对调整(调整后的值为当前调整)和相对调整(调整后的值为调整值加上原有值),调整完成后,可以进行试算操作,检查是否正确,若正确则点击确认按钮。
批量删除:可以对查询到的记录进行全部删除;
复制:是将一个手续费率模型复制到另一个手续费率模型,可以按照投资者范围、交易所的产品/合约进行复制,则目标的模型手续费率设置将被删除、替换为源的模型手续
费率。
▶在“手续费率模型”界面中,选中所需要模型,点击“激活”即可,这样就实现对一个新建模型的启用。
▲交易所手续费率、投资者手续费率设置都是实时生效的。
条件单是一个带有触发条件的指令。该触发条件可以以市场上的最新行情为基准,也可以以指定价格为基准。比如:一个投资者有1手IF1910的空头持仓,并希望在市场价低于2200时买入平仓,他就可以使用条件单。这样当行情波动到满足该条件时,该报单就会被自动触发报出,而不需要他本人时刻盯着电脑屏幕去监视市场行情。
申报条件单时,条件单的报单回报中OrderSysID是以字符串“TJBD_”开头的由CTP系统自定义的报单编号,针对该交易所当日唯一;只对成功申报的条件单编号,错单不编号。因为这种情况下的编号由CTP系统自行维护,与交易所无关,仅用于管理本系统内的该条件单。
有效的使用条件单,可以做出限价止损指令(stop-and-limit order)和触及市价指令(market-if-touched order)。
CTP条件单为CTP后台系统自带指令,并非交易所官方支持指令。
报入条件单指令使用ReqOrderInsert函数
其中的核心数据结构是CThostFtdcInputOrderField
在结构体中和普通报单有区别的字段如下:
///触发条件
TThostFtdcContingentConditionType ContingentCondition;
///止损价
TThostFtdcPriceType StopPrice;
条件单有效的触发条件如下:
///最新价大于条件价
#define THOST_FTDC_CC_LastPriceGreaterThanStopPrice '5'
///最新价大于等于条件价
#define THOST_FTDC_CC_LastPriceGreaterEqualStopPrice '6'
///最新价小于条件价
#define THOST_FTDC_CC_LastPriceLesserThanStopPrice '7'
///最新价小于等于条件价
#define THOST_FTDC_CC_LastPriceLesserEqualStopPrice '8'
///卖一价大于条件价
#define THOST_FTDC_CC_AskPriceGreaterThanStopPrice '9'
///卖一价大于等于条件价
#define THOST_FTDC_CC_AskPriceGreaterEqualStopPrice 'A'
///卖一价小于条件价
#define THOST_FTDC_CC_AskPriceLesserThanStopPrice 'B'
///卖一价小于等于条件价
#define THOST_FTDC_CC_AskPriceLesserEqualStopPrice 'C'
///买一价大于条件价
#define THOST_FTDC_CC_BidPriceGreaterThanStopPrice 'D'
///买一价大于等于条件价
#define THOST_FTDC_CC_BidPriceGreaterEqualStopPrice 'E'
///买一价小于条件价
#define THOST_FTDC_CC_BidPriceLesserThanStopPrice 'F'
///买一价小于等于条件价
#define THOST_FTDC_CC_BidPriceLesserEqualStopPrice 'H'
条件价对应的字段为StopPrice
结算价交易(Trading at Settlement,以下简称TAS)指令,允许交易者在规定交易时段内按照期货合约当日结算价或当日结算价增减若干个最小变动价位申报买卖期货合约。
TAS指令仅可与同一合约的TAS指令撮合成交。在集合竞价采用最大成交量原则进行撮合,在连续竞价交易时段采用价格优先、时间优先原则进行撮合。
TAS指令已在国际成熟市场得到广泛使用,其本质是为市场提供了一种便捷高效的风险管理工具,从而对促进市场功能发挥、改善市场投资者结构、提升价格影响力等起到积极作用。
对于实体企业,在进行现货贸易点价时,多采用期货合约结算价作为定价基准。在利用期货市场进行风险对冲时,如果没有这个工具,一般要通过盘中多次下单来模拟当日结算价,因此在交易执行方面存在较高的难度,且套保效率较低。
TAS指令可以在规定的交易时段内以结算价或结算价附近的价格进行交易,大大降低了企业围绕结算价交易的不确定性。企业可利用TAS指令更便利、更精准地做好风险管理。
从成熟市场普遍情况看,除了实体企业利用TAS指令进行风险管理,ETF和长期策略基金等机构投资者也运用TAS指令来进行换月移仓交易等,这有效降低了因机构头寸大量变化对市场短期价格的冲击。因此,TAS指令的推出能够促进市场主体有效改善利用结算价进行套保交易的执行效率,吸引境内外多元化市场主体的积极参与,进一步完善投资者结构,有益于市场稳定运行。同时有利于交易者更为便利和广泛地参考中国期货价格,从而提升市场的价格影响力。
11:11:22 初始化CTA回测引擎
11:11:22 策略文件加载完成
11:12:26 ----------------------------------------
11:12:26 rb2201.SHFE-1m开始下载历史数据
11:12:26 数据下载失败,触发异常:
Traceback (most recent call last):
File "D:\ProgramFiles\VnStudio\lib\site-packages\vnpy_ctabacktester\engine.py", line 404, in run_downloading
data = self.datafeed.query_bar_history(req)
File "D:\ProgramFiles\VnStudio\lib\site-packages\vnpy_rqdata\rqdata_datafeed.py", line 164, in query_bar_history
adjust_type="none"
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\decorators.py", line 150, in wrap
return func(args, **kwargs)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\get_price.py", line 103, in get_price
order_book_ids, market
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\get_price.py", line 222, in classify_order_book_ids
ins_list = ensure_instruments(order_book_ids, market=market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\validators.py", line 168, in ensure_instruments
all_instruments = _all_instruments_dict(market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\decorators.py", line 129, in wrapper
value = user_function(args, kwargs)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\basic.py", line 139, in _all_instruments_dict
ins = _all_cached_instruments_list(market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\decorators.py", line 129, in wrapper
value = user_function(*args, kwargs)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\basic.py", line 134, in _all_cached_instruments_list
return _all_instruments_list(market)
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\services\basic.py", line 112, in _all_instruments_list
ins = [Instrument(i) for i in get_client().execute("all_instruments", market=market)]
File "D:\ProgramFiles\VnStudio\lib\site-packages\rqdatac\client.py", line 31, in execute
raise RuntimeError("rqdatac is not initialized")
RuntimeError: rqdatac is not initialized
虽然说是整个删除以前的版本,然后重新安装的2.6版本,但是以前的设置一点也没有改变,难道是以前的米筐接口设置不可以在新版本中继续使用了吗?
在CTP接口之保证金率手续费率查询中我们已经讲过如何查询保证金率和手续费率,这一篇再详细讲一下根据费率计算相应的保证金,为最终资金权益等的计算做铺垫。
保证金的计算公式如下:
保证金 = 按手数保证金费*手数+按金额保证金率*金额*手数*合约乘数=(按手数保证金费+按金额保证金率*金额*合约乘数)*手数
按照API中的字段即是:
多头保证金=(LongMarginRatioByVolume + LongMarginRatioByMoney * Price *VolumeMultiple )* Volume
空头保证金类似。
这个公式中按手数保证金费和按金额保证金率可以通过ReqQryInstrumentMarginRate查询得到;合约乘数可以通过ReqQryInstrument查询得到;手数是涉及到计算到的手数。那就只剩下一个问题,用什么金额去计算?
用什么金额去计算客户的保证金,对于昨仓统一使用昨结算价计算,对于今仓,在期货公司后台是可以设置的。客户也可以通过ReqQryBrokerTradingParams(请求查询经纪公司交易参数)查询得到设置的类型。查询示例如下:
qryField = api.CThostFtdcQryBrokerTradingParamsField()
qryField.BrokerID="8000"
qryField.InvestorID="012345"
qryField.CurrencyID = "CNY"
tradeapi.ReqQryBrokerTradingParams(qryField,0)
在该查询的回调结果中查看MarginPriceType枚举值:
def OnRspQryBrokerTradingParams(self,
pBrokerTradingParams: 'CThostFtdcBrokerTradingParamsField',
pRspInfo: 'CThostFtdcRspInfoField',
nRequestID: 'int', bIsLast: 'bool') -> "void":
if pBrokerTradingParams is not None:
print(f"MarginPriceType:{pBrokerTradingParams.MarginPriceType}")
枚举值列表如下:
///昨结算价
#define THOST_FTDC_MPT_PreSettlementPrice '1'
///最新价
#define THOST_FTDC_MPT_SettlementPrice '2'
///成交均价
#define THOST_FTDC_MPT_AveragePrice '3'
///开仓价
#define THOST_FTDC_MPT_OpenPrice '4'
可见计算保证金中用到的价格类型有4种。
下面我们举例说明,设au2106昨结算价为391.74,查询合约得到合约乘数为1000,查询保证金率得到LongMarginRatioByMoney为0.08,LongMarginRatioByVolume为0.0,ShortMarginRatioByMoney为0.08,ShortMarginRatioByVolume为0.0。
目前simnow即采用昨结算价计算保证金。如果上述查询返回得到参数MarginPriceType为1,则说明CTP后台使用昨结算价计算保证金。
此时假设以396.16报买开1手,未成交,CTP会冻结保证金,账户的可用资金会减少。这时,
冻结保证金=(按手数保证金费+按金额保证金率*昨结算价*合约乘数)*手数=(0.08*391.74*1000)*1=31339.20。
经过一段时间 ,该买开仓以396.16成交,此时客户有1手多头持仓,相应的冻结保证金减少,增加占用保证金。这时,
冻结保证金=0占用保证金=(按手数保证金费+按金额保证金率*昨结算价*合约乘数)*手数=(0.08*391.74*1000)*1=31339.20。
在整个交易日内,au2106合约的行情最新价格上上下下不停变化,这时客户该合约上的持仓盈亏也不断变化,但是这1手占用的保证金因为是用昨结算价计算的,昨结算价不变,占用保证金也不变。
等到第二个交易日,该合约有新的结算价时,占用保证金才发生相应的变化。
如果上述查询返回得到参数MarginPriceType为2,则说明CTP后台是以行情中最新价计算保证金的。
这时冻结保证金和占用保证金均是使用最新 价,而且会随着最新价的更新而动态变化。其他的价格类型也是类似计算,按成交均价算则是按照行情里面的均价更新,按开仓价算则是冻结时用报单价,成交后用成交价计算。
只有在最新价和成交均价这两种情况下今仓的保证金值才会随着行情波动而更新,昨仓的保证金值一直是用昨结算价计算得到,不会改变。
为了增加资金可用率,交易所都有一定的保证金优惠政策。例如上期所的品种大额单边保证金优惠,是指同一品种内所有的合约按多空分别累计保证金,然后实际只收大额一边的保证金,因为同品种多空有一定风险对冲,同时收两边保证金过于保守。
客户总资金为200,000,此时au2106多头占用保证金100,000,au2102空头占用保证金80,000,根据品种大单边优惠规则,CTP后台实际算占用保证金为100,000,也就是说客户现在可用资金为200,000-100,000=100,000。
这时客户又成交1手au2104的空头,占用保证金为30,000,总的空头占用保证金为110,000,大于多头的100,000。所以CTP后台实际占用保证金为110,000,客户可用资金为200,000-110,000=90,000。
品种是否采用大额单边保证金算法可以根据查合约ReqQryInstrument请求得到返回中的MaxMarginSideAlgorithm来判断。当下的大额单边保证金占用多少,可以通过ReqQryInvestorProductGroupMargin查询得到。
除了上期所品种内的大额单边保证金优惠外,中金所还有跨品种的大额单边保证金优惠,例如对股指类期货:沪深300股指期货、上证50股指期货和中证500股指期货这三个的跨品种双向持仓,按照交易保证金单边较大者收取交易保证金;对国债类期货也实施跨品种单向大边保证金制度。
大商所郑商所对普通合约还是按照双边收取保证金,但对于标准套利合约只收两个保证金中较高的,大商所盘中申请组合成功也有大边保证金优惠。
保证金率在整个合约存在过程中并不是一直不变,常见的调整保证金情况有:
对于市价单,在计算保证金冻结时,使用涨跌停价计算,计算占用保证金时,按照价格类型采用相应的价格计算。
保证金率和手续费率都是程序化交易者非常关心的话题,保证金率涉及到开仓时账户资金的冻结和占用,撤单或平仓则会解冻和解占用。手续费率则是真正要付给期货公司的佣金,而且通常开仓,平昨和平今手续费还各有不同。
这里面各种各样的坑,比如有人反应期货公司有的按交易所保证金率收,有的按交易所两倍来收,这样同样的钱在有的期货公司够开2$,而在有的就只能开1手;手续费那更是收的五花八门,各种各样的都有。甚至还有人反应出现过实际收的和开户时说的不一样这种事。
其实这两个费率都是可以通过CTP API直接查询得到的,但这两个查询和其他查询(如查询合约,资金,结算单等)有点不一样,所以本篇单独讲一下。
官方文档中涉及到保证金率的接口好几个,新手开发看到经常一脸懵逼,不知道究竟哪个才是实际中用到的。这里我们先罗列一下相关的:
//请求查询合约保证金率
def ReqQryInstrumentMarginRate(self,
pQryInstrumentMarginRate: 'CThostFtdcQryInstrumentMarginRateField',
nRequestID: 'int') -> "int"
//查询合约的返回中会带每条合约的保证金率
def ReqQryInstrument(self,
pQryInstrument: 'CThostFtdcQryInstrumentField',
nRequestID: 'int') -> "int"
//请求查询交易所保证金率
def ReqQryExchangeMarginRate(self,
pQryExchangeMarginRate: 'CThostFtdcQryExchangeMarginRateField',
nRequestID: 'int') -> "int"
//请求查询交易所调整保证金率
def ReqQryExchangeMarginRateAdjust(self,
pQryExchangeMarginRateAdjust: 'CThostFtdcQryExchangeMarginRateAdjustField',
nRequestID: 'int') -> "int"
在实际交易中冻结和占用的计算其实是采用第一个查询函数ReqQryInstrumentMarginRate的返回结果。
而ReqQryInstrument中返回的保证金率通常是交易所保证金率,计算中并不用到。ReqQryExchangeMarginRate和ReqQryExchangeMarginRateAdjust查询得到的是交易所相关的保证金率,这也只在计算中间过程中会用到。
这里我们只讲实际计算中用到的ReqQryInstrumentMarginRate查询,其参数类型为CThostFtdcQryInstrumentMarginRateField,具体字段:
BrokerID //经纪公司代码
InvestorID //投资者代码
HedgeFlag //投机套保标志
前两者开户时可得到,投机套保标志一般投资者填投机就可以。注意这三个字段是一定要填的,不填的话返回值就为空。
InstrumentID //合约代码
如果InstrumentID填空,则返回客户当前持仓对应的合约保证金率,否则返回相应InstrumentID的保证金率。也就是说这个查询和其他查询不一样,如果什么都不填,是不会得到所有的合约保证金率的。
最后给出个查询保证金率的示例:
qryinfofield = api.CThostFtdcQryInstrumentMarginRateField()
qryinfofield.BrokerID='9999'
qryinfofield.InvestorID='000001'
qryinfofield.InstrumentID='rb2007'
qryinfofield.HedgeFlag = api.THOST_FTDC_HF_Speculation
tapi.ReqQryInstrumentMarginRate(qryinfofield,0)
查手续费的接口也有好几个,罗列如下:
//请求查询合约手续费率
def ReqQryInstrumentCommissionRate(self,
pQryInstrumentCommissionRate: 'CThostFtdcQryInstrumentCommissionRateField',
nRequestID: 'int') -> "int":
//请求查询报单手续费
Def ReqQryInstrumentOrderCommRate(self,
pQryInstrumentOrderCommRate: 'CThostFtdcQryInstrumentOrderCommRateField',
nRequestID: 'int') -> "int":
// 请求查询期权或做市商合约手续费函数这里就不罗列了...
在期货交易中,实际计算用到的只有个前面两个函数,其中第一个查询的是合约的手续费率,第二个查询的是报单申报手续费(就是每报撤一次单子都会收一笔费用),目前来说这是中金所特有的。
也就是说中金所的手续费分为两部分,包括交易手续费和申报费两部分,前者可以通过第一个查询函数查到,后者通过第二个查询函数得到。
两个查询函数的参数类型虽然不一样,但是字段填写方法是一致的,这里我们以ReqQryInstrumentCommissionRate为例讲解,参数类型为CThostFtdcQryInstrumentCommissionRateField,具体字段:
BrokerID //经纪公司代码
InvestorID //投资者代码
InstrumentID //合约代码
如果InstrumentID填空,则返回客户当前持仓对应的合约手续费率,否则返回相应InstrumentID的合约手续费率。和上面查询保证金率的逻辑一样。
查询结果返回值还需要注意几点:
手续费 = 成交数量*(成交价*合约乘数*OpenRatioByMoney+OpenRatioByVolume)
最后给出个查询手续费率的示例:
qryinfofield = api.CThostFtdcQryInstrumentCommissionRateField()
qryinfofield.BrokerID='9999'
qryinfofield.InvestorID='000001'
qryinfofield.InstrumentID='rb2007'
tapi.ReqQryInstrumentCommissionRate(qryinfofield,0)
【来自CTP期货期权API接口说明.chm】
CTP交易系统基于安全和性能考虑,在诸多地方有流量控制,其中流量控制又分FTD报文流量控制、报单流量控制、查询流量控制等。而这些流量控制分布在各个不同的地方。此处将会给大家详细介绍。
报单流控是指用户在本交易系统报单(ReqOrderInsert)、撤单(ReqOrderAction)时每秒内允许的最大笔数。
报单流控限制配置在CTP柜台端【程序化交易频繁报撤单管理】菜单。
如果超过这个限制API会通过OnRspOrderAction提示:“CTP:下单频率限制”。
查询流控是指用户当前Session在做查询的时候每秒内允许的最大请求笔数。
查询流控配置在交易前置组件上,配置项为【QryFreq】。其中API内置了在途查询流控1笔。
自从穿透式监管版本以后,API在连接交易前置时,会去查询到前置的查询流控设置(该设置配置在front_se组件)。假设前置配置了2笔/秒,那么连接该前置的API每秒只能发# 起2笔查询请求。
但是要注意,不管怎么配置,API都内置了在途流控,在途查询流控为1笔。即,当前这笔查询请求发出后,在未收到响应前,不能发起下一笔查询请求。
在过去,查询流控是内置在API里,1笔每秒,在途1笔。
如果超过交易前置配置的查询流控,则会触发OnRspError,并提示:“CTP:查询未就绪,请稍后重试”
如果超过API内置的在途流控,则查询请求的返回值为-2,表示未处理请求超过许可数。
所有ReqQuery开头查询函数不受流控限制,原因是此类函数都是通过交易核心处理的,不通过查询核心。
FTD报文流控是指用户当前Session在提交API指令的时候每秒内允许的最大请求笔数。
FTD报文流控配置在交易前置组件上,配置项为【FTDMaxCommFlux】。
FTD报文流控是一种综合性的流控手段,API的所有接口在跟前置交互的时候都是使用FTD协议的报文进行通讯的,因此这就意味着,登录、查询、报单、撤单和报价等等所有请求都在FTD报文流控的限制范围内。
例如如果配置为6,可以简单理解为用户仅能调用6次API请求指令,包括登录、查询、报单撤单等等。
如果超过FTD报文流控,则超过的指令会被缓存到前置,直到下一秒发出。例如用户在一秒内报单10笔,则前6笔会立即发送给核心,而后4笔则会被缓存在前置,到下一秒才发出。因此对于用户程序端的感受就是后4笔报单延迟很大,实则是受到了流控。
注意,FTD报文流控不会有错误信息或错误返回。
前置连接数流控是指在本交易前置对同一IP每秒允许的最大API连接请求数。
前置连接数流控配置在交易前置组件上,配置项为【ConnectFreq】。
需要注意的是,API连接请求指的是API从init到OnFrontConnected的过程,跟登录无关。可以简单理解为一次init就是一个连接请求。
例如如果配置为20,则一秒内最多有20个session建立跟前置的连接。
如果超过前置连接数流控则会被主动断开连接,触发OnFrontDisconnected。因此用户如果发现自己的程序一连接前置就被断开,则除了版本问题外,有可能是遇到了连接数流控。
同一用户最大允许在线会话数是指同一个用户(UserID)在本交易系统中同时登录在线的最大允许会话数。
同一用户最大允许在线会话数配置在柜台或交易核心中。
注意,这个会话数针对的是本交易系统,而非单一前置。
如果超过同一用户最大允许在线会话,则会通过OnRspUserLogin返回“CTP:用户在线会话超出上限”的错误。
交易所API流控指通过交易所API发送报单等请求的每秒最大允许数。
交易所API流控的阈值设置在交易所端,由交易所API查询获取,该流控实际控制在交易所API端。
受到交易所流控后会触发OnRtnOrder,报“CTP:交易所每秒发送请求数超过许可数”或者“CTP:交易所未处理请求超过许可数”。