From fa13aaf27373480f532375b804359304c84402a3 Mon Sep 17 00:00:00 2001 From: msincenselee Date: Thu, 26 Dec 2019 15:08:55 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=96=B0=E5=8A=9F=E8=83=BD]=20tick=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=9C=8D=E5=8A=A1=EF=BC=8C=E8=AE=B0=E5=BD=95=E8=87=B3?= =?UTF-8?q?=E6=96=87=E4=BB=B6csv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vnpy/app/tick_recorder/__init__.py | 18 ++ vnpy/app/tick_recorder/engine.py | 270 +++++++++++++++++++++++++ vnpy/app/tick_recorder/ui/__init__.py | 1 + vnpy/app/tick_recorder/ui/recorder.ico | Bin 0 -> 67646 bytes vnpy/app/tick_recorder/ui/widget.py | 129 ++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 vnpy/app/tick_recorder/__init__.py create mode 100644 vnpy/app/tick_recorder/engine.py create mode 100644 vnpy/app/tick_recorder/ui/__init__.py create mode 100644 vnpy/app/tick_recorder/ui/recorder.ico create mode 100644 vnpy/app/tick_recorder/ui/widget.py diff --git a/vnpy/app/tick_recorder/__init__.py b/vnpy/app/tick_recorder/__init__.py new file mode 100644 index 00000000..65ebc750 --- /dev/null +++ b/vnpy/app/tick_recorder/__init__.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from vnpy.trader.app import BaseApp +from vnpy.trader.constant import Direction +from vnpy.trader.object import TickData, BarData, TradeData, OrderData +from vnpy.trader.utility import BarGenerator, ArrayManager + +from .engine import TickRecorderEngine, APP_NAME + +class TickRecorderApp(BaseApp): + """""" + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "Tick行情记录" + engine_class = TickRecorderEngine + widget_name = "RecorderManager" + icon_name = "recorder.ico" diff --git a/vnpy/app/tick_recorder/engine.py b/vnpy/app/tick_recorder/engine.py new file mode 100644 index 00000000..ed05d65f --- /dev/null +++ b/vnpy/app/tick_recorder/engine.py @@ -0,0 +1,270 @@ +""" +tick 文件记录 +华富资产 +""" +import os +import csv +from threading import Thread +from queue import Queue, Empty +from copy import copy +from collections import defaultdict +from datetime import datetime + +from vnpy.event import Event, EventEngine +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.trader.constant import Exchange +from vnpy.trader.object import ( + SubscribeRequest, + TickData, + ContractData +) +from vnpy.trader.event import EVENT_TICK, EVENT_CONTRACT +from vnpy.trader.utility import load_json, save_json +from vnpy.app.spread_trading.base import EVENT_SPREAD_DATA, SpreadData + + +APP_NAME = "DataRecorder" + +EVENT_RECORDER_LOG = "eRecorderLog" +EVENT_RECORDER_UPDATE = "eRecorderUpdate" + + +class TickFileRecorder(object): + """ Tick 文件保存""" + def __init__(self, tick_folder: str): + + self.tick_dict = defaultdict(list) # symbol_hour_min: [] + + self.tick_folder = tick_folder + + self.last_minute = 0 + + def save_tick_data(self, tick_list: list = []): + """接收外部的保存tick请求""" + min = None + for tick in tick_list: + min = tick.datetime.minute + key = f'{tick.vt_symbol}_{tick.datetime.hour}-{tick.datetime.minute}' + save_list = self.tick_dict[key] + save_list.append(tick) + + if min is not None and min != self.last_minute: + self.last_minute = min + self.save_expire_datas() + + def save_expire_datas(self): + """保存超时得数据""" + dt = datetime.now() + for key in [key for key in self.tick_dict.keys() if not key.endswith(f'{dt.hour}-{dt.minute}')]: + vt_symbol = key.split('_')[0] + tick_list = self.tick_dict.pop(key) + + self.append_ticks_2_file(symbol=vt_symbol, tick_list=tick_list) + + def append_ticks_2_file(self, symbol: str, tick_list: list): + """创建/追加tick list 到csv文件""" + if len(tick_list) == 0: + return + + trading_day = tick_list[0].trading_day + + file_folder = os.path.abspath(os.path.join(self.tick_folder, trading_day.replace('-', '/'))) + if not os.path.exists(file_folder): + os.makedirs(file_folder) + + file_name = os.path.abspath(os.path.join(file_folder, f'{symbol}_{trading_day}.csv')) + + dict_fieldnames = sorted(list(tick_list[0].__dict__)) + + dict_fieldnames.remove('datetime') + + dict_fieldnames.insert(0, 'datetime') + + if not os.path.exists(file_name): + # 写入表头 + print(f'create and write data into {file_name}') + with open(file_name, 'a', encoding='utf8', newline='') as csvWriteFile: + writer = csv.DictWriter(f=csvWriteFile, fieldnames=dict_fieldnames, dialect='excel') + writer.writeheader() + for tick in tick_list: + d = tick.__dict__ + d.update({'datetime': tick.datetime.strftime('%Y-%m-%d %H:%M:%S.%f')}) + writer.writerow(d) + else: + # 写入数据 + print(f'write data into {file_name}') + with open(file_name, 'a', encoding='utf8', newline='') as csvWriteFile: + writer = csv.DictWriter(f=csvWriteFile, fieldnames=dict_fieldnames, dialect='excel', extrasaction='ignore') + for tick in tick_list: + d = tick.__dict__ + d.update({'datetime': tick.datetime.strftime('%Y-%m-%d %H:%M:%S.%f')}) + writer.writerow(d) + + +class TickRecorderEngine(BaseEngine): + """""" + setting_filename = "data_recorder_setting.json" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + self.queue = Queue() + self.thread = Thread(target=self.run) + self.active = False + + self.tick_recordings = {} + self.tick_folder = '' + + self.load_setting() + + self.tick_recorder = TickFileRecorder(self.tick_folder) + + self.register_event() + self.start() + self.put_event() + + def load_setting(self): + """""" + setting = load_json(self.setting_filename) + self.tick_recordings = setting.get("tick", {}) + self.tick_folder = setting.get('tick_folder', os.getcwd()) + + def save_setting(self): + """""" + setting = { + "tick": self.tick_recordings + } + save_json(self.setting_filename, setting) + + def run(self): + """""" + while self.active: + try: + task = self.queue.get(timeout=1) + task_type, data = task + + if task_type == "tick": + self.tick_recorder.save_tick_data([data]) + + except Empty: + continue + + def close(self): + """""" + self.active = False + + if self.thread.isAlive(): + self.thread.join() + + def start(self): + """""" + self.active = True + self.thread.start() + + def add_tick_recording(self, vt_symbol: str): + """""" + if vt_symbol in self.tick_recordings: + self.write_log(f"已在Tick记录列表中:{vt_symbol}") + return + + # For normal contract + if Exchange.LOCAL.value not in vt_symbol: + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + self.write_log(f"找不到合约:{vt_symbol}") + return + + self.tick_recordings[vt_symbol] = { + "symbol": contract.symbol, + "exchange": contract.exchange.value, + "gateway_name": contract.gateway_name + } + + self.subscribe(contract) + # No need to subscribe for spread data + else: + self.tick_recordings[vt_symbol] = {} + + self.save_setting() + self.put_event() + + self.write_log(f"添加Tick记录成功:{vt_symbol}") + + def remove_tick_recording(self, vt_symbol: str): + """""" + if vt_symbol not in self.tick_recordings: + self.write_log(f"不在Tick记录列表中:{vt_symbol}") + return + + self.tick_recordings.pop(vt_symbol) + self.save_setting() + self.put_event() + + self.write_log(f"移除Tick记录成功:{vt_symbol}") + + def register_event(self): + """""" + self.event_engine.register(EVENT_TICK, self.process_tick_event) + self.event_engine.register(EVENT_CONTRACT, self.process_contract_event) + self.event_engine.register( + EVENT_SPREAD_DATA, self.process_spread_event) + + def update_tick(self, tick: TickData): + """""" + if tick.vt_symbol in self.tick_recordings: + self.record_tick(tick) + + def process_tick_event(self, event: Event): + """""" + tick = event.data + self.update_tick(tick) + + def process_contract_event(self, event: Event): + """""" + contract = event.data + vt_symbol = contract.vt_symbol + + if vt_symbol in self.tick_recordings: + self.subscribe(contract) + + def process_spread_event(self, event: Event): + """""" + spread: SpreadData = event.data + tick = spread.to_tick() + + # Filter not inited spread data + if tick.datetime: + self.update_tick(tick) + + def write_log(self, msg: str): + """""" + print(msg) + + def put_event(self): + """""" + tick_symbols = list(self.tick_recordings.keys()) + tick_symbols.sort() + + data = { + "tick": tick_symbols + } + + event = Event( + EVENT_RECORDER_UPDATE, + data + ) + self.event_engine.put(event) + + def record_tick(self, tick: TickData): + """""" + task = ("tick", copy(tick)) + self.queue.put(task) + + def subscribe(self, contract: ContractData): + """""" + req = SubscribeRequest( + symbol=contract.symbol, + exchange=contract.exchange + ) + self.main_engine.subscribe(req, contract.gateway_name) diff --git a/vnpy/app/tick_recorder/ui/__init__.py b/vnpy/app/tick_recorder/ui/__init__.py new file mode 100644 index 00000000..7339f138 --- /dev/null +++ b/vnpy/app/tick_recorder/ui/__init__.py @@ -0,0 +1 @@ +from .widget import RecorderManager diff --git a/vnpy/app/tick_recorder/ui/recorder.ico b/vnpy/app/tick_recorder/ui/recorder.ico new file mode 100644 index 0000000000000000000000000000000000000000..1ddc8b857059d6f41d87b0dea9634027ad3cfa93 GIT binary patch literal 67646 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 LK0W_}`1Jn|5jZ`@ literal 0 HcmV?d00001 diff --git a/vnpy/app/tick_recorder/ui/widget.py b/vnpy/app/tick_recorder/ui/widget.py new file mode 100644 index 00000000..724427b1 --- /dev/null +++ b/vnpy/app/tick_recorder/ui/widget.py @@ -0,0 +1,129 @@ +from datetime import datetime + + +from vnpy.event import Event, EventEngine +from vnpy.trader.engine import MainEngine +from vnpy.trader.ui import QtCore, QtWidgets +from vnpy.trader.event import EVENT_CONTRACT + +from ..engine import ( + APP_NAME, + EVENT_RECORDER_LOG, + EVENT_RECORDER_UPDATE +) + + +class RecorderManager(QtWidgets.QWidget): + """""" + + signal_log = QtCore.pyqtSignal(Event) + signal_update = QtCore.pyqtSignal(Event) + signal_contract = QtCore.pyqtSignal(Event) + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + super().__init__() + + self.main_engine = main_engine + self.event_engine = event_engine + self.recorder_engine = main_engine.get_engine(APP_NAME) + + self.init_ui() + self.register_event() + self.recorder_engine.put_event() + + def init_ui(self): + """""" + self.setWindowTitle("Tick行情记录") + self.resize(1000, 600) + + # Create widgets + self.symbol_line = QtWidgets.QLineEdit() + self.symbol_line.setFixedHeight( + self.symbol_line.sizeHint().height() * 2) + + contracts = self.main_engine.get_all_contracts() + self.vt_symbols = [contract.vt_symbol for contract in contracts] + + self.symbol_completer = QtWidgets.QCompleter(self.vt_symbols) + self.symbol_completer.setFilterMode(QtCore.Qt.MatchContains) + self.symbol_completer.setCompletionMode( + self.symbol_completer.PopupCompletion) + self.symbol_line.setCompleter(self.symbol_completer) + + add_tick_button = QtWidgets.QPushButton("添加") + add_tick_button.clicked.connect(self.add_tick_recording) + + remove_tick_button = QtWidgets.QPushButton("移除") + remove_tick_button.clicked.connect(self.remove_tick_recording) + + self.tick_recording_edit = QtWidgets.QTextEdit() + self.tick_recording_edit.setReadOnly(True) + + self.log_edit = QtWidgets.QTextEdit() + self.log_edit.setReadOnly(True) + + # Set layout + grid = QtWidgets.QGridLayout() + grid.addWidget(QtWidgets.QLabel("Tick记录"), 0, 0) + grid.addWidget(add_tick_button, 0, 1) + grid.addWidget(remove_tick_button, 0, 2) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(QtWidgets.QLabel("本地代码")) + hbox.addWidget(self.symbol_line) + hbox.addWidget(QtWidgets.QLabel(" ")) + hbox.addLayout(grid) + hbox.addStretch() + + grid2 = QtWidgets.QGridLayout() + grid2.addWidget(QtWidgets.QLabel("Tick记录列表"), 0, 0) + grid2.addWidget(self.tick_recording_edit, 0, 1) + + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox) + vbox.addLayout(grid2) + self.setLayout(vbox) + + def register_event(self): + """""" + self.signal_log.connect(self.process_log_event) + self.signal_contract.connect(self.process_contract_event) + self.signal_update.connect(self.process_update_event) + + self.event_engine.register(EVENT_CONTRACT, self.signal_contract.emit) + self.event_engine.register( + EVENT_RECORDER_LOG, self.signal_log.emit) + self.event_engine.register( + EVENT_RECORDER_UPDATE, self.signal_update.emit) + + def process_log_event(self, event: Event): + """""" + timestamp = datetime.now().strftime("%H:%M:%S") + msg = f"{timestamp}\t{event.data}" + self.log_edit.append(msg) + + def process_update_event(self, event: Event): + """""" + data = event.data + + self.tick_recording_edit.clear() + tick_text = "\n".join(data["tick"]) + self.tick_recording_edit.setText(tick_text) + + def process_contract_event(self, event: Event): + """""" + contract = event.data + self.vt_symbols.append(contract.vt_symbol) + + model = self.symbol_completer.model() + model.setStringList(self.vt_symbols) + + def add_tick_recording(self): + """""" + vt_symbol = self.symbol_line.text() + self.recorder_engine.add_tick_recording(vt_symbol) + + def remove_tick_recording(self): + """""" + vt_symbol = self.symbol_line.text() + self.recorder_engine.remove_tick_recording(vt_symbol)