From 5a9dfa747d4b6e1441d84deddc58386fd6111186 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sat, 14 Sep 2019 11:06:08 +0800
Subject: [PATCH 01/41] [Add] new spread trading app
---
vnpy/app/spread_trading/__init__.py | 17 +++++++++++++++++
vnpy/app/spread_trading/engine.py | 13 +++++++++++++
vnpy/app/spread_trading/ui/__init__.py | 1 +
vnpy/app/spread_trading/ui/spread.ico | Bin 0 -> 67646 bytes
vnpy/app/spread_trading/ui/widget.py | 25 +++++++++++++++++++++++++
5 files changed, 56 insertions(+)
create mode 100644 vnpy/app/spread_trading/__init__.py
create mode 100644 vnpy/app/spread_trading/engine.py
create mode 100644 vnpy/app/spread_trading/ui/__init__.py
create mode 100644 vnpy/app/spread_trading/ui/spread.ico
create mode 100644 vnpy/app/spread_trading/ui/widget.py
diff --git a/vnpy/app/spread_trading/__init__.py b/vnpy/app/spread_trading/__init__.py
new file mode 100644
index 00000000..7ff1bf13
--- /dev/null
+++ b/vnpy/app/spread_trading/__init__.py
@@ -0,0 +1,17 @@
+from pathlib import Path
+
+from vnpy.trader.app import BaseApp
+
+from .engine import SpreadEngine, APP_NAME
+
+
+class AlgoTradingApp(BaseApp):
+ """"""
+
+ app_name = APP_NAME
+ app_module = __module__
+ app_path = Path(__file__).parent
+ display_name = "价差交易"
+ engine_class = SpreadEngine
+ widget_name = "SpreadManager"
+ icon_name = "spread.ico"
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
new file mode 100644
index 00000000..1e4584b9
--- /dev/null
+++ b/vnpy/app/spread_trading/engine.py
@@ -0,0 +1,13 @@
+
+from vnpy.event import EventEngine, Event
+from vnpy.trader.engine import BaseEngine, MainEngine
+
+APP_NAME = "SpreadTrading"
+
+
+class SpreadEngine(BaseEngine):
+ """"""
+
+ def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
+ """Constructor"""
+ super().__init__(main_engine, event_engine, APP_NAME)
diff --git a/vnpy/app/spread_trading/ui/__init__.py b/vnpy/app/spread_trading/ui/__init__.py
new file mode 100644
index 00000000..c7639754
--- /dev/null
+++ b/vnpy/app/spread_trading/ui/__init__.py
@@ -0,0 +1 @@
+from .widget import SpreadManager
diff --git a/vnpy/app/spread_trading/ui/spread.ico b/vnpy/app/spread_trading/ui/spread.ico
new file mode 100644
index 0000000000000000000000000000000000000000..05b3d571bd19802b6fa8529759d887b4f1639811
GIT binary patch
literal 67646
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
Date: Sat, 14 Sep 2019 13:20:09 +0800
Subject: [PATCH 02/41] [Add] spread price and pos calculation
---
vnpy/app/spread_trading/base.py | 156 ++++++++++++++++++++++++++++++
vnpy/app/spread_trading/engine.py | 145 +++++++++++++++++++++++++++
2 files changed, 301 insertions(+)
create mode 100644 vnpy/app/spread_trading/base.py
diff --git a/vnpy/app/spread_trading/base.py b/vnpy/app/spread_trading/base.py
new file mode 100644
index 00000000..574dc9b9
--- /dev/null
+++ b/vnpy/app/spread_trading/base.py
@@ -0,0 +1,156 @@
+from typing import Dict, List
+from math import floor, ceil
+from datetime import datetime
+
+from vnpy.trader.object import TickData, PositionData
+from vnpy.trader.constant import Direction
+
+
+class LegData:
+ """"""
+
+ def __init__(
+ self,
+ vt_symbol: str,
+ price_multiplier: float,
+ trading_multiplier: float
+ ):
+ """"""
+ self.vt_symbol: str = vt_symbol
+
+ # For calculating spread price
+ self.price_multiplier: float = price_multiplier
+
+ # For calculating spread pos and sending orders
+ self.trading_multiplier: float = trading_multiplier
+
+ # Price and position data
+ self.bid_price: float = 0
+ self.ask_price: float = 0
+ self.bid_volume: float = 0
+ self.ask_volume: float = 0
+
+ self.long_pos: float = 0
+ self.short_pos: float = 0
+ self.net_pos: float = 0
+
+ def update_tick(self, tick: TickData):
+ """"""
+ self.bid_price = tick.bid_price_1
+ self.ask_price = tick.ask_price_1
+ self.bid_volume = tick.bid_volume_1
+ self.ask_volume = tick.ask_volume_1
+
+ def update_position(self, position: PositionData):
+ """"""
+ if position.direction == Direction.NET:
+ self.net_pos = position.volume
+ else:
+ if position.direction == Direction.LONG:
+ self.long_pos = position.volume
+ else:
+ self.short_pos = position.volume
+ self.net_pos = self.long_pos - self.short_pos
+
+
+class SpreadData:
+ """"""
+
+ def __init__(
+ self,
+ name: str,
+ legs: List[LegData],
+ active_symbol: str
+ ):
+ """"""
+ self.name: str = name
+
+ self.legs: Dict[str, LegData] = {}
+ self.active_leg: LegData = None
+ self.passive_legs: List[LegData] = []
+
+ for leg in legs:
+ self.legs[leg.vt_symbol] = leg
+ if leg.vt_symbol == active_symbol:
+ self.active_leg = leg
+ else:
+ self.passive_legs.append(leg)
+
+ # Spread data
+ self.bid_price: float = 0
+ self.ask_price: float = 0
+ self.bid_volume: float = 0
+ self.ask_volume: float = 0
+
+ self.net_pos: float = 0
+ self.datetime: datetime = None
+
+ def calculate_price(self):
+ """"""
+ self.clear_price()
+
+ # Go through all legs to calculate price
+ for n, leg in enumerate(self.legs.values()):
+ # Filter not all leg price data has been received
+ if not leg.bid_volume or not leg.ask_volume:
+ self.clear_price()
+ return
+
+ # Calculate price
+ if leg.price_multiplier > 0:
+ self.bid_price += leg.bid_price * leg.price_multiplier
+ self.ask_price += leg.ask_price * leg.price_multiplier
+ else:
+ self.bid_price += leg.ask_price * leg.price_multiplier
+ self.ask_price += leg.bid_price * leg.price_multiplier
+
+ # Calculate volume
+ if leg.trading_multiplier > 0:
+ adjusted_bid_volume = floor(
+ leg.bid_volume / leg.trading_multiplier)
+ adjusted_ask_volume = floor(
+ leg.ask_volume / leg.trading_multiplier)
+ else:
+ adjusted_bid_volume = floor(
+ leg.ask_volume / abs(leg.trading_multiplier))
+ adjusted_ask_volume = floor(
+ leg.bid_volume / abs(leg.trading_multiplier))
+
+ # For the first leg, just initialize
+ if not n:
+ self.bid_volume = adjusted_bid_volume
+ self.ask_volume = adjusted_ask_volume
+ # For following legs, use min value of each leg quoting volume
+ else:
+ self.bid_volume = min(self.bid_volume, adjusted_bid_volume)
+ self.ask_volume = min(self.ask_volume, adjusted_ask_volume)
+
+ # Update calculate time
+ self.datetime = datetime.now()
+
+ def calculate_pos(self):
+ """"""
+ self.net_pos = 0
+
+ for n, leg in enumerate(self.legs.values()):
+ adjusted_net_pos = leg.net_pos / leg.trading_multiplier
+
+ if adjusted_net_pos > 0:
+ adjusted_net_pos = floor(adjusted_net_pos)
+ else:
+ adjusted_net_pos = ceil(adjusted_net_pos)
+
+ if not n:
+ self.net_pos = adjusted_net_pos
+ else:
+ if adjusted_net_pos > 0:
+ self.net_pos = min(self.net_pos, adjusted_net_pos)
+ else:
+ self.net_pos = max(self.net_pos, adjusted_net_pos)
+
+ def clear_price(self):
+ """"""
+ self.bid_price = 0
+ self.ask_price = 0
+ self.bid_volume = 0
+ self.ask_volume = 0
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 1e4584b9..5ad45241 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -1,6 +1,13 @@
+from typing import List, Dict
+from collections import defaultdict
from vnpy.event import EventEngine, Event
from vnpy.trader.engine import BaseEngine, MainEngine
+from vnpy.trader.event import EVENT_TICK, EVENT_POSITION
+from vnpy.trader.utility import load_json, save_json
+
+from .base import LegData, SpreadData
+
APP_NAME = "SpreadTrading"
@@ -11,3 +18,141 @@ class SpreadEngine(BaseEngine):
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
"""Constructor"""
super().__init__(main_engine, event_engine, APP_NAME)
+
+ def write_log(self, msg: str):
+ """"""
+ pass
+
+
+class SpreadDataEngine:
+ """"""
+ setting_filename = "spread_trading_setting.json"
+
+ def __init__(self, spread_engine: SpreadEngine):
+ """"""
+ self.spread_engine: SpreadEngine = spread_engine
+ self.main_engine: MainEngine = spread_engine.main_engine
+ self.event_engine: EventEngine = spread_engine.event_engine
+
+ self.write_log = spread_engine.write_log
+
+ self.legs: Dict[str, LegData] = {} # vt_symbol: leg
+ self.spreads: Dict[str, SpreadData] = {} # name: spread
+ self.symbol_spread_map: Dict[str, List[SpreadData]] = defaultdict(list)
+
+ self.load_setting()
+ self.register_event()
+
+ def load_setting(self):
+ """"""
+ setting = load_json(self.setting_filename)
+
+ for spread_setting in setting:
+ self.add_spread(
+ spread_setting["name"],
+ spread_setting["leg_settings"],
+ spread_setting["active_symbol"],
+ save=False
+ )
+
+ def save_setting(self):
+ """"""
+ setting = []
+
+ for spread in self.spreads.values():
+ leg_settings = []
+ for leg in spread.legs:
+ leg_setting = {
+ "vt_symbol": leg.vt_symbol,
+ "price_multiplier": leg.price_multiplier,
+ "trading_multiplier": leg.trading_multiplier
+ }
+ leg_settings.append(leg_setting)
+
+ spread_setting = {
+ "name": spread.name,
+ "leg_settings": leg_settings,
+ "active_symbol": spread.active_leg.vt_symbol
+ }
+ setting.append(spread_setting)
+
+ save_json(self.setting_filename, setting)
+
+ def register_event(self):
+ """"""
+ self.event_engine.register(EVENT_TICK, self.process_tick_event)
+ self.event_engine.register(EVENT_POSITION, self.process_position_event)
+
+ def process_tick_event(self, event: Event):
+ """"""
+ tick = event.data
+
+ leg = self.legs.get(tick.vt_symbol, None)
+ if not leg:
+ return
+ leg.update_tick(tick)
+
+ for spread in self.symbol_spread_map[tick.vt_symbol]:
+ spread.calculate_price()
+
+ def process_position_event(self, event: Event):
+ """"""
+ position = event.data
+
+ leg = self.legs.get(position.vt_symbol, None)
+ if not leg:
+ return
+ leg.update_position(position)
+
+ for spread in self.symbol_spread_map[position.vt_symbol]:
+ spread.calculate_pos()
+
+ def add_spread(
+ self,
+ name: str,
+ leg_settings: List[Dict],
+ active_symbol: str,
+ save: bool = True
+ ):
+ """"""
+ if name in self.spreads:
+ self.write_log("价差创建失败,名称重复:{}".format(name))
+ return
+
+ legs: List[LegData] = []
+ for leg_setting in leg_settings:
+ vt_symbol = leg_setting["vt_symbol"]
+
+ leg = self.legs.get(vt_symbol, None)
+ if not leg:
+ leg = LegData(
+ vt_symbol,
+ leg_setting["price_multiplier"],
+ leg_setting["trading_multiplier"]
+ )
+ self.legs[vt_symbol] = leg
+
+ legs.append(leg)
+
+ spread = SpreadData(name, legs, active_symbol)
+ self.spreads[name] = spread
+
+ for leg in spread.legs:
+ self.symbol_spread_map[leg.vt_symbol].append(spread)
+
+ if save:
+ self.save_setting()
+
+ self.write_log("价差创建成功:{}".format(name))
+
+ def remove_spread(self, name: str):
+ """"""
+ if name not in self.spreads:
+ return
+
+ spread = self.spreads.pop(name)
+
+ for leg in spread.legs:
+ self.symbol_spread_map[leg.vt_symbol].remove(spread)
+
+ self.write_log("价差删除成功:{}".format(name))
From 0ee582a12efcda1b27f7df058641b9abb6161f4a Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sat, 14 Sep 2019 14:39:24 +0800
Subject: [PATCH 03/41] [Mod] move OffsetConverter module to vnpy.trader
---
vnpy/app/cta_strategy/engine.py | 2 +-
vnpy/{app/cta_strategy => trader}/converter.py | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename vnpy/{app/cta_strategy => trader}/converter.py (100%)
diff --git a/vnpy/app/cta_strategy/engine.py b/vnpy/app/cta_strategy/engine.py
index 5245faca..c4622fc2 100644
--- a/vnpy/app/cta_strategy/engine.py
+++ b/vnpy/app/cta_strategy/engine.py
@@ -38,6 +38,7 @@ from vnpy.trader.constant import (
from vnpy.trader.utility import load_json, save_json, extract_vt_symbol, round_to
from vnpy.trader.database import database_manager
from vnpy.trader.rqdata import rqdata_client
+from vnpy.trader.converter import OffsetConverter
from .base import (
APP_NAME,
@@ -50,7 +51,6 @@ from .base import (
STOPORDER_PREFIX
)
from .template import CtaTemplate
-from .converter import OffsetConverter
STOP_STATUS_MAP = {
diff --git a/vnpy/app/cta_strategy/converter.py b/vnpy/trader/converter.py
similarity index 100%
rename from vnpy/app/cta_strategy/converter.py
rename to vnpy/trader/converter.py
From 0f4402833db6852060ead54919070c111197c784 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sat, 14 Sep 2019 15:57:27 +0800
Subject: [PATCH 04/41] [Add] template for spreading algo
---
vnpy/app/spread_trading/engine.py | 41 ++++++
vnpy/app/spread_trading/template.py | 189 ++++++++++++++++++++++++++++
2 files changed, 230 insertions(+)
create mode 100644 vnpy/app/spread_trading/template.py
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 5ad45241..c1e9d84b 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -5,6 +5,7 @@ from vnpy.event import EventEngine, Event
from vnpy.trader.engine import BaseEngine, MainEngine
from vnpy.trader.event import EVENT_TICK, EVENT_POSITION
from vnpy.trader.utility import load_json, save_json
+from vnpy.trader.object import TickData, ContractData
from .base import LegData, SpreadData
@@ -156,3 +157,43 @@ class SpreadDataEngine:
self.symbol_spread_map[leg.vt_symbol].remove(spread)
self.write_log("价差删除成功:{}".format(name))
+
+
+class SpreadAlgoEngine:
+ """"""
+
+ def __init__(self, spread_engine: SpreadEngine):
+ """"""
+ self.spread_engine: SpreadEngine = spread_engine
+ self.main_engine: MainEngine = spread_engine.main_engine
+ self.event_engine: EventEngine = spread_engine.event_engine
+
+ self.write_log = spread_engine.write_log
+
+ def put_event(self, algo) -> None:
+ """"""
+ pass
+
+ def send_order(
+ self,
+ algo,
+ vt_symbol,
+ price,
+ volume,
+ direction,
+ offset
+ ) -> List[str]:
+ """"""
+ pass
+
+ def cancel_order(self, algo, vt_orderid) -> None:
+ """"""
+ pass
+
+ def get_tick(self, vt_symbol: str) -> TickData:
+ """"""
+ return self.main_engine.get_tick(vt_symbol)
+
+ def get_contract(self, vt_symbol: str) -> ContractData:
+ """"""
+ return self.main_engine.get_contract(vt_symbol)
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
new file mode 100644
index 00000000..16c81924
--- /dev/null
+++ b/vnpy/app/spread_trading/template.py
@@ -0,0 +1,189 @@
+
+from collections import defaultdict
+from typing import Dict, List
+from math import floor, ceil
+
+from vnpy.trader.object import TickData, TradeData, OrderData, ContractData
+from vnpy.trader.constant import Direction, Status, Offset
+from vnpy.trader.utility import virtual
+
+from .base import SpreadData
+from .engine import SpreadAlgoEngine
+
+
+class SpreadAlgoTemplate:
+ """
+ Template for writing spread trading algos.
+ """
+ algo_name = "AlgoTemplate"
+
+ def __init__(
+ self,
+ algo_engine: SpreadAlgoEngine,
+ algoid: str,
+ spread: SpreadData,
+ direction: Direction,
+ price: float,
+ volume: float,
+ payup: int
+ ):
+ """"""
+ self.algo_engine: SpreadAlgoEngine = algo_engine
+ self.algoid: str = algoid
+ self.spread: SpreadData = spread
+
+ self.direction: Direction = direction
+ self.price: float = price
+ self.volume: float = volume
+ self.payup: int = payup
+
+ if direction == Direction.LONG:
+ self.target = volume
+ else:
+ self.target = -volume
+
+ self.status: Status = Status.NOTTRADED
+ self.traded: float = 0
+
+ self.leg_traded: Dict[str, float] = defaultdict(int)
+ self.leg_orders: Dict[str, List[str]] = defaultdict[list]
+
+ def is_active(self):
+ """"""
+ if self.status not in [Status.CANCELLED, Status.ALLTRADED]:
+ return True
+ else:
+ return False
+
+ def stop(self):
+ """"""
+ if self.is_active():
+ self.cancel_leg_order()
+ self.status = Status.CANCELLED
+ self.put_event()
+
+ def update_tick(self, tick: TickData):
+ """"""
+ self.on_tick(tick)
+
+ def update_trade(self, trade: TradeData):
+ """"""
+ if trade.direction == Direction.LONG:
+ self.leg_traded[trade.vt_symbol] += trade.volume
+ else:
+ self.leg_traded[trade.vt_symbol] -= trade.volume
+
+ self.calculate_traded()
+
+ self.on_trade(trade)
+
+ def update_order(self, order: OrderData):
+ """"""
+ if not order.is_active():
+ self.leg_orders[order.vt_symbol].remove(order.vt_orderid)
+
+ self.on_order(order)
+
+ def update_timer(self):
+ """"""
+ self.on_timer()
+
+ def put_event(self):
+ """"""
+ self.algo_engine.put_event(self)
+
+ def write_log(self, msg: str):
+ """"""
+ self.algo_engine.write_log(msg)
+
+ def send_long_order(self, vt_symbol: str, price: float, volume: float):
+ """"""
+ self.send_order(vt_symbol, price, volume, Direction.LONG)
+
+ def send_short_order(self, vt_symbol: str, price: float, volume: float):
+ """"""
+ self.send_order(vt_symbol, price, volume, Direction.SHORT)
+
+ def send_order(
+ self,
+ vt_symbol: str,
+ price: float,
+ volume: float,
+ direction: Direction,
+ ):
+ """"""
+ vt_orderids = self.algo_engine.send_order(
+ self,
+ vt_symbol,
+ price,
+ volume,
+ direction,
+ )
+
+ self.leg_orders[vt_symbol].extend(vt_orderids)
+
+ def cancel_leg_order(self, vt_symbol: str):
+ """"""
+ for vt_orderid in self.leg_orders[vt_symbol]:
+ self.algo_engine.cancel_order(vt_orderid)
+
+ def cancel_all_order(self):
+ """"""
+ for vt_symbol in self.leg_orders.keys():
+ self.cancel_leg_order(vt_symbol)
+
+ def calculate_traded(self):
+ """"""
+ self.traded = 0
+
+ for n, leg in enumerate(self.spread.legs.values()):
+ leg_traded = self.leg_traded[leg.vt_symbol]
+ adjusted_leg_traded = leg_traded / leg.trading_multiplier
+
+ if adjusted_leg_traded > 0:
+ adjusted_leg_traded = floor(adjusted_leg_traded)
+ else:
+ adjusted_leg_traded = ceil(adjusted_leg_traded)
+
+ if not n:
+ self.traded = adjusted_leg_traded
+ else:
+ if adjusted_leg_traded > 0:
+ self.traded = min(self.traded, adjusted_leg_traded)
+ else:
+ self.traded = max(self.traded, adjusted_leg_traded)
+
+ if self.traded == self.target:
+ self.status = Status.ALLTRADED
+ elif not self.traded:
+ self.status = Status.NOTTRADED
+ else:
+ self.status = Status.PARTTRADED
+
+ def get_tick(self, vt_symbol: str) -> TickData:
+ """"""
+ return self.algo_engine.get_tick(vt_symbol)
+
+ def get_contract(self, vt_symbol: str) -> ContractData:
+ """"""
+ return self.algo_engine.get_contract(vt_symbol)
+
+ @virtual
+ def on_tick(self, tick: TickData):
+ """"""
+ pass
+
+ @virtual
+ def on_order(self, order: OrderData):
+ """"""
+ pass
+
+ @virtual
+ def on_trade(self, trade: TradeData):
+ """"""
+ pass
+
+ @virtual
+ def on_timer(self):
+ """"""
+ pass
From 0ad86c8638ad8d11a79379091d5712de37d06f8e Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sun, 15 Sep 2019 17:44:08 +0800
Subject: [PATCH 05/41] [Add] spread trading algo and engine
---
examples/vn_trader/run.py | 14 +-
vnpy/app/spread_trading/__init__.py | 2 +-
vnpy/app/spread_trading/algo.py | 132 +++++++++++++++
vnpy/app/spread_trading/base.py | 37 ++++
vnpy/app/spread_trading/engine.py | 245 ++++++++++++++++++++++++---
vnpy/app/spread_trading/template.py | 74 ++++++--
vnpy/app/spread_trading/ui/widget.py | 127 +++++++++++++-
vnpy/trader/ui/widget.py | 3 +
8 files changed, 591 insertions(+), 43 deletions(-)
create mode 100644 vnpy/app/spread_trading/algo.py
diff --git a/examples/vn_trader/run.py b/examples/vn_trader/run.py
index 74e3ca1f..f063a687 100644
--- a/examples/vn_trader/run.py
+++ b/examples/vn_trader/run.py
@@ -5,7 +5,7 @@ from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import MainWindow, create_qapp
# from vnpy.gateway.binance import BinanceGateway
-# from vnpy.gateway.bitmex import BitmexGateway
+from vnpy.gateway.bitmex import BitmexGateway
# from vnpy.gateway.futu import FutuGateway
# from vnpy.gateway.ib import IbGateway
# from vnpy.gateway.ctp import CtpGateway
@@ -38,6 +38,7 @@ from vnpy.app.cta_backtester import CtaBacktesterApp
# from vnpy.app.risk_manager import RiskManagerApp
from vnpy.app.script_trader import ScriptTraderApp
from vnpy.app.rpc_service import RpcServiceApp
+from vnpy.app.spread_trading import SpreadTradingApp
def main():
@@ -57,7 +58,7 @@ def main():
# main_engine.add_gateway(FemasGateway)
# main_engine.add_gateway(IbGateway)
# main_engine.add_gateway(FutuGateway)
- # main_engine.add_gateway(BitmexGateway)
+ main_engine.add_gateway(BitmexGateway)
# main_engine.add_gateway(TigerGateway)
# main_engine.add_gateway(OesGateway)
# main_engine.add_gateway(OkexGateway)
@@ -74,14 +75,15 @@ def main():
# main_engine.add_gateway(DaGateway)
main_engine.add_gateway(CoinbaseGateway)
- main_engine.add_app(CtaStrategyApp)
- main_engine.add_app(CtaBacktesterApp)
+ # main_engine.add_app(CtaStrategyApp)
+ # main_engine.add_app(CtaBacktesterApp)
# main_engine.add_app(CsvLoaderApp)
# main_engine.add_app(AlgoTradingApp)
# main_engine.add_app(DataRecorderApp)
# main_engine.add_app(RiskManagerApp)
- main_engine.add_app(ScriptTraderApp)
- main_engine.add_app(RpcServiceApp)
+ # main_engine.add_app(ScriptTraderApp)
+ # main_engine.add_app(RpcServiceApp)
+ main_engine.add_app(SpreadTradingApp)
main_window = MainWindow(main_engine, event_engine)
main_window.showMaximized()
diff --git a/vnpy/app/spread_trading/__init__.py b/vnpy/app/spread_trading/__init__.py
index 7ff1bf13..36ab7f9b 100644
--- a/vnpy/app/spread_trading/__init__.py
+++ b/vnpy/app/spread_trading/__init__.py
@@ -5,7 +5,7 @@ from vnpy.trader.app import BaseApp
from .engine import SpreadEngine, APP_NAME
-class AlgoTradingApp(BaseApp):
+class SpreadTradingApp(BaseApp):
""""""
app_name = APP_NAME
diff --git a/vnpy/app/spread_trading/algo.py b/vnpy/app/spread_trading/algo.py
new file mode 100644
index 00000000..120d9ae4
--- /dev/null
+++ b/vnpy/app/spread_trading/algo.py
@@ -0,0 +1,132 @@
+from typing import Any
+from math import floor, ceil
+
+from vnpy.trader.constant import Direction
+from vnpy.trader.object import (TickData, OrderData, TradeData)
+
+from .template import SpreadAlgoTemplate
+from .base import SpreadData
+
+
+class SpreadTakerAlgo(SpreadAlgoTemplate):
+ """"""
+ algo_name = "SpreadTaker"
+
+ def __init__(
+ self,
+ algo_engine: Any,
+ algoid: str,
+ spread: SpreadData,
+ direction: Direction,
+ price: float,
+ volume: float
+ ):
+ """"""
+ super().__init__(algo_engine, algoid, spread, direction, price, volume)
+
+ self.cancel_interval: int = 2
+ self.timer_count: int = 0
+
+ def on_tick(self, tick: TickData):
+ """"""
+ # Return if there are any existing orders
+ if not self.check_order_finished():
+ return
+
+ # Hedge if active leg is not fully hedged
+ if not self.check_hedge_finished():
+ self.hedge_passive_legs()
+ return
+
+ # Otherwise check if should take active leg
+ if self.direction == Direction.LONG:
+ if self.spread.ask_price <= self.price:
+ self.take_active_leg()
+ else:
+ if self.spread.bid_price >= self.price:
+ self.take_active_leg()
+
+ def on_order(self, order: OrderData):
+ """"""
+ # Only care active leg order update
+ if order.vt_symbol != self.spread.active_leg.vt_symbol:
+ return
+
+ # Do nothing if still any existing orders
+ if not self.check_order_finished():
+ return
+
+ # Hedge passive legs if necessary
+ if not self.check_hedge_finished():
+ self.hedge_passive_legs()
+
+ def on_trade(self, trade: TradeData):
+ """"""
+ pass
+
+ def on_interval(self):
+ """"""
+ if not self.check_order_finished():
+ self.cancel_all_order()
+
+ def take_active_leg(self):
+ """"""
+ # Calculate spread order volume of new round trade
+ spread_volume_left = self.target - self.traded
+
+ if self.direction == Direction.LONG:
+ spread_order_volume = self.spread.ask_volume
+ spread_order_volume = min(spread_order_volume, spread_volume_left)
+ else:
+ spread_order_volume = -self.spread.bid_volume
+ spread_order_volume = max(spread_order_volume, spread_volume_left)
+
+ # Calculate active leg order volume
+ leg_order_volume = self.spread.caculate_leg_volume(
+ self.spread.active_leg.vt_symbol,
+ spread_order_volume
+ )
+
+ # Send active leg order
+ self.send_leg_order(
+ self.spread.active_leg.vt_symbol,
+ leg_order_volume
+ )
+
+ def hedge_passive_legs(self):
+ """
+ Send orders to hedge all passive legs.
+ """
+ # Calcualte spread volume to hedge
+ active_leg = self.spread.active_leg
+ active_traded = self.leg_traded[active_leg.vt_symbol]
+
+ hedge_volume = self.spread.calculate_spread_volume(
+ active_leg.vt_symbol,
+ active_traded
+ )
+
+ # Calculate passive leg target volume and do hedge
+ for leg in self.spread.passive_legs:
+ passive_traded = self.leg_orders[leg.vt_symbol]
+ passive_target = self.spread.caculate_leg_volume(
+ leg.vt_symbol,
+ hedge_volume
+ )
+
+ leg_order_volume = passive_target - passive_traded
+ if leg_order_volume:
+ self.send_leg_order(leg.vt_symbol, leg_order_volume)
+
+ def send_leg_order(self, vt_symbol: str, leg_volume: float):
+ """"""
+ leg = self.spread.legs[vt_symbol]
+ leg_tick = self.get_tick(vt_symbol)
+ leg_contract = self.get_contract(vt_symbol)
+
+ if leg_volume > 0:
+ price = leg_tick.ask_price_1 + leg_contract.pricetick * self.payup
+ self.send_long_order(leg.vt_symbol, price, abs(leg_volume))
+ else:
+ price = leg_tick.bid_price_1 - leg_contract.pricetick * self.payup
+ self.send_short_order(leg.vt_symbol, price, abs(leg_volume))
diff --git a/vnpy/app/spread_trading/base.py b/vnpy/app/spread_trading/base.py
index 574dc9b9..aee999c4 100644
--- a/vnpy/app/spread_trading/base.py
+++ b/vnpy/app/spread_trading/base.py
@@ -6,6 +6,12 @@ from vnpy.trader.object import TickData, PositionData
from vnpy.trader.constant import Direction
+EVENT_SPREAD_DATA = "eSpreadData"
+EVENT_SPREAD_LOG = "eSpreadLog"
+EVENT_SPREAD_ALGO = "eSpreadAlgo"
+EVENT_SPREAD_STRATEGY = "eSpreadStrategy"
+
+
class LegData:
""""""
@@ -69,6 +75,9 @@ class SpreadData:
self.active_leg: LegData = None
self.passive_legs: List[LegData] = []
+ self.price_formula: str = ""
+ self.trading_formula: str = ""
+
for leg in legs:
self.legs[leg.vt_symbol] = leg
if leg.vt_symbol == active_symbol:
@@ -76,6 +85,16 @@ class SpreadData:
else:
self.passive_legs.append(leg)
+ if leg.price_multiplier > 0:
+ self.price_formula += f"+{leg.trading_multiplier}*{leg.vt_symbol}"
+ else:
+ self.price_formula += f"{leg.trading_multiplier}*{leg.vt_symbol}"
+
+ if leg.trading_multiplier > 0:
+ self.trading_formula += f"+{leg.trading_multiplier}*{leg.vt_symbol}"
+ else:
+ self.trading_formula += f"{leg.trading_multiplier}*{leg.vt_symbol}"
+
# Spread data
self.bid_price: float = 0
self.ask_price: float = 0
@@ -154,3 +173,21 @@ class SpreadData:
self.ask_price = 0
self.bid_volume = 0
self.ask_volume = 0
+
+ def calculate_leg_volume(self, vt_symbol: str, spread_volume: float) -> float:
+ """"""
+ leg = self.legs[vt_symbol]
+ leg_volume = spread_volume * leg.trading_multiplier
+ return leg_volume
+
+ def calculate_spread_volume(self, vt_symbol: str, leg_volume: float) -> float:
+ """"""
+ leg = self.legs[vt_symbol]
+ spread_volume = leg_volume / leg.trading_multiplier
+
+ if spread_volume > 0:
+ spread_volume = floor(spread_volume)
+ else:
+ spread_volume = ceil(spread_volume)
+
+ return spread_volume
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index c1e9d84b..411c54a4 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -1,13 +1,27 @@
from typing import List, Dict
from collections import defaultdict
+from copy import copy
from vnpy.event import EventEngine, Event
from vnpy.trader.engine import BaseEngine, MainEngine
-from vnpy.trader.event import EVENT_TICK, EVENT_POSITION
+from vnpy.trader.event import (
+ EVENT_TICK, EVENT_POSITION, EVENT_CONTRACT,
+ EVENT_ORDER, EVENT_TRADE, EVENT_TIMER
+)
from vnpy.trader.utility import load_json, save_json
-from vnpy.trader.object import TickData, ContractData
+from vnpy.trader.object import (
+ TickData, ContractData, LogData,
+ SubscribeRequest, OrderRequest, CancelRequest
+)
+from vnpy.trader.constant import Direction
-from .base import LegData, SpreadData
+from .base import (
+ LegData, SpreadData,
+ EVENT_SPREAD_DATA, EVENT_SPREAD_ALGO,
+ EVENT_SPREAD_LOG, EVENT_SPREAD_STRATEGY
+)
+from .template import SpreadAlgoTemplate
+from .algo import SpreadTakerAlgo
APP_NAME = "SpreadTrading"
@@ -20,9 +34,28 @@ class SpreadEngine(BaseEngine):
"""Constructor"""
super().__init__(main_engine, event_engine, APP_NAME)
+ self.active = False
+
+ self.data_engine: SpreadDataEngine = SpreadDataEngine(self)
+ self.algo_engine: SpreadAlgoEngine = SpreadAlgoEngine(self)
+
+ def start(self):
+ """"""
+ if self.active:
+ return
+ self.active = True
+
+ self.data_engine.start()
+ self.algo_engine.start()
+
def write_log(self, msg: str):
""""""
- pass
+ log = LogData(
+ msg=msg,
+ gateway_name=APP_NAME
+ )
+ event = Event(EVENT_SPREAD_LOG, log)
+ self.event_engine.put(event)
class SpreadDataEngine:
@@ -41,10 +74,33 @@ class SpreadDataEngine:
self.spreads: Dict[str, SpreadData] = {} # name: spread
self.symbol_spread_map: Dict[str, List[SpreadData]] = defaultdict(list)
+ def start(self):
+ """"""
self.load_setting()
self.register_event()
+ self.test()
- def load_setting(self):
+ self.write_log("价差数据引擎启动成功")
+
+ def test(self):
+ """"""
+ name = "test"
+ leg_settings = [
+ {
+ "vt_symbol": "XBTUSD.BITMEX",
+ "price_multiplier": 1,
+ "trading_multiplier": 1
+ },
+ {
+ "vt_symbol": "XBTZ19.BITMEX",
+ "price_multiplier": -1,
+ "trading_multiplier": -1
+ }
+ ]
+ active_symbol = "XBTUSD.BITMEX"
+ self.add_spread(name, leg_settings, active_symbol, True)
+
+ def load_setting(self) -> None:
""""""
setting = load_json(self.setting_filename)
@@ -56,13 +112,13 @@ class SpreadDataEngine:
save=False
)
- def save_setting(self):
+ def save_setting(self) -> None:
""""""
setting = []
for spread in self.spreads.values():
leg_settings = []
- for leg in spread.legs:
+ for leg in spread.legs.values():
leg_setting = {
"vt_symbol": leg.vt_symbol,
"price_multiplier": leg.price_multiplier,
@@ -79,12 +135,13 @@ class SpreadDataEngine:
save_json(self.setting_filename, setting)
- def register_event(self):
+ def register_event(self) -> None:
""""""
self.event_engine.register(EVENT_TICK, self.process_tick_event)
self.event_engine.register(EVENT_POSITION, self.process_position_event)
+ self.event_engine.register(EVENT_CONTRACT, self.process_contract_event)
- def process_tick_event(self, event: Event):
+ def process_tick_event(self, event: Event) -> None:
""""""
tick = event.data
@@ -96,7 +153,9 @@ class SpreadDataEngine:
for spread in self.symbol_spread_map[tick.vt_symbol]:
spread.calculate_price()
- def process_position_event(self, event: Event):
+ self.put_data_event(spread)
+
+ def process_position_event(self, event: Event) -> None:
""""""
position = event.data
@@ -108,13 +167,30 @@ class SpreadDataEngine:
for spread in self.symbol_spread_map[position.vt_symbol]:
spread.calculate_pos()
+ self.put_data_event(spread)
+
+ def process_contract_event(self, event: Event) -> None:
+ """"""
+ contract = event.data
+
+ if contract.vt_symbol in self.legs:
+ req = SubscribeRequest(
+ contract.symbol, contract.exchange
+ )
+ self.main_engine.subscribe(req, contract.gateway_name)
+
+ def put_data_event(self, spread: SpreadData) -> None:
+ """"""
+ event = Event(EVENT_SPREAD_DATA, spread)
+ self.event_engine.put(event)
+
def add_spread(
self,
name: str,
leg_settings: List[Dict],
active_symbol: str,
save: bool = True
- ):
+ ) -> None:
""""""
if name in self.spreads:
self.write_log("价差创建失败,名称重复:{}".format(name))
@@ -138,15 +214,16 @@ class SpreadDataEngine:
spread = SpreadData(name, legs, active_symbol)
self.spreads[name] = spread
- for leg in spread.legs:
+ for leg in spread.legs.values():
self.symbol_spread_map[leg.vt_symbol].append(spread)
if save:
self.save_setting()
self.write_log("价差创建成功:{}".format(name))
+ self.put_data_event(spread)
- def remove_spread(self, name: str):
+ def remove_spread(self, name: str) -> None:
""""""
if name not in self.spreads:
return
@@ -161,6 +238,7 @@ class SpreadDataEngine:
class SpreadAlgoEngine:
""""""
+ algo_class = SpreadTakerAlgo
def __init__(self, spread_engine: SpreadEngine):
""""""
@@ -170,23 +248,146 @@ class SpreadAlgoEngine:
self.write_log = spread_engine.write_log
- def put_event(self, algo) -> None:
+ self.spreads: Dict[str: SpreadData] = {}
+ self.algos: Dict[str: SpreadAlgoTemplate] = {}
+
+ self.order_algo_map: dict[str: SpreadAlgoTemplate] = {}
+ self.symbol_algo_map: dict[str: SpreadAlgoTemplate] = defaultdict(list)
+
+ self.algo_count: int = 0
+
+ def start(self):
""""""
- pass
+ self.register_event()
+
+ self.write_log("价差算法引擎启动成功")
+
+ def register_event(self):
+ """"""
+ 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_TIMER, self.process_timer_event)
+
+ def process_spread_event(self, event: Event):
+ """"""
+ spread: SpreadData = event.data
+ self.spreads[spread.name] = spread
+
+ def process_tick_event(self, event: Event):
+ """"""
+ tick = event.data
+ algos = self.symbol_algo_map[tick.vt_symbol]
+ if not algos:
+ return
+
+ buf = copy(algos)
+ for algo in buf:
+ if not algo.is_active():
+ algos.remove(algo)
+ else:
+ algo.update_tick(tick)
+
+ def process_order_event(self, event: Event):
+ """"""
+ order = event.data
+ algo = self.order_algo_map.get(order.vt_orderid, None)
+ if algo and algo.is_active():
+ algo.update_order(order)
+
+ def process_trade_event(self, event: Event):
+ """"""
+ trade = event.data
+ algo = self.order_algo_map.get(trade.vt_orderid, None)
+ if algo and algo.is_active():
+ algo.update_trade(trade)
+
+ def process_timer_event(self, event: Event):
+ """"""
+ buf = self.algos.values()
+
+ for algo in buf:
+ if not algo.is_active():
+ self.algos.pop(algo.algoid)
+ else:
+ algo.update_timer()
+
+ def start_algo(
+ self,
+ spread_name: str,
+ direction: Direction,
+ price: float,
+ volume: float,
+ payup: int,
+ interval: int
+ ) -> str:
+ # Find spread object
+ spread = self.spreads.get(spread_name, None)
+ if not spread:
+ self.write_log("创建价差算法失败,找不到价差:{}".format(spread_name))
+ return ""
+
+ # Generate algoid str
+ self.algo_count += 1
+ algo_count_str = str(self.algo_count).rjust(6, "0")
+ algoid = f"{self.algo_class.algo_name}_{algo_count_str}"
+
+ # Create algo object
+ algo = self.algo_class(
+ self,
+ algoid,
+ spread,
+ direction,
+ price,
+ volume,
+ payup,
+ interval
+ )
+ self.algos[algoid] = algo
+
+ # Generate map between vt_symbol and algo
+ for leg in spread.legs.values():
+ self.symbol_algo_map[leg.vt_symbol].append(algo)
+
+ # Put event to update GUI
+ self.put_algo_event(algo)
+
+ return algoid
+
+ def stop_algo(
+ self,
+ algoid: str
+ ):
+ """"""
+ algo = self.algos.get(algoid, None)
+ if not algo:
+ self.write_log("停止价差算法失败,找不到算法:{}".format(algoid))
+ return
+
+ algo.stop()
+
+ def put_algo_event(self, algo: SpreadAlgoTemplate) -> None:
+ """"""
+ event = Event(EVENT_SPREAD_ALGO, algo)
+ self.event_engine.put(event)
+
+ def write_algo_log(self, algo: SpreadAlgoTemplate, msg: str) -> None:
+ """"""
+ msg = f"{algo.algoid}:{msg}"
+ self.write_log(msg)
def send_order(
self,
- algo,
- vt_symbol,
- price,
- volume,
- direction,
- offset
+ algo: SpreadAlgoTemplate,
+ vt_symbol: str,
+ price: float,
+ volume: float,
+ direction: Direction,
) -> List[str]:
""""""
pass
- def cancel_order(self, algo, vt_orderid) -> None:
+ def cancel_order(self, algo: SpreadAlgoTemplate, vt_orderid: str) -> None:
""""""
pass
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index 16c81924..9d910677 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -4,11 +4,10 @@ from typing import Dict, List
from math import floor, ceil
from vnpy.trader.object import TickData, TradeData, OrderData, ContractData
-from vnpy.trader.constant import Direction, Status, Offset
+from vnpy.trader.constant import Direction, Status
from vnpy.trader.utility import virtual
from .base import SpreadData
-from .engine import SpreadAlgoEngine
class SpreadAlgoTemplate:
@@ -19,31 +18,37 @@ class SpreadAlgoTemplate:
def __init__(
self,
- algo_engine: SpreadAlgoEngine,
+ algo_engine,
algoid: str,
spread: SpreadData,
direction: Direction,
price: float,
volume: float,
- payup: int
+ payup: int,
+ interval: int
):
""""""
- self.algo_engine: SpreadAlgoEngine = algo_engine
+ self.algo_engine = algo_engine
self.algoid: str = algoid
+
self.spread: SpreadData = spread
+ self.spread_name: str = spread.name
self.direction: Direction = direction
self.price: float = price
self.volume: float = volume
self.payup: int = payup
+ self.interval = interval
if direction == Direction.LONG:
self.target = volume
else:
self.target = -volume
- self.status: Status = Status.NOTTRADED
- self.traded: float = 0
+ self.status: Status = Status.NOTTRADED # Algo status
+ self.count: int = 0 # Timer count
+ self.traded: float = 0 # Volume traded
+ self.traded_volume: float = 0 # Volume traded (Abs value)
self.leg_traded: Dict[str, float] = defaultdict(int)
self.leg_orders: Dict[str, List[str]] = defaultdict[list]
@@ -55,12 +60,50 @@ class SpreadAlgoTemplate:
else:
return False
+ def check_order_finished(self):
+ """"""
+ finished = True
+
+ for leg in self.spread.legs.values():
+ vt_orderids = self.leg_orders[leg.vt_symbol]
+
+ if vt_orderids:
+ finished = False
+ break
+
+ return finished
+
+ def check_hedge_finished(self):
+ """"""
+ active_symbol = self.spread.active_leg.vt_symbol
+ active_traded = self.leg_traded[active_symbol]
+
+ spread_volume = self.spread.calculate_spread_volume(
+ active_symbol, active_traded
+ )
+
+ finished = True
+
+ for leg in self.spread.passive_legs:
+ passive_symbol = leg.vt_symbol
+
+ leg_target = self.spread.calculate_leg_volume(
+ passive_symbol, spread_volume
+ )
+ leg_traded = self.leg_traded[passive_symbol]
+
+ if leg_traded != leg_target:
+ finished = False
+ break
+
+ return finished
+
def stop(self):
""""""
if self.is_active():
- self.cancel_leg_order()
+ self.cancel_all_order()
self.status = Status.CANCELLED
- self.put_event()
+ self.put_algo_event()
def update_tick(self, tick: TickData):
""""""
@@ -86,7 +129,12 @@ class SpreadAlgoTemplate:
def update_timer(self):
""""""
- self.on_timer()
+ self.count += 1
+ if self.count < self.interval:
+ return
+ self.count = 0
+
+ self.on_interval()
def put_event(self):
""""""
@@ -94,7 +142,7 @@ class SpreadAlgoTemplate:
def write_log(self, msg: str):
""""""
- self.algo_engine.write_log(msg)
+ self.algo_engine.write_algo_log(msg)
def send_long_order(self, vt_symbol: str, price: float, volume: float):
""""""
@@ -153,6 +201,8 @@ class SpreadAlgoTemplate:
else:
self.traded = max(self.traded, adjusted_leg_traded)
+ self.traded_volume = abs(self.traded)
+
if self.traded == self.target:
self.status = Status.ALLTRADED
elif not self.traded:
@@ -184,6 +234,6 @@ class SpreadAlgoTemplate:
pass
@virtual
- def on_timer(self):
+ def on_interval(self):
""""""
pass
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index ced5317c..6519957c 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -5,11 +5,21 @@ Widget for spread trading.
from vnpy.event import EventEngine, Event
from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import QtWidgets, QtCore
+from vnpy.trader.ui.widget import (
+ BaseMonitor, BaseCell,
+ BidCell, AskCell,
+ TimeCell, MsgCell,
+ PnlCell, DirectionCell,
+ EnumCell,
+)
from ..engine import (
- AlgoEngine,
- AlgoTemplate,
+ SpreadEngine,
APP_NAME,
+ EVENT_SPREAD_DATA,
+ EVENT_SPREAD_LOG,
+ EVENT_SPREAD_ALGO,
+ EVENT_SPREAD_STRATEGY
)
@@ -23,3 +33,116 @@ class SpreadManager(QtWidgets.QWidget):
self.main_engine = main_engine
self.event_engine = event_engine
self.spread_engine = main_engine.get_engine(APP_NAME)
+
+ self.init_ui()
+
+ def init_ui(self):
+ """"""
+ self.setWindowTitle("价差交易")
+
+ self.data_monitor = SpreadDataMonitor(
+ self.main_engine,
+ self.event_engine
+ )
+ self.log_monitor = SpreadLogMonitor(
+ self.main_engine,
+ self.event_engine
+ )
+ self.algo_monitor = SpreadAlgoMonitor(
+ self.main_engine,
+ self.event_engine
+ )
+
+ vbox = QtWidgets.QVBoxLayout()
+ vbox.addWidget(self.data_monitor)
+ vbox.addWidget(self.log_monitor)
+
+ hbox = QtWidgets.QHBoxLayout()
+ hbox.addLayout(vbox)
+ hbox.addWidget(self.algo_monitor)
+
+ self.setLayout(hbox)
+
+ def show(self):
+ """"""
+ self.spread_engine.start()
+
+ self.showMaximized()
+
+
+class SpreadDataMonitor(BaseMonitor):
+ """
+ Monitor for spread data.
+ """
+
+ event_type = EVENT_SPREAD_DATA
+ data_key = "name"
+ sorting = False
+
+ headers = {
+ "name": {"display": "名称", "cell": BaseCell, "update": False},
+ "price_formula": {"display": "定价", "cell": BaseCell, "update": False},
+ "trading_formula": {"display": "交易", "cell": BaseCell, "update": False},
+ "bid_volume": {"display": "买量", "cell": BidCell, "update": True},
+ "bid_price": {"display": "买价", "cell": BidCell, "update": True},
+ "ask_price": {"display": "卖价", "cell": AskCell, "update": True},
+ "ask_volume": {"display": "卖量", "cell": AskCell, "update": True},
+ "net_pos": {"display": "净仓", "cell": PnlCell, "update": True},
+ "datetime": {"display": "时间", "cell": TimeCell, "update": True},
+ }
+
+
+class SpreadLogMonitor(QtWidgets.QTextEdit):
+ """
+ Monitor for log data.
+ """
+ signal = 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.init_ui()
+ self.register_event()
+
+ def init_ui(self):
+ """"""
+ self.setReadOnly(True)
+
+ def register_event(self):
+ """"""
+ self.signal.connect(self.process_log_event)
+
+ self.event_engine.register(EVENT_SPREAD_LOG, self.signal.emit)
+
+ def process_log_event(self, event: Event):
+ """"""
+ log = event.data
+ msg = f"{log.time}:{log.msg}"
+ self.append(msg)
+
+
+class SpreadAlgoMonitor(BaseMonitor):
+ """
+ Monitor for algo status.
+ """
+
+ event_type = EVENT_SPREAD_ALGO
+ data_key = "algoid"
+ sorting = False
+
+ headers = {
+ "algoid": {"display": "算法", "cell": BaseCell, "update": False},
+ "spread_name": {"display": "价差", "cell": BaseCell, "update": False},
+ "direction": {"display": "方向", "cell": EnumCell, "update": False},
+ "price": {"display": "价格", "cell": BaseCell, "update": False},
+ "payup": {"display": "超价", "cell": BaseCell, "update": False},
+ "volume": {"display": "数量", "cell": BaseCell, "update": False},
+ "traded_volume": {"display": "成交", "cell": BaseCell, "update": True},
+ "interval": {"display": "间隔", "cell": BaseCell, "update": False},
+ "count": {"display": "计数", "cell": BaseCell, "update": True},
+ "status": {"display": "状态", "cell": EnumCell, "update": True},
+ }
diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py
index ad8c0297..78f860dd 100644
--- a/vnpy/trader/ui/widget.py
+++ b/vnpy/trader/ui/widget.py
@@ -156,6 +156,9 @@ class TimeCell(BaseCell):
"""
Time format is 12:12:12.5
"""
+ if content is None:
+ return
+
timestamp = content.strftime("%H:%M:%S")
millisecond = int(content.microsecond / 1000)
From 2e4776eceb89259064caefe91190d17af4bbe53b Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sun, 15 Sep 2019 20:53:18 +0800
Subject: [PATCH 06/41] [Add] dialog ui for starting algo
---
vnpy/app/spread_trading/algo.py | 9 ++-
vnpy/app/spread_trading/engine.py | 7 +++
vnpy/app/spread_trading/template.py | 2 +-
vnpy/app/spread_trading/ui/widget.py | 85 ++++++++++++++++++++++++++--
4 files changed, 95 insertions(+), 8 deletions(-)
diff --git a/vnpy/app/spread_trading/algo.py b/vnpy/app/spread_trading/algo.py
index 120d9ae4..39ca2ecf 100644
--- a/vnpy/app/spread_trading/algo.py
+++ b/vnpy/app/spread_trading/algo.py
@@ -19,10 +19,15 @@ class SpreadTakerAlgo(SpreadAlgoTemplate):
spread: SpreadData,
direction: Direction,
price: float,
- volume: float
+ volume: float,
+ payup: int,
+ interval: int
):
""""""
- super().__init__(algo_engine, algoid, spread, direction, price, volume)
+ super().__init__(
+ algo_engine, algoid, spread, direction,
+ price, volume, payup, interval
+ )
self.cancel_interval: int = 2
self.timer_count: int = 0
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 411c54a4..f59739a7 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -39,6 +39,12 @@ class SpreadEngine(BaseEngine):
self.data_engine: SpreadDataEngine = SpreadDataEngine(self)
self.algo_engine: SpreadAlgoEngine = SpreadAlgoEngine(self)
+ self.add_spread = self.data_engine.add_spread
+ self.remove_spread = self.data_engine.remove_spread
+
+ self.start_algo = self.algo_engine.start_algo
+ self.stop_algo = self.algo_engine.stop_algo
+
def start(self):
""""""
if self.active:
@@ -268,6 +274,7 @@ class SpreadAlgoEngine:
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_TIMER, self.process_timer_event)
+ self.event_engine.register(EVENT_SPREAD_DATA, self.process_spread_event)
def process_spread_event(self, event: Event):
""""""
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index 9d910677..8ab76697 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -51,7 +51,7 @@ class SpreadAlgoTemplate:
self.traded_volume: float = 0 # Volume traded (Abs value)
self.leg_traded: Dict[str, float] = defaultdict(int)
- self.leg_orders: Dict[str, List[str]] = defaultdict[list]
+ self.leg_orders: Dict[str, List[str]] = defaultdict(list)
def is_active(self):
""""""
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index 6519957c..e42dff6a 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -4,7 +4,8 @@ Widget for spread trading.
from vnpy.event import EventEngine, Event
from vnpy.trader.engine import MainEngine
-from vnpy.trader.ui import QtWidgets, QtCore
+from vnpy.trader.constant import Direction
+from vnpy.trader.ui import QtWidgets, QtCore, QtGui
from vnpy.trader.ui.widget import (
BaseMonitor, BaseCell,
BidCell, AskCell,
@@ -40,6 +41,8 @@ class SpreadManager(QtWidgets.QWidget):
""""""
self.setWindowTitle("价差交易")
+ self.algo_dialog = SpreadAlgoDialog(self.spread_engine)
+
self.data_monitor = SpreadDataMonitor(
self.main_engine,
self.event_engine
@@ -53,12 +56,17 @@ class SpreadManager(QtWidgets.QWidget):
self.event_engine
)
- vbox = QtWidgets.QVBoxLayout()
- vbox.addWidget(self.data_monitor)
- vbox.addWidget(self.log_monitor)
+ vbox1 = QtWidgets.QVBoxLayout()
+ vbox1.addWidget(self.algo_dialog)
+ vbox1.addStretch()
+
+ vbox2 = QtWidgets.QVBoxLayout()
+ vbox2.addWidget(self.data_monitor)
+ vbox2.addWidget(self.log_monitor)
hbox = QtWidgets.QHBoxLayout()
- hbox.addLayout(vbox)
+ hbox.addLayout(vbox1)
+ hbox.addLayout(vbox2)
hbox.addWidget(self.algo_monitor)
self.setLayout(hbox)
@@ -146,3 +154,70 @@ class SpreadAlgoMonitor(BaseMonitor):
"count": {"display": "计数", "cell": BaseCell, "update": True},
"status": {"display": "状态", "cell": EnumCell, "update": True},
}
+
+
+class SpreadAlgoDialog(QtWidgets.QDialog):
+ """"""
+
+ def __init__(self, spread_engine: SpreadEngine):
+ """"""
+ super().__init__()
+
+ self.spread_engine: SpreadEngine = spread_engine
+
+ self.init_ui()
+
+ def init_ui(self):
+ """"""
+ self.setWindowTitle("启动算法")
+
+ self.name_line = QtWidgets.QLineEdit()
+
+ self.direction_combo = QtWidgets.QComboBox()
+ self.direction_combo.addItems(
+ [Direction.LONG.value, Direction.SHORT.value]
+ )
+
+ float_validator = QtGui.QDoubleValidator()
+ float_validator.setBottom(0)
+
+ self.price_line = QtWidgets.QLineEdit()
+ self.price_line.setValidator(float_validator)
+
+ self.volume_line = QtWidgets.QLineEdit()
+ self.volume_line.setValidator(float_validator)
+
+ int_validator = QtGui.QIntValidator()
+
+ self.payup_line = QtWidgets.QLineEdit()
+ self.payup_line.setValidator(int_validator)
+
+ self.interval_line = QtWidgets.QLineEdit()
+ self.interval_line.setValidator(int_validator)
+
+ button_start = QtWidgets.QPushButton("启动")
+ button_start.clicked.connect(self.start_algo)
+
+ form = QtWidgets.QFormLayout()
+ form.addRow("价差", self.name_line)
+ form.addRow("方向", self.direction_combo)
+ form.addRow("价格", self.price_line)
+ form.addRow("数量", self.volume_line)
+ form.addRow("超价", self.payup_line)
+ form.addRow("间隔", self.interval_line)
+ form.addRow(button_start)
+
+ self.setLayout(form)
+
+ def start_algo(self):
+ """"""
+ name = self.name_line.text()
+ direction = Direction(self.direction_combo.currentText())
+ price = float(self.price_line.text())
+ volume = float(self.volume_line.text())
+ payup = int(self.payup_line.text())
+ interval = int(self.interval_line.text())
+
+ self.spread_engine.start_algo(
+ name, direction, price, volume, payup, interval
+ )
From b4081eb01e78aba14ae6ed025bd4c2c324336592 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sun, 15 Sep 2019 21:34:47 +0800
Subject: [PATCH 07/41] [Add] dialog ui for creating new spread
---
vnpy/app/spread_trading/ui/widget.py | 131 +++++++++++++++++++++++++++
1 file changed, 131 insertions(+)
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index e42dff6a..b1b797ad 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -56,9 +56,13 @@ class SpreadManager(QtWidgets.QWidget):
self.event_engine
)
+ add_spread_button = QtWidgets.QPushButton("创建价差")
+ add_spread_button.clicked.connect(self.add_spread)
+
vbox1 = QtWidgets.QVBoxLayout()
vbox1.addWidget(self.algo_dialog)
vbox1.addStretch()
+ vbox1.addWidget(add_spread_button)
vbox2 = QtWidgets.QVBoxLayout()
vbox2.addWidget(self.data_monitor)
@@ -77,6 +81,11 @@ class SpreadManager(QtWidgets.QWidget):
self.showMaximized()
+ def add_spread(self):
+ """"""
+ dialog = SpreadDataDialog(self.spread_engine)
+ dialog.exec_()
+
class SpreadDataMonitor(BaseMonitor):
"""
@@ -221,3 +230,125 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
self.spread_engine.start_algo(
name, direction, price, volume, payup, interval
)
+
+
+class SpreadDataDialog(QtWidgets.QDialog):
+ """"""
+
+ def __init__(self, spread_engine: SpreadEngine):
+ """"""
+ super().__init__()
+
+ self.spread_engine: SpreadEngine = spread_engine
+
+ self.leg_widgets = []
+
+ self.init_ui()
+
+ def init_ui(self):
+ """"""
+ self.setWindowTitle("创建价差")
+
+ self.name_line = QtWidgets.QLineEdit()
+ self.active_line = QtWidgets.QLineEdit()
+
+ self.grid = QtWidgets.QGridLayout()
+
+ button_add = QtWidgets.QPushButton("创建价差")
+ button_add.clicked.connect(self.add_spread)
+
+ Label = QtWidgets.QLabel
+
+ grid = QtWidgets.QGridLayout()
+ grid.addWidget(Label("价差名称"), 0, 0)
+ grid.addWidget(self.name_line, 0, 1, 1, 3)
+ grid.addWidget(Label("主动腿代码"), 1, 0)
+ grid.addWidget(self.active_line, 1, 1, 1, 3)
+
+ grid.addWidget(Label(""), 2, 0)
+ grid.addWidget(Label("本地代码"), 3, 1)
+ grid.addWidget(Label("价格乘数"), 3, 2)
+ grid.addWidget(Label("交易乘数"), 3, 3)
+
+ int_validator = QtGui.QIntValidator()
+
+ leg_count = 5
+ for i in range(leg_count):
+ symbol_line = QtWidgets.QLineEdit()
+
+ price_line = QtWidgets.QLineEdit()
+ price_line.setValidator(int_validator)
+
+ trading_line = QtWidgets.QLineEdit()
+ trading_line.setValidator(int_validator)
+
+ grid.addWidget(Label("腿{}".format(i + 1)), 4 + i, 0)
+ grid.addWidget(symbol_line, 4 + i, 1)
+ grid.addWidget(price_line, 4 + i, 2)
+ grid.addWidget(trading_line, 4 + i, 3)
+
+ d = {
+ "symbol": symbol_line,
+ "price": price_line,
+ "trading": trading_line
+ }
+ self.leg_widgets.append(d)
+
+ grid.addWidget(Label(""), 4 + leg_count, 0,)
+ grid.addWidget(button_add, 5 + leg_count, 0, 1, 4)
+
+ self.setLayout(grid)
+
+ def add_spread(self):
+ """"""
+ spread_name = self.name_line.text()
+ if not spread_name:
+ QtWidgets.QMessageBox.warning(
+ self,
+ "创建失败",
+ "请输入价差名称",
+ QtWidgets.QMessageBox.Ok
+ )
+ return
+
+ active_symbol = self.active_line.text()
+
+ leg_settings = {}
+ for d in self.leg_widgets:
+ try:
+ vt_symbol = d["symbol"].text()
+ price_multiplier = int(d["price"].text())
+ trading_multiplier = int(d["trading"].text())
+
+ leg_settings[vt_symbol] = {
+ "vt_symbol": vt_symbol,
+ "price_multiplier": price_multiplier,
+ "trading_multiplier": trading_multiplier
+ }
+ except ValueError:
+ pass
+
+ if len(leg_settings) < 2:
+ QtWidgets.QMessageBox.warning(
+ self,
+ "创建失败",
+ "价差最少需要2条腿",
+ QtWidgets.QMessageBox.Ok
+ )
+ return
+
+ if active_symbol not in leg_settings:
+ QtWidgets.QMessageBox.warning(
+ self,
+ "创建失败",
+ "各条腿中找不到主动腿代码",
+ QtWidgets.QMessageBox.Ok
+ )
+ return
+
+ self.spread_engine.add_spread(
+ spread_name,
+ list(leg_settings.values()),
+ active_symbol
+ )
+ self.accept()
From d4cab7bfc2a9ebf2fc45ba76358a18094cc71bf7 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sun, 15 Sep 2019 21:46:49 +0800
Subject: [PATCH 08/41] [Add] stop algo by double click monitor cells
---
vnpy/app/spread_trading/engine.py | 5 +++--
vnpy/app/spread_trading/template.py | 4 ++--
vnpy/app/spread_trading/ui/widget.py | 25 +++++++++++++++++++++++--
3 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index f59739a7..8bcfb831 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -274,7 +274,8 @@ class SpreadAlgoEngine:
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_TIMER, self.process_timer_event)
- self.event_engine.register(EVENT_SPREAD_DATA, self.process_spread_event)
+ self.event_engine.register(
+ EVENT_SPREAD_DATA, self.process_spread_event)
def process_spread_event(self, event: Event):
""""""
@@ -311,7 +312,7 @@ class SpreadAlgoEngine:
def process_timer_event(self, event: Event):
""""""
- buf = self.algos.values()
+ buf = list(self.algos.values())
for algo in buf:
if not algo.is_active():
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index 8ab76697..6c967d91 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -103,7 +103,7 @@ class SpreadAlgoTemplate:
if self.is_active():
self.cancel_all_order()
self.status = Status.CANCELLED
- self.put_algo_event()
+ self.put_event()
def update_tick(self, tick: TickData):
""""""
@@ -138,7 +138,7 @@ class SpreadAlgoTemplate:
def put_event(self):
""""""
- self.algo_engine.put_event(self)
+ self.algo_engine.put_algo_event(self)
def write_log(self, msg: str):
""""""
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index b1b797ad..ebfe8977 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -52,8 +52,7 @@ class SpreadManager(QtWidgets.QWidget):
self.event_engine
)
self.algo_monitor = SpreadAlgoMonitor(
- self.main_engine,
- self.event_engine
+ self.spread_engine
)
add_spread_button = QtWidgets.QPushButton("创建价差")
@@ -164,6 +163,28 @@ class SpreadAlgoMonitor(BaseMonitor):
"status": {"display": "状态", "cell": EnumCell, "update": True},
}
+ def __init__(self, spread_engine: SpreadEngine):
+ """"""
+ super().__init__(spread_engine.main_engine, spread_engine.event_engine)
+
+ self.spread_engine = spread_engine
+
+ def init_ui(self):
+ """
+ Connect signal.
+ """
+ super().init_ui()
+
+ self.setToolTip("双击单元格停止算法")
+ self.itemDoubleClicked.connect(self.stop_algo)
+
+ def stop_algo(self, cell):
+ """
+ Stop algo if cell double clicked.
+ """
+ algo = cell.get_data()
+ self.spread_engine.stop_algo(algo.algoid)
+
class SpreadAlgoDialog(QtWidgets.QDialog):
""""""
From f94144b704c07908bba133cec33aaa690abf2abf Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Sun, 15 Sep 2019 23:53:49 +0800
Subject: [PATCH 09/41] [Mod] remove test function
---
vnpy/app/spread_trading/algo.py | 4 ++--
vnpy/app/spread_trading/engine.py | 19 -------------------
vnpy/app/spread_trading/ui/widget.py | 1 -
3 files changed, 2 insertions(+), 22 deletions(-)
diff --git a/vnpy/app/spread_trading/algo.py b/vnpy/app/spread_trading/algo.py
index 39ca2ecf..9e0066ed 100644
--- a/vnpy/app/spread_trading/algo.py
+++ b/vnpy/app/spread_trading/algo.py
@@ -87,7 +87,7 @@ class SpreadTakerAlgo(SpreadAlgoTemplate):
spread_order_volume = max(spread_order_volume, spread_volume_left)
# Calculate active leg order volume
- leg_order_volume = self.spread.caculate_leg_volume(
+ leg_order_volume = self.spread.calculate_leg_volume(
self.spread.active_leg.vt_symbol,
spread_order_volume
)
@@ -114,7 +114,7 @@ class SpreadTakerAlgo(SpreadAlgoTemplate):
# Calculate passive leg target volume and do hedge
for leg in self.spread.passive_legs:
passive_traded = self.leg_orders[leg.vt_symbol]
- passive_target = self.spread.caculate_leg_volume(
+ passive_target = self.spread.calculate_leg_volume(
leg.vt_symbol,
hedge_volume
)
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 8bcfb831..42690280 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -84,28 +84,9 @@ class SpreadDataEngine:
""""""
self.load_setting()
self.register_event()
- self.test()
self.write_log("价差数据引擎启动成功")
- def test(self):
- """"""
- name = "test"
- leg_settings = [
- {
- "vt_symbol": "XBTUSD.BITMEX",
- "price_multiplier": 1,
- "trading_multiplier": 1
- },
- {
- "vt_symbol": "XBTZ19.BITMEX",
- "price_multiplier": -1,
- "trading_multiplier": -1
- }
- ]
- active_symbol = "XBTUSD.BITMEX"
- self.add_spread(name, leg_settings, active_symbol, True)
-
def load_setting(self) -> None:
""""""
setting = load_json(self.setting_filename)
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index ebfe8977..062e6221 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -209,7 +209,6 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
)
float_validator = QtGui.QDoubleValidator()
- float_validator.setBottom(0)
self.price_line = QtWidgets.QLineEdit()
self.price_line.setValidator(float_validator)
From be9142e878ca4328a14917ebb9aa619adb32310b Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Mon, 16 Sep 2019 14:51:57 +0800
Subject: [PATCH 10/41] [Mod] add send/cancel order function of algo engine
---
vnpy/app/spread_trading/algo.py | 5 +-
vnpy/app/spread_trading/engine.py | 93 ++++++++++++++++++++++++++--
vnpy/app/spread_trading/template.py | 5 +-
vnpy/app/spread_trading/ui/widget.py | 14 ++++-
4 files changed, 108 insertions(+), 9 deletions(-)
diff --git a/vnpy/app/spread_trading/algo.py b/vnpy/app/spread_trading/algo.py
index 9e0066ed..13b3e968 100644
--- a/vnpy/app/spread_trading/algo.py
+++ b/vnpy/app/spread_trading/algo.py
@@ -21,12 +21,13 @@ class SpreadTakerAlgo(SpreadAlgoTemplate):
price: float,
volume: float,
payup: int,
- interval: int
+ interval: int,
+ lock: bool
):
""""""
super().__init__(
algo_engine, algoid, spread, direction,
- price, volume, payup, interval
+ price, volume, payup, interval, lock
)
self.cancel_interval: int = 2
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 42690280..e12afeb2 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -1,4 +1,4 @@
-from typing import List, Dict
+from typing import List, Dict, Set
from collections import defaultdict
from copy import copy
@@ -13,7 +13,8 @@ from vnpy.trader.object import (
TickData, ContractData, LogData,
SubscribeRequest, OrderRequest, CancelRequest
)
-from vnpy.trader.constant import Direction
+from vnpy.trader.constant import Direction, Offset, OrderType
+from vnpy.trader.converter import OffsetConverter, PositionHolding
from .base import (
LegData, SpreadData,
@@ -242,6 +243,11 @@ class SpreadAlgoEngine:
self.symbol_algo_map: dict[str: SpreadAlgoTemplate] = defaultdict(list)
self.algo_count: int = 0
+ self.vt_tradeids: Set = set()
+
+ self.offset_converter: OffsetConverter = OffsetConverter(
+ self.main_engine
+ )
def start(self):
""""""
@@ -254,9 +260,11 @@ class SpreadAlgoEngine:
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_TIMER, self.process_timer_event)
self.event_engine.register(
- EVENT_SPREAD_DATA, self.process_spread_event)
+ EVENT_SPREAD_DATA, self.process_spread_event
+ )
def process_spread_event(self, event: Event):
""""""
@@ -280,6 +288,9 @@ class SpreadAlgoEngine:
def process_order_event(self, event: Event):
""""""
order = event.data
+
+ self.offset_converter.update_order(order)
+
algo = self.order_algo_map.get(order.vt_orderid, None)
if algo and algo.is_active():
algo.update_order(order)
@@ -287,10 +298,24 @@ class SpreadAlgoEngine:
def process_trade_event(self, event: Event):
""""""
trade = event.data
+
+ # Filter duplicate trade push
+ if trade.vt_tradeid in self.vt_tradeids:
+ return
+ self.vt_tradeids.add(trade.vt_tradeid)
+
+ self.offset_converter.update_trade(trade)
+
algo = self.order_algo_map.get(trade.vt_orderid, None)
if algo and algo.is_active():
algo.update_trade(trade)
+ def process_position_event(self, event: Event):
+ """"""
+ position = event.data
+
+ self.offset_converter.update_position(position)
+
def process_timer_event(self, event: Event):
""""""
buf = list(self.algos.values())
@@ -372,13 +397,71 @@ class SpreadAlgoEngine:
price: float,
volume: float,
direction: Direction,
+ lock: bool
) -> List[str]:
""""""
- pass
+ holding = self.offset_converter.get_position_holding(vt_symbol)
+ contract = self.main_engine.get_contract(vt_symbol)
+
+ if direction == Direction.LONG:
+ available = holding.short_pos - holding.short_pos_frozen
+ else:
+ available = holding.long_pos - holding.long_pos_frozen
+
+ # If no position to close, just open new
+ if not available:
+ offset = Offset.OPEN
+ # If enougth position to close, just close old
+ elif volume < available:
+ offset = Offset.CLOSE
+ # Otherwise, just close existing position
+ else:
+ volume = available
+ offset = Offset.CLOSE
+
+ original_req = OrderRequest(
+ contract.symbol,
+ contract.exchange,
+ direction,
+ offset,
+ OrderType.LIMIT,
+ price,
+ volume
+ )
+
+ # Convert with offset converter
+ req_list = self.offset_converter.convert_order_request(
+ original_req, lock)
+
+ # Send Orders
+ vt_orderids = []
+
+ for req in req_list:
+ vt_orderid = self.main_engine.send_order(
+ req, contract.gateway_name)
+
+ # Check if sending order successful
+ if not vt_orderid:
+ continue
+
+ vt_orderids.append(vt_orderid)
+
+ self.offset_converter.update_order_request(req, vt_orderid)
+
+ # Save relationship between orderid and algo.
+ self.order_algo_map[vt_orderid] = algo
+
+ return vt_orderids
def cancel_order(self, algo: SpreadAlgoTemplate, vt_orderid: str) -> None:
""""""
- pass
+ order = self.main_engine.get_order(vt_orderid)
+ if not order:
+ self.write_algo_log(algo, "撤单失败,找不到委托{}".format(vt_orderid))
+ return
+
+ req = order.create_cancel_request()
+ self.main_engine.cancel_order(req, order.gateway_name)
def get_tick(self, vt_symbol: str) -> TickData:
""""""
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index 6c967d91..0a0767ec 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -25,7 +25,8 @@ class SpreadAlgoTemplate:
price: float,
volume: float,
payup: int,
- interval: int
+ interval: int,
+ lock: bool
):
""""""
self.algo_engine = algo_engine
@@ -39,6 +40,7 @@ class SpreadAlgoTemplate:
self.volume: float = volume
self.payup: int = payup
self.interval = interval
+ self.lock = lock
if direction == Direction.LONG:
self.target = volume
@@ -166,6 +168,7 @@ class SpreadAlgoTemplate:
price,
volume,
direction,
+ self.lock
)
self.leg_orders[vt_symbol].extend(vt_orderids)
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index 062e6221..644bcef7 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -227,6 +227,11 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
button_start = QtWidgets.QPushButton("启动")
button_start.clicked.connect(self.start_algo)
+ self.lock_combo = QtWidgets.QComboBox()
+ self.lock_combo.addItems(
+ ["否", "是"]
+ )
+
form = QtWidgets.QFormLayout()
form.addRow("价差", self.name_line)
form.addRow("方向", self.direction_combo)
@@ -234,6 +239,7 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
form.addRow("数量", self.volume_line)
form.addRow("超价", self.payup_line)
form.addRow("间隔", self.interval_line)
+ form.addRow("锁仓", self.lock_line)
form.addRow(button_start)
self.setLayout(form)
@@ -247,8 +253,14 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
payup = int(self.payup_line.text())
interval = int(self.interval_line.text())
+ lock_str = self.lock_combo.currentText()
+ if lock_str == "是":
+ lock = True
+ else:
+ lock = False
+
self.spread_engine.start_algo(
- name, direction, price, volume, payup, interval
+ name, direction, price, volume, payup, interval, lock
)
From 80c9dfe3785f958ff9b7808024f816b196497c36 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Mon, 16 Sep 2019 16:42:16 +0800
Subject: [PATCH 11/41] [Mod] complete test of algo trading function
---
vnpy/app/spread_trading/algo.py | 9 +++++---
vnpy/app/spread_trading/engine.py | 26 ++++++++++++----------
vnpy/app/spread_trading/template.py | 32 +++++++++++++++++++++++----
vnpy/app/spread_trading/ui/widget.py | 12 +++++-----
vnpy/gateway/bitmex/bitmex_gateway.py | 5 ++++-
vnpy/trader/constant.py | 1 +
vnpy/trader/ui/widget.py | 2 +-
7 files changed, 59 insertions(+), 28 deletions(-)
diff --git a/vnpy/app/spread_trading/algo.py b/vnpy/app/spread_trading/algo.py
index 13b3e968..bda5d317 100644
--- a/vnpy/app/spread_trading/algo.py
+++ b/vnpy/app/spread_trading/algo.py
@@ -1,5 +1,4 @@
from typing import Any
-from math import floor, ceil
from vnpy.trader.constant import Direction
from vnpy.trader.object import (TickData, OrderData, TradeData)
@@ -35,6 +34,10 @@ class SpreadTakerAlgo(SpreadAlgoTemplate):
def on_tick(self, tick: TickData):
""""""
+ # Return if tick not inited
+ if not self.spread.bid_volume or not self.spread.ask_volume:
+ return
+
# Return if there are any existing orders
if not self.check_order_finished():
return
@@ -114,7 +117,7 @@ class SpreadTakerAlgo(SpreadAlgoTemplate):
# Calculate passive leg target volume and do hedge
for leg in self.spread.passive_legs:
- passive_traded = self.leg_orders[leg.vt_symbol]
+ passive_traded = self.leg_traded[leg.vt_symbol]
passive_target = self.spread.calculate_leg_volume(
leg.vt_symbol,
hedge_volume
@@ -133,6 +136,6 @@ class SpreadTakerAlgo(SpreadAlgoTemplate):
if leg_volume > 0:
price = leg_tick.ask_price_1 + leg_contract.pricetick * self.payup
self.send_long_order(leg.vt_symbol, price, abs(leg_volume))
- else:
+ elif leg_volume < 0:
price = leg_tick.bid_price_1 - leg_contract.pricetick * self.payup
self.send_short_order(leg.vt_symbol, price, abs(leg_volume))
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index e12afeb2..a9f1d66e 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -11,15 +11,15 @@ from vnpy.trader.event import (
from vnpy.trader.utility import load_json, save_json
from vnpy.trader.object import (
TickData, ContractData, LogData,
- SubscribeRequest, OrderRequest, CancelRequest
+ SubscribeRequest, OrderRequest
)
from vnpy.trader.constant import Direction, Offset, OrderType
-from vnpy.trader.converter import OffsetConverter, PositionHolding
+from vnpy.trader.converter import OffsetConverter
from .base import (
LegData, SpreadData,
EVENT_SPREAD_DATA, EVENT_SPREAD_ALGO,
- EVENT_SPREAD_LOG, EVENT_SPREAD_STRATEGY
+ EVENT_SPREAD_LOG
)
from .template import SpreadAlgoTemplate
from .algo import SpreadTakerAlgo
@@ -333,7 +333,8 @@ class SpreadAlgoEngine:
price: float,
volume: float,
payup: int,
- interval: int
+ interval: int,
+ lock: bool
) -> str:
# Find spread object
spread = self.spreads.get(spread_name, None)
@@ -355,7 +356,8 @@ class SpreadAlgoEngine:
price,
volume,
payup,
- interval
+ interval,
+ lock
)
self.algos[algoid] = algo
@@ -420,13 +422,13 @@ class SpreadAlgoEngine:
offset = Offset.CLOSE
original_req = OrderRequest(
- contract.symbol,
- contract.exchange,
- direction,
- offset,
- OrderType.LIMIT,
- price,
- volume
+ symbol=contract.symbol,
+ exchange=contract.exchange,
+ direction=direction,
+ offset=offset,
+ type=OrderType.LIMIT,
+ price=price,
+ volume=volume
)
# Convert with offset converter
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index 0a0767ec..cef6781e 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -55,6 +55,8 @@ class SpreadAlgoTemplate:
self.leg_traded: Dict[str, float] = defaultdict(int)
self.leg_orders: Dict[str, List[str]] = defaultdict(list)
+ self.write_log("算法已启动")
+
def is_active(self):
""""""
if self.status not in [Status.CANCELLED, Status.ALLTRADED]:
@@ -105,6 +107,7 @@ class SpreadAlgoTemplate:
if self.is_active():
self.cancel_all_order()
self.status = Status.CANCELLED
+ self.write_log("算法已停止")
self.put_event()
def update_tick(self, tick: TickData):
@@ -118,14 +121,25 @@ class SpreadAlgoTemplate:
else:
self.leg_traded[trade.vt_symbol] -= trade.volume
+ msg = "委托成交,{},{},{}@{}".format(
+ trade.vt_symbol,
+ trade.direction,
+ trade.volume,
+ trade.price
+ )
+ self.write_log(msg)
+
self.calculate_traded()
+ self.put_event()
self.on_trade(trade)
def update_order(self, order: OrderData):
""""""
if not order.is_active():
- self.leg_orders[order.vt_symbol].remove(order.vt_orderid)
+ vt_orderids = self.leg_orders[order.vt_symbol]
+ if order.vt_orderid in vt_orderids:
+ vt_orderids.remove(order.vt_orderid)
self.on_order(order)
@@ -144,7 +158,7 @@ class SpreadAlgoTemplate:
def write_log(self, msg: str):
""""""
- self.algo_engine.write_algo_log(msg)
+ self.algo_engine.write_algo_log(self, msg)
def send_long_order(self, vt_symbol: str, price: float, volume: float):
""""""
@@ -173,10 +187,18 @@ class SpreadAlgoTemplate:
self.leg_orders[vt_symbol].extend(vt_orderids)
+ msg = "发出委托,{},{},{}@{}".format(
+ vt_symbol,
+ direction,
+ volume,
+ price
+ )
+ self.write_log(msg)
+
def cancel_leg_order(self, vt_symbol: str):
""""""
for vt_orderid in self.leg_orders[vt_symbol]:
- self.algo_engine.cancel_order(vt_orderid)
+ self.algo_engine.cancel_order(self, vt_orderid)
def cancel_all_order(self):
""""""
@@ -201,8 +223,10 @@ class SpreadAlgoTemplate:
else:
if adjusted_leg_traded > 0:
self.traded = min(self.traded, adjusted_leg_traded)
- else:
+ elif adjusted_leg_traded < 0:
self.traded = max(self.traded, adjusted_leg_traded)
+ else:
+ self.traded = 0
self.traded_volume = abs(self.traded)
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index 644bcef7..33ab52c9 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -9,9 +9,8 @@ from vnpy.trader.ui import QtWidgets, QtCore, QtGui
from vnpy.trader.ui.widget import (
BaseMonitor, BaseCell,
BidCell, AskCell,
- TimeCell, MsgCell,
- PnlCell, DirectionCell,
- EnumCell,
+ TimeCell, PnlCell,
+ DirectionCell, EnumCell,
)
from ..engine import (
@@ -19,8 +18,7 @@ from ..engine import (
APP_NAME,
EVENT_SPREAD_DATA,
EVENT_SPREAD_LOG,
- EVENT_SPREAD_ALGO,
- EVENT_SPREAD_STRATEGY
+ EVENT_SPREAD_ALGO
)
@@ -153,7 +151,7 @@ class SpreadAlgoMonitor(BaseMonitor):
headers = {
"algoid": {"display": "算法", "cell": BaseCell, "update": False},
"spread_name": {"display": "价差", "cell": BaseCell, "update": False},
- "direction": {"display": "方向", "cell": EnumCell, "update": False},
+ "direction": {"display": "方向", "cell": DirectionCell, "update": False},
"price": {"display": "价格", "cell": BaseCell, "update": False},
"payup": {"display": "超价", "cell": BaseCell, "update": False},
"volume": {"display": "数量", "cell": BaseCell, "update": False},
@@ -239,7 +237,7 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
form.addRow("数量", self.volume_line)
form.addRow("超价", self.payup_line)
form.addRow("间隔", self.interval_line)
- form.addRow("锁仓", self.lock_line)
+ form.addRow("锁仓", self.lock_combo)
form.addRow(button_start)
self.setLayout(form)
diff --git a/vnpy/gateway/bitmex/bitmex_gateway.py b/vnpy/gateway/bitmex/bitmex_gateway.py
index ca62365f..18de3404 100644
--- a/vnpy/gateway/bitmex/bitmex_gateway.py
+++ b/vnpy/gateway/bitmex/bitmex_gateway.py
@@ -471,7 +471,10 @@ class BitmexRestApi(RestClient):
headers = request.response.headers
self.rate_limit_remaining = int(headers["x-ratelimit-remaining"])
- self.rate_limit_sleep = int(headers.get("Retry-After", 0)) + 1 # 1 extra second sleep
+
+ self.rate_limit_sleep = int(headers.get("Retry-After", 0))
+ if self.rate_limit_sleep:
+ self.rate_limit_sleep += 1 # 1 extra second sleep
def reset_rate_limit(self):
"""
diff --git a/vnpy/trader/constant.py b/vnpy/trader/constant.py
index 7bce5e10..72aea310 100644
--- a/vnpy/trader/constant.py
+++ b/vnpy/trader/constant.py
@@ -117,6 +117,7 @@ class Exchange(Enum):
BINANCE = "BINANCE"
COINBASE = "COINBASE"
+
class Currency(Enum):
"""
Currency.
diff --git a/vnpy/trader/ui/widget.py b/vnpy/trader/ui/widget.py
index 78f860dd..591b0b00 100644
--- a/vnpy/trader/ui/widget.py
+++ b/vnpy/trader/ui/widget.py
@@ -158,7 +158,7 @@ class TimeCell(BaseCell):
"""
if content is None:
return
-
+
timestamp = content.strftime("%H:%M:%S")
millisecond = int(content.microsecond / 1000)
From f46f796475e4283464cb0d3a3f80c8ec244abec3 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Mon, 16 Sep 2019 17:22:47 +0800
Subject: [PATCH 12/41] [Mod] move leg multiplier to SpreadData
---
vnpy/app/spread_trading/base.py | 67 ++++++++++++++++-------------
vnpy/app/spread_trading/engine.py | 27 ++++++++----
vnpy/app/spread_trading/template.py | 4 +-
3 files changed, 58 insertions(+), 40 deletions(-)
diff --git a/vnpy/app/spread_trading/base.py b/vnpy/app/spread_trading/base.py
index aee999c4..d9ab9529 100644
--- a/vnpy/app/spread_trading/base.py
+++ b/vnpy/app/spread_trading/base.py
@@ -15,21 +15,10 @@ EVENT_SPREAD_STRATEGY = "eSpreadStrategy"
class LegData:
""""""
- def __init__(
- self,
- vt_symbol: str,
- price_multiplier: float,
- trading_multiplier: float
- ):
+ def __init__(self, vt_symbol: str):
""""""
self.vt_symbol: str = vt_symbol
- # For calculating spread price
- self.price_multiplier: float = price_multiplier
-
- # For calculating spread pos and sending orders
- self.trading_multiplier: float = trading_multiplier
-
# Price and position data
self.bid_price: float = 0
self.ask_price: float = 0
@@ -66,6 +55,8 @@ class SpreadData:
self,
name: str,
legs: List[LegData],
+ price_multipliers: Dict[str, int],
+ trading_multipliers: Dict[str, int],
active_symbol: str
):
""""""
@@ -75,6 +66,12 @@ class SpreadData:
self.active_leg: LegData = None
self.passive_legs: List[LegData] = []
+ # For calculating spread price
+ self.price_multipliers: Dict[str: int] = price_multipliers
+
+ # For calculating spread pos and sending orders
+ self.trading_multipliers: Dict[str: int] = trading_multipliers
+
self.price_formula: str = ""
self.trading_formula: str = ""
@@ -85,15 +82,17 @@ class SpreadData:
else:
self.passive_legs.append(leg)
- if leg.price_multiplier > 0:
- self.price_formula += f"+{leg.trading_multiplier}*{leg.vt_symbol}"
+ price_multiplier = self.price_multipliers[leg.vt_symbol]
+ if price_multiplier > 0:
+ self.price_formula += f"+{price_multiplier}*{leg.vt_symbol}"
else:
- self.price_formula += f"{leg.trading_multiplier}*{leg.vt_symbol}"
+ self.price_formula += f"{price_multiplier}*{leg.vt_symbol}"
- if leg.trading_multiplier > 0:
- self.trading_formula += f"+{leg.trading_multiplier}*{leg.vt_symbol}"
+ trading_multiplier = self.trading_multipliers[leg.vt_symbol]
+ if trading_multiplier > 0:
+ self.trading_formula += f"+{trading_multiplier}*{leg.vt_symbol}"
else:
- self.trading_formula += f"{leg.trading_multiplier}*{leg.vt_symbol}"
+ self.trading_formula += f"{trading_multiplier}*{leg.vt_symbol}"
# Spread data
self.bid_price: float = 0
@@ -116,24 +115,27 @@ class SpreadData:
return
# Calculate price
- if leg.price_multiplier > 0:
- self.bid_price += leg.bid_price * leg.price_multiplier
- self.ask_price += leg.ask_price * leg.price_multiplier
+ price_multiplier = self.price_multipliers[leg.vt_symbol]
+ if price_multiplier > 0:
+ self.bid_price += leg.bid_price * price_multiplier
+ self.ask_price += leg.ask_price * price_multiplier
else:
- self.bid_price += leg.ask_price * leg.price_multiplier
- self.ask_price += leg.bid_price * leg.price_multiplier
+ self.bid_price += leg.ask_price * price_multiplier
+ self.ask_price += leg.bid_price * price_multiplier
# Calculate volume
- if leg.trading_multiplier > 0:
+ trading_multiplier = self.trading_multipliers[leg.vt_symbol]
+
+ if trading_multiplier > 0:
adjusted_bid_volume = floor(
- leg.bid_volume / leg.trading_multiplier)
+ leg.bid_volume / trading_multiplier)
adjusted_ask_volume = floor(
- leg.ask_volume / leg.trading_multiplier)
+ leg.ask_volume / trading_multiplier)
else:
adjusted_bid_volume = floor(
- leg.ask_volume / abs(leg.trading_multiplier))
+ leg.ask_volume / abs(trading_multiplier))
adjusted_ask_volume = floor(
- leg.bid_volume / abs(leg.trading_multiplier))
+ leg.bid_volume / abs(trading_multiplier))
# For the first leg, just initialize
if not n:
@@ -152,7 +154,8 @@ class SpreadData:
self.net_pos = 0
for n, leg in enumerate(self.legs.values()):
- adjusted_net_pos = leg.net_pos / leg.trading_multiplier
+ trading_multiplier = self.trading_multipliers[leg.vt_symbol]
+ adjusted_net_pos = leg.net_pos / trading_multiplier
if adjusted_net_pos > 0:
adjusted_net_pos = floor(adjusted_net_pos)
@@ -177,13 +180,15 @@ class SpreadData:
def calculate_leg_volume(self, vt_symbol: str, spread_volume: float) -> float:
""""""
leg = self.legs[vt_symbol]
- leg_volume = spread_volume * leg.trading_multiplier
+ trading_multiplier = self.trading_multipliers[leg.vt_symbol]
+ leg_volume = spread_volume * trading_multiplier
return leg_volume
def calculate_spread_volume(self, vt_symbol: str, leg_volume: float) -> float:
""""""
leg = self.legs[vt_symbol]
- spread_volume = leg_volume / leg.trading_multiplier
+ trading_multiplier = self.trading_multipliers[leg.vt_symbol]
+ spread_volume = leg_volume / trading_multiplier
if spread_volume > 0:
spread_volume = floor(spread_volume)
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index a9f1d66e..71be653b 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -107,10 +107,13 @@ class SpreadDataEngine:
for spread in self.spreads.values():
leg_settings = []
for leg in spread.legs.values():
+ price_multiplier = spread.price_multipliers[leg.vt_symbol]
+ trading_multiplier = spread.trading_multipliers[leg.vt_symbol]
+
leg_setting = {
"vt_symbol": leg.vt_symbol,
- "price_multiplier": leg.price_multiplier,
- "trading_multiplier": leg.trading_multiplier
+ "price_multiplier": price_multiplier,
+ "trading_multiplier": trading_multiplier
}
leg_settings.append(leg_setting)
@@ -185,21 +188,28 @@ class SpreadDataEngine:
return
legs: List[LegData] = []
+ price_multipliers: Dict[str, int] = {}
+ trading_multipliers: Dict[str, int] = {}
+
for leg_setting in leg_settings:
vt_symbol = leg_setting["vt_symbol"]
leg = self.legs.get(vt_symbol, None)
if not leg:
- leg = LegData(
- vt_symbol,
- leg_setting["price_multiplier"],
- leg_setting["trading_multiplier"]
- )
+ leg = LegData(vt_symbol)
self.legs[vt_symbol] = leg
legs.append(leg)
+ price_multipliers[vt_symbol] = leg_setting["price_multiplier"]
+ trading_multipliers[vt_symbol] = leg_setting["trading_multiplier"]
- spread = SpreadData(name, legs, active_symbol)
+ spread = SpreadData(
+ name,
+ legs,
+ price_multipliers,
+ trading_multipliers,
+ active_symbol
+ )
self.spreads[name] = spread
for leg in spread.legs.values():
@@ -221,6 +231,7 @@ class SpreadDataEngine:
for leg in spread.legs:
self.symbol_spread_map[leg.vt_symbol].remove(spread)
+ self.save_setting()
self.write_log("价差删除成功:{}".format(name))
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index cef6781e..c723010c 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -211,7 +211,9 @@ class SpreadAlgoTemplate:
for n, leg in enumerate(self.spread.legs.values()):
leg_traded = self.leg_traded[leg.vt_symbol]
- adjusted_leg_traded = leg_traded / leg.trading_multiplier
+ trading_multiplier = self.spread.trading_multipliers[
+ leg.vt_symbol]
+ adjusted_leg_traded = leg_traded / trading_multiplier
if adjusted_leg_traded > 0:
adjusted_leg_traded = floor(adjusted_leg_traded)
From bbd62e02b72e8a0cd92b3f0a4404ebfae2084725 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Mon, 16 Sep 2019 23:06:40 +0800
Subject: [PATCH 13/41] [Mod] add EVENT_SPREAD_POS event type
---
vnpy/app/spread_trading/base.py | 41 ++++++++++++++++++++++++++--
vnpy/app/spread_trading/ui/widget.py | 8 ++++++
2 files changed, 47 insertions(+), 2 deletions(-)
diff --git a/vnpy/app/spread_trading/base.py b/vnpy/app/spread_trading/base.py
index d9ab9529..5eff4b48 100644
--- a/vnpy/app/spread_trading/base.py
+++ b/vnpy/app/spread_trading/base.py
@@ -2,11 +2,12 @@ from typing import Dict, List
from math import floor, ceil
from datetime import datetime
-from vnpy.trader.object import TickData, PositionData
-from vnpy.trader.constant import Direction
+from vnpy.trader.object import TickData, PositionData, TradeData
+from vnpy.trader.constant import Direction, Offset, Exchange
EVENT_SPREAD_DATA = "eSpreadData"
+EVENT_SPREAD_POS = "eSpreadPos"
EVENT_SPREAD_LOG = "eSpreadLog"
EVENT_SPREAD_ALGO = "eSpreadAlgo"
EVENT_SPREAD_STRATEGY = "eSpreadStrategy"
@@ -29,6 +30,9 @@ class LegData:
self.short_pos: float = 0
self.net_pos: float = 0
+ # Tick data buf
+ self.tick: TickData = None
+
def update_tick(self, tick: TickData):
""""""
self.bid_price = tick.bid_price_1
@@ -36,6 +40,8 @@ class LegData:
self.bid_volume = tick.bid_volume_1
self.ask_volume = tick.ask_volume_1
+ self.tick = tick
+
def update_position(self, position: PositionData):
""""""
if position.direction == Direction.NET:
@@ -47,6 +53,21 @@ class LegData:
self.short_pos = position.volume
self.net_pos = self.long_pos - self.short_pos
+ def update_trade(self, trade: TradeData):
+ """"""
+ if trade.direction == Direction.LONG:
+ if trade.offset == Offset.OPEN:
+ self.long_pos += trade.volume
+ else:
+ self.short_pos -= trade.volume
+ else:
+ if trade.offset == Offset.OPEN:
+ self.short_pos += trade.volume
+ else:
+ self.long_pos -= trade.volume
+
+ self.net_pos = self.long_pos - self.net_pos
+
class SpreadData:
""""""
@@ -196,3 +217,19 @@ class SpreadData:
spread_volume = ceil(spread_volume)
return spread_volume
+
+ def to_tick(self):
+ """"""
+ tick = TickData(
+ symbol=self.name,
+ exchange=Exchange.LOCAL,
+ datetime=self.datetime,
+ name=self.name,
+ last_price=(self.bid_price + self.ask_price) / 2,
+ bid_price_1=self.bid_price,
+ ask_price_1=self.ask_price,
+ bid_volume_1=self.bid_volume,
+ ask_volume_1=self.ask_volume,
+ gateway_name="SPREAD"
+ )
+ return tick
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index 33ab52c9..289b38fa 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -17,6 +17,7 @@ from ..engine import (
SpreadEngine,
APP_NAME,
EVENT_SPREAD_DATA,
+ EVENT_SPREAD_POS,
EVENT_SPREAD_LOG,
EVENT_SPREAD_ALGO
)
@@ -105,6 +106,13 @@ class SpreadDataMonitor(BaseMonitor):
"datetime": {"display": "时间", "cell": TimeCell, "update": True},
}
+ def register_event(self):
+ """
+ Register event handler into event engine.
+ """
+ super().register_event()
+ self.event_engine.register(EVENT_SPREAD_POS, self.signal.emit)
+
class SpreadLogMonitor(QtWidgets.QTextEdit):
"""
From 68090e4552e4e471edc41655c2d7589b51995d9f Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Mon, 16 Sep 2019 23:33:36 +0800
Subject: [PATCH 14/41] [Mod] add SpreadStrategyTemplate for creating spread
strategies
---
vnpy/app/spread_trading/engine.py | 135 ++++++++++++++++++++++--
vnpy/app/spread_trading/template.py | 155 +++++++++++++++++++++++++++-
vnpy/trader/constant.py | 3 +
3 files changed, 285 insertions(+), 8 deletions(-)
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 71be653b..deb7e801 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -18,10 +18,11 @@ from vnpy.trader.converter import OffsetConverter
from .base import (
LegData, SpreadData,
- EVENT_SPREAD_DATA, EVENT_SPREAD_ALGO,
- EVENT_SPREAD_LOG
+ EVENT_SPREAD_DATA, EVENT_SPREAD_POS,
+ EVENT_SPREAD_ALGO, EVENT_SPREAD_LOG,
+ EVENT_SPREAD_STRATEGY
)
-from .template import SpreadAlgoTemplate
+from .template import SpreadAlgoTemplate, SpreadStrategyTemplate
from .algo import SpreadTakerAlgo
@@ -129,6 +130,7 @@ class SpreadDataEngine:
def register_event(self) -> None:
""""""
self.event_engine.register(EVENT_TICK, self.process_tick_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_CONTRACT, self.process_contract_event)
@@ -143,8 +145,7 @@ class SpreadDataEngine:
for spread in self.symbol_spread_map[tick.vt_symbol]:
spread.calculate_price()
-
- self.put_data_event(spread)
+ self.put_data_event(spread)
def process_position_event(self, event: Event) -> None:
""""""
@@ -157,8 +158,20 @@ class SpreadDataEngine:
for spread in self.symbol_spread_map[position.vt_symbol]:
spread.calculate_pos()
+ self.put_pos_event(spread)
- self.put_data_event(spread)
+ def process_trade_event(self, event: Event) -> None:
+ """"""
+ trade = event.data
+
+ leg = self.legs.get(trade.vt_symbol, None)
+ if not leg:
+ return
+ leg.update_trade(trade)
+
+ for spread in self.symbol_spread_map[trade.vt_symbol]:
+ spread.calculate_pos()
+ self.put_pos_event(spread)
def process_contract_event(self, event: Event) -> None:
""""""
@@ -175,6 +188,11 @@ class SpreadDataEngine:
event = Event(EVENT_SPREAD_DATA, spread)
self.event_engine.put(event)
+ def put_pos_event(self, spread: SpreadData) -> None:
+ """"""
+ event = Event(EVENT_SPREAD_POS, spread)
+ self.event_engine.put(event)
+
def add_spread(
self,
name: str,
@@ -483,3 +501,108 @@ class SpreadAlgoEngine:
def get_contract(self, vt_symbol: str) -> ContractData:
""""""
return self.main_engine.get_contract(vt_symbol)
+
+
+class SpreadStrategyEngine:
+ """"""
+
+ def __init__(self, spread_engine: SpreadEngine):
+ """"""
+ self.spread_engine: SpreadEngine = spread_engine
+ self.main_engine: MainEngine = spread_engine.main_engine
+ self.event_engine: EventEngine = spread_engine.event_engine
+
+ self.write_log = spread_engine.write_log
+
+ self.strategies: Dict[str: SpreadStrategyTemplate] = {}
+
+ self.order_strategy_map: dict[str: SpreadStrategyTemplate] = {}
+ self.name_strategy_map: dict[str: SpreadStrategyTemplate] = defaultdict(
+ list)
+
+ self.vt_tradeids: Set = set()
+
+ def start(self):
+ """"""
+ self.load_setting()
+ self.register_event()
+
+ self.write_log("价差策略引擎启动成功")
+
+ def load_setting(self):
+ """"""
+ pass
+
+ def save_setting(self):
+ """"""
+ pass
+
+ def register_event(self):
+ """"""
+ ee = self.event_engine
+ ee.register(EVENT_ORDER, self.process_order_event)
+ ee.register(EVENT_TRADE, self.process_trade_event)
+ ee.register(EVENT_SPREAD_DATA, self.process_spread_data_event)
+ ee.register(EVENT_SPREAD_POS, self.process_spread_pos_event)
+ ee.register(EVENT_SPREAD_ALGO, self.process_spread_algo_event)
+
+ def process_spread_data_event(self, event: Event):
+ """"""
+ pass
+
+ def process_spread_pos_event(self, event: Event):
+ """"""
+ pass
+
+ def process_spread_algo_event(self, event: Event):
+ """"""
+ pass
+
+ def process_order_event(self, event: Event):
+ """"""
+ pass
+
+ def process_trade_event(self, event: Event):
+ """"""
+ pass
+
+ def start_algo(
+ self,
+ strategy: SpreadStrategyTemplate,
+ direction: Direction,
+ price: float,
+ volume: float,
+ payup: int,
+ interval: int,
+ lock: bool
+ ) -> str:
+ """"""
+ pass
+
+ def stop_algo(self, algoid: str):
+ """"""
+ pass
+
+ def send_order(
+ self,
+ strategy: SpreadStrategyTemplate,
+ vt_symbol: str,
+ price: float,
+ volume: float,
+ direction: Direction,
+ offset: Offset
+ ) -> str:
+ pass
+
+ def cancel_order(self, vt_orderid: str):
+ """"""
+ pass
+
+ def put_strategy_event(self, strategy: SpreadStrategyTemplate):
+ """"""
+ pass
+
+ def write_strategy_log(self, strategy: SpreadStrategyTemplate, msg: str):
+ """"""
+ pass
+
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index c723010c..8f428d4b 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -4,7 +4,7 @@ from typing import Dict, List
from math import floor, ceil
from vnpy.trader.object import TickData, TradeData, OrderData, ContractData
-from vnpy.trader.constant import Direction, Status
+from vnpy.trader.constant import Direction, Status, Offset
from vnpy.trader.utility import virtual
from .base import SpreadData
@@ -12,7 +12,7 @@ from .base import SpreadData
class SpreadAlgoTemplate:
"""
- Template for writing spread trading algos.
+ Template for implementing spread trading algos.
"""
algo_name = "AlgoTemplate"
@@ -266,3 +266,154 @@ class SpreadAlgoTemplate:
def on_interval(self):
""""""
pass
+
+
+class SpreadStrategyTemplate:
+ """
+ Template for implementing spread trading strategies.
+ """
+ strategy_name = "StrategyTemplate"
+
+ def __init__(
+ self,
+ strategy_engine,
+ strategy_id: str,
+ spread: SpreadData
+ ):
+ """"""
+ self.strategy_engine = strategy_engine
+ self.strategy_id = strategy_id
+ self.spread = spread
+
+ @virtual
+ def on_spread_data(self):
+ """"""
+ pass
+
+ @virtual
+ def on_spread_pos(self):
+ """"""
+ pass
+
+ @virtual
+ def on_spread_algo(self, algo: SpreadAlgoTemplate):
+ """"""
+ pass
+
+ @virtual
+ def on_order(self, order: OrderData):
+ """"""
+ pass
+
+ @virtual
+ def on_trade(self, trade: TradeData):
+ """"""
+ pass
+
+ def start_algo(
+ self,
+ direction: Direction,
+ price: float,
+ volume: float,
+ payup: int,
+ interval: int,
+ lock: bool
+ ) -> str:
+ """"""
+ pass
+
+ def start_long_algo(
+ self,
+ price: float,
+ volume: float,
+ payup: int,
+ interval: int,
+ lock: bool
+ ) -> str:
+ """"""
+ return self.start_algo(Direction.LONG, price, volume, payup, interval, lock)
+
+ def start_short_algo(
+ self,
+ price: float,
+ volume: float,
+ payup: int,
+ interval: int,
+ lock: bool
+ ) -> str:
+ """"""
+ return self.start_algo(Direction.SHORT, price, volume, payup, interval, lock)
+
+ def stop_algo(self, algoid: str):
+ """"""
+ pass
+
+ def buy(self, vt_symbol: str, price: float, volume: float):
+ """"""
+ return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.OPEN)
+
+ def sell(self, vt_symbol: str, price: float, volume: float):
+ """"""
+ return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.CLOSE)
+
+ def short(self, vt_symbol: str, price: float, volume: float):
+ """"""
+ return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.OPEN)
+
+ def cover(self, vt_symbol: str, price: float, volume: float):
+ """"""
+ return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.CLOSE)
+
+ def send_order(
+ self,
+ vt_symbol: str,
+ price: float,
+ volume: float,
+ direction: Direction,
+ offset: Offset
+ ):
+ """"""
+ pass
+
+ def cancel_order(self, vt_orderid: str):
+ """"""
+ pass
+
+ def put_event(self):
+ """"""
+ pass
+
+ def write_log(self, msg: str):
+ """"""
+ pass
+
+ def get_spread_tick(self) -> TickData:
+ """"""
+ return self.spread.to_tick()
+
+ def get_spread_pos(self) -> float:
+ """"""
+ return self.spread.net_pos
+
+ def get_leg_tick(self, vt_symbol: str) -> TickData:
+ """"""
+ leg = self.spread.legs.get(vt_symbol, None)
+
+ if not leg:
+ return None
+
+ return leg.tick
+
+ def get_leg_pos(self, vt_symbol: str, direction: Direction = Direction.NET) -> float:
+ """"""
+ leg = self.spread.legs.get(vt_symbol, None)
+
+ if not leg:
+ return None
+
+ if direction == Direction.NET:
+ return leg.net_pos
+ elif direction == Direction.LONG:
+ return leg.long_pos
+ else:
+ return leg.short_pos
diff --git a/vnpy/trader/constant.py b/vnpy/trader/constant.py
index 72aea310..1ebb2aa8 100644
--- a/vnpy/trader/constant.py
+++ b/vnpy/trader/constant.py
@@ -117,6 +117,9 @@ class Exchange(Enum):
BINANCE = "BINANCE"
COINBASE = "COINBASE"
+ # Special Function
+ LOCAL = "LOCAL" # For local generated data
+
class Currency(Enum):
"""
From 3b9f1c314826b202f26f17012d37b9a7f32bb916 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 16:26:48 +0800
Subject: [PATCH 15/41] [Add] SpreadStrategyEngine for managing spread trading
strategies
---
vnpy/app/spread_trading/engine.py | 326 ++++++++++++++++++++++++++--
vnpy/app/spread_trading/template.py | 227 ++++++++++++++++---
2 files changed, 504 insertions(+), 49 deletions(-)
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index deb7e801..6025d30e 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -1,6 +1,10 @@
-from typing import List, Dict, Set
+import traceback
+import importlib
+import os
+from typing import List, Dict, Set, Callable, Any, Type
from collections import defaultdict
from copy import copy
+from path import Path
from vnpy.event import EventEngine, Event
from vnpy.trader.engine import BaseEngine, MainEngine
@@ -40,6 +44,7 @@ class SpreadEngine(BaseEngine):
self.data_engine: SpreadDataEngine = SpreadDataEngine(self)
self.algo_engine: SpreadAlgoEngine = SpreadAlgoEngine(self)
+ self.strategy_engine: SpreadStrategyEngine = SpreadStrategyEngine(self)
self.add_spread = self.data_engine.add_spread
self.remove_spread = self.data_engine.remove_spread
@@ -506,6 +511,8 @@ class SpreadAlgoEngine:
class SpreadStrategyEngine:
""""""
+ setting_filename = "spraed_trading_strategy.json"
+
def __init__(self, spread_engine: SpreadEngine):
""""""
self.spread_engine: SpreadEngine = spread_engine
@@ -514,28 +521,107 @@ class SpreadStrategyEngine:
self.write_log = spread_engine.write_log
+ self.strategy_setting: Dict[str: Dict] = {}
+
+ self.classes: Dict[str: Type[SpreadStrategyTemplate]] = {}
self.strategies: Dict[str: SpreadStrategyTemplate] = {}
self.order_strategy_map: dict[str: SpreadStrategyTemplate] = {}
- self.name_strategy_map: dict[str: SpreadStrategyTemplate] = defaultdict(
+ self.algo_strategy_map: dict[str: SpreadStrategyTemplate] = {}
+ self.spread_strategy_map: dict[str: SpreadStrategyTemplate] = defaultdict(
list)
self.vt_tradeids: Set = set()
def start(self):
""""""
- self.load_setting()
+ self.load_strategy_class()
+ self.load_strategy_setting()
self.register_event()
self.write_log("价差策略引擎启动成功")
- def load_setting(self):
+ def close(self):
""""""
- pass
+ self.stop_all_strategies()
- def save_setting(self):
- """"""
- pass
+ def load_strategy_class(self):
+ """
+ Load strategy class from source code.
+ """
+ path1 = Path(__file__).parent.joinpath("strategies")
+ self.load_strategy_class_from_folder(
+ path1, "vnpy.app.cta_strategy.strategies")
+
+ path2 = Path.cwd().joinpath("strategies")
+ self.load_strategy_class_from_folder(path2, "strategies")
+
+ def load_strategy_class_from_folder(self, path: Path, module_name: str = ""):
+ """
+ Load strategy class from certain folder.
+ """
+ for dirpath, dirnames, filenames in os.walk(str(path)):
+ for filename in filenames:
+ if filename.endswith(".py"):
+ strategy_module_name = ".".join(
+ [module_name, filename.replace(".py", "")])
+ elif filename.endswith(".pyd"):
+ strategy_module_name = ".".join(
+ [module_name, filename.split(".")[0]])
+
+ self.load_strategy_class_from_module(strategy_module_name)
+
+ def load_strategy_class_from_module(self, module_name: str):
+ """
+ Load strategy class from module file.
+ """
+ try:
+ module = importlib.import_module(module_name)
+
+ for name in dir(module):
+ value = getattr(module, name)
+ if (isinstance(value, type) and issubclass(value, SpreadStrategyTemplate) and value is not SpreadStrategyTemplate):
+ self.classes[value.__name__] = value
+ except: # noqa
+ msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}"
+ self.write_log(msg)
+
+ def load_strategy_setting(self):
+ """
+ Load setting file.
+ """
+ self.strategy_setting = load_json(self.setting_filename)
+
+ for strategy_name, strategy_config in self.strategy_setting.items():
+ self.add_strategy(
+ strategy_config["class_name"],
+ strategy_name,
+ strategy_config["spread_name"],
+ strategy_config["setting"]
+ )
+
+ def update_strategy_setting(self, strategy_name: str, setting: dict):
+ """
+ Update setting file.
+ """
+ strategy = self.strategies[strategy_name]
+
+ self.strategy_setting[strategy_name] = {
+ "class_name": strategy.__class__.__name__,
+ "spread_name": strategy.spread_name,
+ "setting": setting,
+ }
+ save_json(self.setting_filename, self.strategy_setting)
+
+ def remove_strategy_setting(self, strategy_name: str):
+ """
+ Update setting file.
+ """
+ if strategy_name not in self.strategy_setting:
+ return
+
+ self.strategy_setting.pop(strategy_name)
+ save_json(self.setting_filename, self.strategy_setting)
def register_event(self):
""""""
@@ -548,27 +634,152 @@ class SpreadStrategyEngine:
def process_spread_data_event(self, event: Event):
""""""
- pass
+ spread = event.data
+ strategies = self.spread_strategy_map[spread.name]
+
+ for strategy in strategies:
+ self.call_strategy_func(strategy, strategy.on_spread_data)
def process_spread_pos_event(self, event: Event):
""""""
- pass
+ spread = event.data
+ strategies = self.spread_strategy_map[spread.name]
+
+ for strategy in strategies:
+ self.call_strategy_func(strategy, strategy.on_spread_pos)
def process_spread_algo_event(self, event: Event):
""""""
- pass
+ algo = event.data
+ strategy = self.algo_strategy_map.get(algo.algoid, None)
+
+ if strategy:
+ self.call_strategy_func(strategy, strategy.update_spread_algo, algo)
def process_order_event(self, event: Event):
""""""
- pass
+ order = event.data
+ strategy = self.order_strategy_map.get(order.vt_orderid, None)
+
+ if strategy:
+ self.call_strategy_func(strategy, strategy.update_order, order)
def process_trade_event(self, event: Event):
""""""
- pass
+ trade = event.data
+ strategy = self.trade_strategy_map.get(trade.vt_orderid, None)
+
+ if strategy:
+ self.call_strategy_func(strategy, strategy.on_trade, trade)
+
+ def call_strategy_func(
+ self, strategy: SpreadStrategyTemplate, func: Callable, params: Any = None
+ ):
+ """
+ Call function of a strategy and catch any exception raised.
+ """
+ try:
+ if params:
+ func(params)
+ else:
+ func()
+ except Exception:
+ strategy.trading = False
+ strategy.inited = False
+
+ msg = f"触发异常已停止\n{traceback.format_exc()}"
+ self.write_log(msg, strategy)
+
+ def add_strategy(
+ self, class_name: str, strategy_name: str, spread_name: str, setting: dict
+ ):
+ """
+ Add a new strategy.
+ """
+ if strategy_name in self.strategies:
+ self.write_log(f"创建策略失败,存在重名{strategy_name}")
+ return
+
+ strategy_class = self.classes.get(class_name, None)
+ if not strategy_class:
+ self.write_log(f"创建策略失败,找不到策略类{class_name}")
+ return
+
+ strategy = strategy_class(self, strategy_name, spread_name, setting)
+ self.strategies[strategy_name] = strategy
+
+ # Add vt_symbol to strategy map.
+ strategies = self.spread_strategy_map[spread_name]
+ strategies.append(strategy)
+
+ # Update to setting file.
+ self.update_strategy_setting(strategy_name, setting)
+
+ self.put_strategy_event(strategy)
+
+ def init_strategy(self, strategy_name: str):
+ """"""
+ strategy = self.strategies[strategy_name]
+
+ if strategy.inited:
+ self.write_log(f"{strategy_name}已经完成初始化,禁止重复操作")
+ return
+
+ self.call_strategy_func(strategy, strategy.on_init)
+ strategy.inited = True
+
+ self.put_strategy_event(strategy)
+ self.write_log(f"{strategy_name}初始化完成")
+
+ def start_strategy(self, strategy_name: str):
+ """"""
+ strategy = self.strategies[strategy_name]
+ if not strategy.inited:
+ self.write_log(f"策略{strategy.strategy_name}启动失败,请先初始化")
+ return
+
+ if strategy.trading:
+ self.write_log(f"{strategy_name}已经启动,请勿重复操作")
+ return
+
+ self.call_strategy_func(strategy, strategy.on_start)
+ strategy.trading = True
+
+ self.put_strategy_event(strategy)
+
+ def stop_strategy(self, strategy_name: str):
+ """"""
+ strategy = self.strategies[strategy_name]
+ if not strategy.trading:
+ return
+
+ self.call_strategy_func(strategy, strategy.on_stop)
+ strategy.trading = False
+
+ strategy.stop_all_algos()
+ strategy.cancel_all_orders()
+
+ self.put_strategy_event(strategy)
+
+ def init_all_strategies(self):
+ """"""
+ for strategy in self.strategies.values():
+ self.init_strategy(strategy)
+
+ def start_all_strategies(self):
+ """"""
+ for strategy in self.strategies.values():
+ self.start_strategy(strategy)
+
+ def stop_all_strategies(self):
+ """"""
+ for strategy in self.strategies.values():
+ self.stop_strategy(strategy)
def start_algo(
self,
strategy: SpreadStrategyTemplate,
+ spread_name: str,
direction: Direction,
price: float,
volume: float,
@@ -577,9 +788,25 @@ class SpreadStrategyEngine:
lock: bool
) -> str:
""""""
- pass
+ algoid = self.spread_engine.start_algo(
+ spread_name,
+ direction,
+ price,
+ volume,
+ payup,
+ interval,
+ lock
+ )
- def stop_algo(self, algoid: str):
+ self.algo_strategy_map[algoid] = strategy
+
+ return algoid
+
+ def stop_algo(self, strategy: SpreadStrategyTemplate, algoid: str):
+ """"""
+ self.spread_engine.stop_algo(algoid)
+
+ def stop_all_algos(self, strategy: SpreadStrategyTemplate):
""""""
pass
@@ -590,19 +817,74 @@ class SpreadStrategyEngine:
price: float,
volume: float,
direction: Direction,
- offset: Offset
- ) -> str:
- pass
+ offset: Offset,
+ lock: bool
+ ) -> List[str]:
+ contract = self.main_engine.get_contract(vt_symbol)
- def cancel_order(self, vt_orderid: str):
+ original_req = OrderRequest(
+ symbol=contract.symbol,
+ exchange=contract.exchange,
+ direction=direction,
+ offset=offset,
+ type=OrderType.LIMIT,
+ price=price,
+ volume=volume
+ )
+
+ # Convert with offset converter
+ req_list = self.offset_converter.convert_order_request(
+ original_req, lock)
+
+ # Send Orders
+ vt_orderids = []
+
+ for req in req_list:
+ vt_orderid = self.main_engine.send_order(
+ req, contract.gateway_name)
+
+ # Check if sending order successful
+ if not vt_orderid:
+ continue
+
+ vt_orderids.append(vt_orderid)
+
+ self.offset_converter.update_order_request(req, vt_orderid)
+
+ # Save relationship between orderid and strategy.
+ self.order_strategy_map[vt_orderid] = strategy
+
+ return vt_orderids
+
+ def cancel_order(self, strategy: SpreadStrategyTemplate, vt_orderid: str):
+ """"""
+ order = self.main_engine.get_order(vt_orderid)
+ if not order:
+ self.write_strategy_log(strategy, "撤单失败,找不到委托{}".format(vt_orderid))
+ return
+
+ req = order.create_cancel_request()
+ self.main_engine.cancel_order(req, order.gateway_name)
+
+ def cancel_all_orders(self, strategy: SpreadStrategyTemplate):
""""""
pass
def put_strategy_event(self, strategy: SpreadStrategyTemplate):
""""""
- pass
+ event = Event(EVENT_SPREAD_STRATEGY, strategy)
+ self.event_engine.put(event)
def write_strategy_log(self, strategy: SpreadStrategyTemplate, msg: str):
""""""
- pass
-
+ msg = f"{strategy.strategy_name}:{msg}"
+ self.write_log(msg)
+
+ def send_strategy_email(self, strategy: SpreadStrategyTemplate, msg: str):
+ """"""
+ if strategy:
+ subject = f"{strategy.strategy_name}"
+ else:
+ subject = "价差策略引擎"
+
+ self.main_engine.send_email(subject, msg)
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index 8f428d4b..88f572ed 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -1,7 +1,8 @@
from collections import defaultdict
-from typing import Dict, List
+from typing import Dict, List, Set
from math import floor, ceil
+from copy import copy
from vnpy.trader.object import TickData, TradeData, OrderData, ContractData
from vnpy.trader.constant import Direction, Status, Offset
@@ -272,42 +273,158 @@ class SpreadStrategyTemplate:
"""
Template for implementing spread trading strategies.
"""
- strategy_name = "StrategyTemplate"
+
+ author: str = ""
+ parameters: List[str] = []
+ variables: List[str] = []
def __init__(
self,
strategy_engine,
- strategy_id: str,
- spread: SpreadData
+ strategy_name: str,
+ spread: SpreadData,
+ setting: dict
):
""""""
self.strategy_engine = strategy_engine
- self.strategy_id = strategy_id
+ self.strategy_name = strategy_name
self.spread = spread
+ self.spread_name = spread.name
+
+ self.inited = False
+ self.trading = False
+
+ self.variables = copy(self.variables)
+ self.variables.insert(0, "inited")
+ self.variables.insert(1, "trading")
+
+ self.vt_orderids: Set[str] = set()
+ self.algoids: Set[str] = set()
+
+ self.update_setting(setting)
+
+ def update_setting(self, setting: dict):
+ """
+ Update strategy parameter wtih value in setting dict.
+ """
+ for name in self.parameters:
+ if name in setting:
+ setattr(self, name, setting[name])
+
+ @classmethod
+ def get_class_parameters(cls):
+ """
+ Get default parameters dict of strategy class.
+ """
+ class_parameters = {}
+ for name in cls.parameters:
+ class_parameters[name] = getattr(cls, name)
+ return class_parameters
+
+ def get_parameters(self):
+ """
+ Get strategy parameters dict.
+ """
+ strategy_parameters = {}
+ for name in self.parameters:
+ strategy_parameters[name] = getattr(self, name)
+ return strategy_parameters
+
+ def get_variables(self):
+ """
+ Get strategy variables dict.
+ """
+ strategy_variables = {}
+ for name in self.variables:
+ strategy_variables[name] = getattr(self, name)
+ return strategy_variables
+
+ def get_data(self):
+ """
+ Get strategy data.
+ """
+ strategy_data = {
+ "strategy_name": self.strategy_name,
+ "spread_name": self.spread_name,
+ "class_name": self.__class__.__name__,
+ "author": self.author,
+ "parameters": self.get_parameters(),
+ "variables": self.get_variables(),
+ }
+ return strategy_data
+
+ def update_spread_algo(self, algo: SpreadAlgoTemplate):
+ """
+ Callback when algo status is updated.
+ """
+ if not algo.is_active() and algo.algoid in self.algoids:
+ self.algoids.pop(algo.algoid)
+
+ self.on_spread_algo(algo)
+
+ def update_order(self, order: OrderData):
+ """
+ Callback when order status is updated.
+ """
+ if not order.is_active() and order.vt_orderid in self.vt_orderids:
+ self.vt_orderids.pop(order.vt_orderid)
+
+ self.on_order(order)
+
+ @virtual
+ def on_init(self):
+ """
+ Callback when strategy is inited.
+ """
+ pass
+
+ @virtual
+ def on_start(self):
+ """
+ Callback when strategy is started.
+ """
+ pass
+
+ @virtual
+ def on_stop(self):
+ """
+ Callback when strategy is stopped.
+ """
+ pass
@virtual
def on_spread_data(self):
- """"""
+ """
+ Callback when spread price is updated.
+ """
pass
@virtual
def on_spread_pos(self):
- """"""
+ """
+ Callback when spread position is updated.
+ """
pass
@virtual
def on_spread_algo(self, algo: SpreadAlgoTemplate):
- """"""
+ """
+ Callback when algo status is updated.
+ """
pass
@virtual
def on_order(self, order: OrderData):
- """"""
+ """
+ Callback when order status is updated.
+ """
pass
@virtual
def on_trade(self, trade: TradeData):
- """"""
+ """
+ Callback when new trade data is received.
+ """
pass
def start_algo(
@@ -320,7 +437,23 @@ class SpreadStrategyTemplate:
lock: bool
) -> str:
""""""
- pass
+ if not self.trading:
+ return ""
+
+ algoid: str = self.strategy_engine.start_algo(
+ self,
+ self.spread_name,
+ direction,
+ price,
+ volume,
+ payup,
+ interval,
+ lock
+ )
+
+ self.algoids.add(algoid)
+
+ return algoid
def start_long_algo(
self,
@@ -346,23 +479,31 @@ class SpreadStrategyTemplate:
def stop_algo(self, algoid: str):
""""""
- pass
+ if not self.trading:
+ return
- def buy(self, vt_symbol: str, price: float, volume: float):
- """"""
- return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.OPEN)
+ self.strategy_engine.stop_algo(self, algoid)
- def sell(self, vt_symbol: str, price: float, volume: float):
+ def stop_all_algos(self):
""""""
- return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.CLOSE)
+ for algoid in self.algoids:
+ self.stop_algo(algoid)
- def short(self, vt_symbol: str, price: float, volume: float):
+ def buy(self, vt_symbol: str, price: float, volume: float, lock: bool = False) -> List[str]:
""""""
- return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.OPEN)
+ return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.OPEN, lock)
- def cover(self, vt_symbol: str, price: float, volume: float):
+ def sell(self, vt_symbol: str, price: float, volume: float, lock: bool = False) -> List[str]:
""""""
- return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.CLOSE)
+ return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.CLOSE, lock)
+
+ def short(self, vt_symbol: str, price: float, volume: float, lock: bool = False) -> List[str]:
+ """"""
+ return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.OPEN, lock)
+
+ def cover(self, vt_symbol: str, price: float, volume: float, lock: bool = False) -> List[str]:
+ """"""
+ return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.CLOSE, lock)
def send_order(
self,
@@ -370,22 +511,47 @@ class SpreadStrategyTemplate:
price: float,
volume: float,
direction: Direction,
- offset: Offset
- ):
+ offset: Offset,
+ lock: bool
+ ) -> List[str]:
""""""
- pass
+ if not self.trading:
+ return []
+
+ vt_orderids: List[str] = self.strategy_engine.send_order(
+ self,
+ vt_symbol,
+ price,
+ volume,
+ direction,
+ offset,
+ lock
+ )
+
+ for vt_orderid in vt_orderids:
+ self.vt_orderids.add(vt_orderid)
+
+ return vt_orderids
def cancel_order(self, vt_orderid: str):
""""""
- pass
+ if not self.trading:
+ return
+
+ self.strategy_engine.cancel_order(self, vt_orderid)
+
+ def cancel_all_orders(self):
+ """"""
+ for vt_orderid in self.vt_orderids:
+ self.cancel_order(vt_orderid)
def put_event(self):
""""""
- pass
+ self.strategy_engine.put_strategy_event(self)
def write_log(self, msg: str):
""""""
- pass
+ self.strategy_engine.write_strategy_log(self, msg)
def get_spread_tick(self) -> TickData:
""""""
@@ -417,3 +583,10 @@ class SpreadStrategyTemplate:
return leg.long_pos
else:
return leg.short_pos
+
+ def send_email(self, msg: str):
+ """
+ Send email to default receiver.
+ """
+ if self.inited:
+ self.strategy_engine.send_email(msg, self)
From 80d89d1cb802eb18952d67b5f7d7cc2738282cb8 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 17:41:20 +0800
Subject: [PATCH 16/41] [Add] UI component for managing strategies
---
vnpy/app/spread_trading/engine.py | 6 +-
vnpy/app/spread_trading/ui/widget.py | 414 +++++++++++++++++++++++++--
2 files changed, 393 insertions(+), 27 deletions(-)
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 6025d30e..84e65393 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -4,7 +4,7 @@ import os
from typing import List, Dict, Set, Callable, Any, Type
from collections import defaultdict
from copy import copy
-from path import Path
+from pathlib import Path
from vnpy.event import EventEngine, Event
from vnpy.trader.engine import BaseEngine, MainEngine
@@ -586,6 +586,10 @@ class SpreadStrategyEngine:
msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}"
self.write_log(msg)
+ def get_all_strategy_class_names(self):
+ """"""
+ return list(self.classes.keys())
+
def load_strategy_setting(self):
"""
Load setting file.
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index 289b38fa..c5038f3d 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -15,11 +15,13 @@ from vnpy.trader.ui.widget import (
from ..engine import (
SpreadEngine,
+ SpreadStrategyEngine,
APP_NAME,
EVENT_SPREAD_DATA,
EVENT_SPREAD_POS,
EVENT_SPREAD_LOG,
- EVENT_SPREAD_ALGO
+ EVENT_SPREAD_ALGO,
+ EVENT_SPREAD_STRATEGY
)
@@ -40,7 +42,9 @@ class SpreadManager(QtWidgets.QWidget):
""""""
self.setWindowTitle("价差交易")
- self.algo_dialog = SpreadAlgoDialog(self.spread_engine)
+ self.algo_dialog = SpreadAlgoWidget(self.spread_engine)
+ algo_tab = self.create_tab("交易", self.algo_dialog)
+ algo_tab.setMaximumWidth(300)
self.data_monitor = SpreadDataMonitor(
self.main_engine,
@@ -54,22 +58,19 @@ class SpreadManager(QtWidgets.QWidget):
self.spread_engine
)
- add_spread_button = QtWidgets.QPushButton("创建价差")
- add_spread_button.clicked.connect(self.add_spread)
+ self.strategy_monitor = SpreadStrategyMonitor(
+ self.spread_engine
+ )
- vbox1 = QtWidgets.QVBoxLayout()
- vbox1.addWidget(self.algo_dialog)
- vbox1.addStretch()
- vbox1.addWidget(add_spread_button)
-
- vbox2 = QtWidgets.QVBoxLayout()
- vbox2.addWidget(self.data_monitor)
- vbox2.addWidget(self.log_monitor)
+ grid = QtWidgets.QGridLayout()
+ grid.addWidget(self.create_tab("价差", self.data_monitor), 0, 0)
+ grid.addWidget(self.create_tab("日志", self.log_monitor), 1, 0)
+ grid.addWidget(self.create_tab("算法", self.algo_monitor), 0, 1)
+ grid.addWidget(self.create_tab("策略", self.strategy_monitor), 1, 1)
hbox = QtWidgets.QHBoxLayout()
- hbox.addLayout(vbox1)
- hbox.addLayout(vbox2)
- hbox.addWidget(self.algo_monitor)
+ hbox.addWidget(algo_tab)
+ hbox.addLayout(grid)
self.setLayout(hbox)
@@ -79,10 +80,11 @@ class SpreadManager(QtWidgets.QWidget):
self.showMaximized()
- def add_spread(self):
+ def create_tab(self, title: str, widget: QtWidgets.QWidget):
""""""
- dialog = SpreadDataDialog(self.spread_engine)
- dialog.exec_()
+ tab = QtWidgets.QTabWidget()
+ tab.addTab(widget, title)
+ return tab
class SpreadDataMonitor(BaseMonitor):
@@ -96,14 +98,14 @@ class SpreadDataMonitor(BaseMonitor):
headers = {
"name": {"display": "名称", "cell": BaseCell, "update": False},
- "price_formula": {"display": "定价", "cell": BaseCell, "update": False},
- "trading_formula": {"display": "交易", "cell": BaseCell, "update": False},
"bid_volume": {"display": "买量", "cell": BidCell, "update": True},
"bid_price": {"display": "买价", "cell": BidCell, "update": True},
"ask_price": {"display": "卖价", "cell": AskCell, "update": True},
"ask_volume": {"display": "卖量", "cell": AskCell, "update": True},
"net_pos": {"display": "净仓", "cell": PnlCell, "update": True},
"datetime": {"display": "时间", "cell": TimeCell, "update": True},
+ "price_formula": {"display": "定价", "cell": BaseCell, "update": False},
+ "trading_formula": {"display": "交易", "cell": BaseCell, "update": False},
}
def register_event(self):
@@ -143,7 +145,7 @@ class SpreadLogMonitor(QtWidgets.QTextEdit):
def process_log_event(self, event: Event):
""""""
log = event.data
- msg = f"{log.time}:{log.msg}"
+ msg = f"{log.time.strftime('%H:%M:%S')}:{log.msg}"
self.append(msg)
@@ -192,7 +194,7 @@ class SpreadAlgoMonitor(BaseMonitor):
self.spread_engine.stop_algo(algo.algoid)
-class SpreadAlgoDialog(QtWidgets.QDialog):
+class SpreadAlgoWidget(QtWidgets.QFrame):
""""""
def __init__(self, spread_engine: SpreadEngine):
@@ -200,12 +202,16 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
super().__init__()
self.spread_engine: SpreadEngine = spread_engine
+ self.strategy_engine: SpreadStrategyEngine = spread_engine.strategy_engine
self.init_ui()
+ self.update_class_combo()
def init_ui(self):
""""""
self.setWindowTitle("启动算法")
+ self.setFrameShape(self.Box)
+ self.setLineWidth(1)
self.name_line = QtWidgets.QLineEdit()
@@ -238,6 +244,26 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
["否", "是"]
)
+ self.class_combo = QtWidgets.QComboBox()
+
+ add_button = QtWidgets.QPushButton("添加策略")
+ add_button.clicked.connect(self.add_strategy)
+
+ init_button = QtWidgets.QPushButton("全部初始化")
+ init_button.clicked.connect(self.strategy_engine.init_all_strategies)
+
+ start_button = QtWidgets.QPushButton("全部启动")
+ start_button.clicked.connect(self.strategy_engine.start_all_strategies)
+
+ stop_button = QtWidgets.QPushButton("全部停止")
+ stop_button.clicked.connect(self.strategy_engine.stop_all_strategies)
+
+ add_spread_button = QtWidgets.QPushButton("创建价差")
+ add_spread_button.clicked.connect(self.add_spread)
+
+ remove_spread_button = QtWidgets.QPushButton("移除价差")
+ remove_spread_button.clicked.connect(self.remove_spread)
+
form = QtWidgets.QFormLayout()
form.addRow("价差", self.name_line)
form.addRow("方向", self.direction_combo)
@@ -248,7 +274,19 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
form.addRow("锁仓", self.lock_combo)
form.addRow(button_start)
- self.setLayout(form)
+ vbox = QtWidgets.QVBoxLayout()
+ vbox.addLayout(form)
+ vbox.addStretch()
+ vbox.addWidget(self.class_combo)
+ vbox.addWidget(add_button)
+ vbox.addWidget(init_button)
+ vbox.addWidget(start_button)
+ vbox.addWidget(stop_button)
+ vbox.addStretch()
+ vbox.addWidget(add_spread_button)
+ vbox.addWidget(remove_spread_button)
+
+ self.setLayout(vbox)
def start_algo(self):
""""""
@@ -269,6 +307,46 @@ class SpreadAlgoDialog(QtWidgets.QDialog):
name, direction, price, volume, payup, interval, lock
)
+ def add_spread(self):
+ """"""
+ dialog = SpreadDataDialog(self.spread_engine)
+ dialog.exec_()
+
+ def remove_spread(self):
+ """"""
+ pass
+
+ def update_class_combo(self):
+ """"""
+ self.class_combo.addItems(
+ self.strategy_engine.get_all_strategy_class_names()
+ )
+
+ def remove_strategy(self, strategy_name):
+ """"""
+ manager = self.managers.pop(strategy_name)
+ manager.deleteLater()
+
+ def add_strategy(self):
+ """"""
+ class_name = str(self.class_combo.currentText())
+ if not class_name:
+ return
+
+ parameters = self.strategy_engine.get_strategy_class_parameters(
+ class_name)
+ editor = SettingEditor(parameters, class_name=class_name)
+ n = editor.exec_()
+
+ if n == editor.Accepted:
+ setting = editor.get_setting()
+ spread_name = setting.pop("spread_name")
+ strategy_name = setting.pop("strategy_name")
+
+ self.strategy_engine.add_strategy(
+ class_name, strategy_name, spread_name, setting
+ )
+
class SpreadDataDialog(QtWidgets.QDialog):
""""""
@@ -354,12 +432,12 @@ class SpreadDataDialog(QtWidgets.QDialog):
leg_settings = {}
for d in self.leg_widgets:
try:
- vt_symbol = d["symbol"].text()
+ spread_name = d["symbol"].text()
price_multiplier = int(d["price"].text())
trading_multiplier = int(d["trading"].text())
- leg_settings[vt_symbol] = {
- "vt_symbol": vt_symbol,
+ leg_settings[spread_name] = {
+ "spread_name": spread_name,
"price_multiplier": price_multiplier,
"trading_multiplier": trading_multiplier
}
@@ -390,3 +468,287 @@ class SpreadDataDialog(QtWidgets.QDialog):
active_symbol
)
self.accept()
+
+
+class SpreadStrategyMonitor(QtWidgets.QScrollArea):
+ """"""
+
+ signal_strategy = QtCore.pyqtSignal(Event)
+
+ def __init__(self, spread_engine: SpreadEngine):
+ super().__init__()
+
+ self.strategy_engine = spread_engine.strategy_engine
+ self.main_engine = spread_engine.main_engine
+ self.event_engine = spread_engine.event_engine
+
+ self.managers = {}
+
+ self.init_ui()
+ self.register_event()
+
+ def init_ui(self):
+ """"""
+ self.scroll_layout = QtWidgets.QVBoxLayout()
+ self.scroll_layout.addStretch()
+
+ scroll_widget = QtWidgets.QWidget()
+ scroll_widget.setLayout(self.scroll_layout)
+
+ self.setWidgetResizable(True)
+ self.setWidget(scroll_widget)
+
+ def register_event(self):
+ """"""
+ self.signal_strategy.connect(self.process_strategy_event)
+
+ self.event_engine.register(
+ EVENT_SPREAD_STRATEGY, self.signal_strategy.emit
+ )
+
+ def process_strategy_event(self, event):
+ """
+ Update strategy status onto its monitor.
+ """
+ data = event.data
+ strategy_name = data["strategy_name"]
+
+ if strategy_name in self.managers:
+ manager = self.managers[strategy_name]
+ manager.update_data(data)
+ else:
+ manager = SpreadStrategyWidget(self.strategy_engine, data)
+ self.scroll_layout.insertWidget(0, manager)
+ self.managers[strategy_name] = manager
+
+
+class SpreadStrategyWidget(QtWidgets.QFrame):
+ """
+ Manager for a strategy
+ """
+
+ def __init__(
+ self,
+ strategy_engine: SpreadStrategyEngine,
+ data: dict
+ ):
+ """"""
+ super().__init__()
+
+ self.strategy_engine = strategy_engine
+
+ self.strategy_name = data["strategy_name"]
+ self._data = data
+
+ self.init_ui()
+
+ def init_ui(self):
+ """"""
+ self.setFixedHeight(300)
+ self.setFrameShape(self.Box)
+ self.setLineWidth(1)
+
+ init_button = QtWidgets.QPushButton("初始化")
+ init_button.clicked.connect(self.init_strategy)
+
+ start_button = QtWidgets.QPushButton("启动")
+ start_button.clicked.connect(self.start_strategy)
+
+ stop_button = QtWidgets.QPushButton("停止")
+ stop_button.clicked.connect(self.stop_strategy)
+
+ edit_button = QtWidgets.QPushButton("编辑")
+ edit_button.clicked.connect(self.edit_strategy)
+
+ remove_button = QtWidgets.QPushButton("移除")
+ remove_button.clicked.connect(self.remove_strategy)
+
+ strategy_name = self._data["strategy_name"]
+ spread_name = self._data["spread_name"]
+ class_name = self._data["class_name"]
+ author = self._data["author"]
+
+ label_text = (
+ f"{strategy_name} - {spread_name} ({class_name} by {author})"
+ )
+ label = QtWidgets.QLabel(label_text)
+ label.setAlignment(QtCore.Qt.AlignCenter)
+
+ self.parameters_monitor = StrategyDataMonitor(self._data["parameters"])
+ self.variables_monitor = StrategyDataMonitor(self._data["variables"])
+
+ hbox = QtWidgets.QHBoxLayout()
+ hbox.addWidget(init_button)
+ hbox.addWidget(start_button)
+ hbox.addWidget(stop_button)
+ hbox.addWidget(edit_button)
+ hbox.addWidget(remove_button)
+
+ vbox = QtWidgets.QVBoxLayout()
+ vbox.addWidget(label)
+ vbox.addLayout(hbox)
+ vbox.addWidget(self.parameters_monitor)
+ vbox.addWidget(self.variables_monitor)
+ self.setLayout(vbox)
+
+ def update_data(self, data: dict):
+ """"""
+ self._data = data
+
+ self.parameters_monitor.update_data(data["parameters"])
+ self.variables_monitor.update_data(data["variables"])
+
+ def init_strategy(self):
+ """"""
+ self.strategy_engine.init_strategy(self.strategy_name)
+
+ def start_strategy(self):
+ """"""
+ self.strategy_engine.start_strategy(self.strategy_name)
+
+ def stop_strategy(self):
+ """"""
+ self.strategy_engine.stop_strategy(self.strategy_name)
+
+ def edit_strategy(self):
+ """"""
+ strategy_name = self._data["strategy_name"]
+
+ parameters = self.strategy_engine.get_strategy_parameters(
+ strategy_name)
+ editor = SettingEditor(parameters, strategy_name=strategy_name)
+ n = editor.exec_()
+
+ if n == editor.Accepted:
+ setting = editor.get_setting()
+ self.strategy_engine.edit_strategy(strategy_name, setting)
+
+ def remove_strategy(self):
+ """"""
+ result = self.strategy_engine.remove_strategy(self.strategy_name)
+
+ # Only remove strategy gui manager if it has been removed from engine
+ if result:
+ self.spread_manager.remove_strategy(self.strategy_name)
+
+
+class StrategyDataMonitor(QtWidgets.QTableWidget):
+ """
+ Table monitor for parameters and variables.
+ """
+
+ def __init__(self, data: dict):
+ """"""
+ super().__init__()
+
+ self._data = data
+ self.cells = {}
+
+ self.init_ui()
+
+ def init_ui(self):
+ """"""
+ labels = list(self._data.keys())
+ self.setColumnCount(len(labels))
+ self.setHorizontalHeaderLabels(labels)
+
+ self.setRowCount(1)
+ self.verticalHeader().setSectionResizeMode(
+ QtWidgets.QHeaderView.Stretch
+ )
+ self.verticalHeader().setVisible(False)
+ self.setEditTriggers(self.NoEditTriggers)
+
+ for column, name in enumerate(self._data.keys()):
+ value = self._data[name]
+
+ cell = QtWidgets.QTableWidgetItem(str(value))
+ cell.setTextAlignment(QtCore.Qt.AlignCenter)
+
+ self.setItem(0, column, cell)
+ self.cells[name] = cell
+
+ def update_data(self, data: dict):
+ """"""
+ for name, value in data.items():
+ cell = self.cells[name]
+ cell.setText(str(value))
+
+
+class SettingEditor(QtWidgets.QDialog):
+ """
+ For creating new strategy and editing strategy parameters.
+ """
+
+ def __init__(
+ self, parameters: dict, strategy_name: str = "", class_name: str = ""
+ ):
+ """"""
+ super(SettingEditor, self).__init__()
+
+ self.parameters = parameters
+ self.strategy_name = strategy_name
+ self.class_name = class_name
+
+ self.edits = {}
+
+ self.init_ui()
+
+ def init_ui(self):
+ """"""
+ form = QtWidgets.QFormLayout()
+
+ # Add vt_symbol and name edit if add new strategy
+ if self.class_name:
+ self.setWindowTitle(f"添加策略:{self.class_name}")
+ button_text = "添加"
+ parameters = {"strategy_name": "", "vt_symbol": ""}
+ parameters.update(self.parameters)
+ else:
+ self.setWindowTitle(f"参数编辑:{self.strategy_name}")
+ button_text = "确定"
+ parameters = self.parameters
+
+ for name, value in parameters.items():
+ type_ = type(value)
+
+ edit = QtWidgets.QLineEdit(str(value))
+ if type_ is int:
+ validator = QtGui.QIntValidator()
+ edit.setValidator(validator)
+ elif type_ is float:
+ validator = QtGui.QDoubleValidator()
+ edit.setValidator(validator)
+
+ form.addRow(f"{name} {type_}", edit)
+
+ self.edits[name] = (edit, type_)
+
+ button = QtWidgets.QPushButton(button_text)
+ button.clicked.connect(self.accept)
+ form.addRow(button)
+
+ self.setLayout(form)
+
+ def get_setting(self):
+ """"""
+ setting = {}
+
+ if self.class_name:
+ setting["class_name"] = self.class_name
+
+ for name, tp in self.edits.items():
+ edit, type_ = tp
+ value_text = edit.text()
+
+ if type_ == bool:
+ if value_text == "True":
+ value = True
+ else:
+ value = False
+ else:
+ value = type_(value_text)
+
+ setting[name] = value
+
+ return setting
From 6a5d04e61f65dc4cf18a96c9413f8468ff9546c1 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 22:35:40 +0800
Subject: [PATCH 17/41] [Add] BasicSpreadStrategy for demo
---
examples/vn_trader/run.py | 2 +-
vnpy/app/spread_trading/__init__.py | 13 +-
vnpy/app/spread_trading/engine.py | 120 +++++++++++--
.../app/spread_trading/strategies/__init__.py | 0
.../strategies/basic_spread_strategy.py | 168 ++++++++++++++++++
vnpy/app/spread_trading/template.py | 16 +-
vnpy/app/spread_trading/ui/widget.py | 100 ++++++++---
7 files changed, 371 insertions(+), 48 deletions(-)
create mode 100644 vnpy/app/spread_trading/strategies/__init__.py
create mode 100644 vnpy/app/spread_trading/strategies/basic_spread_strategy.py
diff --git a/examples/vn_trader/run.py b/examples/vn_trader/run.py
index f063a687..229679e6 100644
--- a/examples/vn_trader/run.py
+++ b/examples/vn_trader/run.py
@@ -75,7 +75,7 @@ def main():
# main_engine.add_gateway(DaGateway)
main_engine.add_gateway(CoinbaseGateway)
- # main_engine.add_app(CtaStrategyApp)
+ main_engine.add_app(CtaStrategyApp)
# main_engine.add_app(CtaBacktesterApp)
# main_engine.add_app(CsvLoaderApp)
# main_engine.add_app(AlgoTradingApp)
diff --git a/vnpy/app/spread_trading/__init__.py b/vnpy/app/spread_trading/__init__.py
index 36ab7f9b..360a4390 100644
--- a/vnpy/app/spread_trading/__init__.py
+++ b/vnpy/app/spread_trading/__init__.py
@@ -1,8 +1,19 @@
from pathlib import Path
from vnpy.trader.app import BaseApp
+from vnpy.trader.object import (
+ OrderData,
+ TradeData
+)
-from .engine import SpreadEngine, APP_NAME
+from .engine import (
+ SpreadEngine,
+ APP_NAME,
+ SpreadData,
+ LegData,
+ SpreadStrategyTemplate,
+ SpreadAlgoTemplate
+)
class SpreadTradingApp(BaseApp):
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 84e65393..c3157b6a 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -48,6 +48,8 @@ class SpreadEngine(BaseEngine):
self.add_spread = self.data_engine.add_spread
self.remove_spread = self.data_engine.remove_spread
+ self.get_spread = self.data_engine.get_spread
+ self.get_all_spreads = self.data_engine.get_all_spreads
self.start_algo = self.algo_engine.start_algo
self.stop_algo = self.algo_engine.stop_algo
@@ -60,6 +62,13 @@ class SpreadEngine(BaseEngine):
self.data_engine.start()
self.algo_engine.start()
+ self.strategy_engine.start()
+
+ def stop(self):
+ """"""
+ self.data_engine.stop()
+ self.algo_engine.stop()
+ self.strategy_engine.stop()
def write_log(self, msg: str):
""""""
@@ -94,6 +103,10 @@ class SpreadDataEngine:
self.write_log("价差数据引擎启动成功")
+ def stop(self):
+ """"""
+ pass
+
def load_setting(self) -> None:
""""""
setting = load_json(self.setting_filename)
@@ -251,11 +264,20 @@ class SpreadDataEngine:
spread = self.spreads.pop(name)
- for leg in spread.legs:
+ for leg in spread.legs.values():
self.symbol_spread_map[leg.vt_symbol].remove(spread)
self.save_setting()
- self.write_log("价差删除成功:{}".format(name))
+ self.write_log("价差移除成功:{},重启后生效".format(name))
+
+ def get_spread(self, name: str) -> SpreadData:
+ """"""
+ spread = self.spreads.get(name, None)
+ return spread
+
+ def get_all_spreads(self) -> List[SpreadData]:
+ """"""
+ return list(self.spreads.values())
class SpreadAlgoEngine:
@@ -289,6 +311,11 @@ class SpreadAlgoEngine:
self.write_log("价差算法引擎启动成功")
+ def stop(self):
+ """"""
+ for algo in self.algos.values():
+ self.stop_algo(algo)
+
def register_event(self):
""""""
self.event_engine.register(EVENT_TICK, self.process_tick_event)
@@ -533,9 +560,10 @@ class SpreadStrategyEngine:
self.vt_tradeids: Set = set()
+ self.load_strategy_class()
+
def start(self):
""""""
- self.load_strategy_class()
self.load_strategy_setting()
self.register_event()
@@ -551,7 +579,7 @@ class SpreadStrategyEngine:
"""
path1 = Path(__file__).parent.joinpath("strategies")
self.load_strategy_class_from_folder(
- path1, "vnpy.app.cta_strategy.strategies")
+ path1, "vnpy.app.spread_trading.strategies")
path2 = Path.cwd().joinpath("strategies")
self.load_strategy_class_from_folder(path2, "strategies")
@@ -642,7 +670,8 @@ class SpreadStrategyEngine:
strategies = self.spread_strategy_map[spread.name]
for strategy in strategies:
- self.call_strategy_func(strategy, strategy.on_spread_data)
+ if strategy.inited:
+ self.call_strategy_func(strategy, strategy.on_spread_data)
def process_spread_pos_event(self, event: Event):
""""""
@@ -650,7 +679,8 @@ class SpreadStrategyEngine:
strategies = self.spread_strategy_map[spread.name]
for strategy in strategies:
- self.call_strategy_func(strategy, strategy.on_spread_pos)
+ if strategy.inited:
+ self.call_strategy_func(strategy, strategy.on_spread_pos)
def process_spread_algo_event(self, event: Event):
""""""
@@ -671,7 +701,7 @@ class SpreadStrategyEngine:
def process_trade_event(self, event: Event):
""""""
trade = event.data
- strategy = self.trade_strategy_map.get(trade.vt_orderid, None)
+ strategy = self.order_strategy_map.get(trade.vt_orderid, None)
if strategy:
self.call_strategy_func(strategy, strategy.on_trade, trade)
@@ -692,7 +722,7 @@ class SpreadStrategyEngine:
strategy.inited = False
msg = f"触发异常已停止\n{traceback.format_exc()}"
- self.write_log(msg, strategy)
+ self.write_strategy_log(strategy, msg)
def add_strategy(
self, class_name: str, strategy_name: str, spread_name: str, setting: dict
@@ -709,7 +739,12 @@ class SpreadStrategyEngine:
self.write_log(f"创建策略失败,找不到策略类{class_name}")
return
- strategy = strategy_class(self, strategy_name, spread_name, setting)
+ spread = self.spread_engine.get_spread(spread_name)
+ if not spread:
+ self.write_log(f"创建策略失败,找不到价差{spread_name}")
+ return
+
+ strategy = strategy_class(self, strategy_name, spread, setting)
self.strategies[strategy_name] = strategy
# Add vt_symbol to strategy map.
@@ -721,6 +756,37 @@ class SpreadStrategyEngine:
self.put_strategy_event(strategy)
+ def edit_strategy(self, strategy_name: str, setting: dict):
+ """
+ Edit parameters of a strategy.
+ """
+ strategy = self.strategies[strategy_name]
+ strategy.update_setting(setting)
+
+ self.update_strategy_setting(strategy_name, setting)
+ self.put_strategy_event(strategy)
+
+ def remove_strategy(self, strategy_name: str):
+ """
+ Remove a strategy.
+ """
+ strategy = self.strategies[strategy_name]
+ if strategy.trading:
+ self.write_log(f"策略{strategy.strategy_name}移除失败,请先停止")
+ return
+
+ # Remove setting
+ self.remove_strategy_setting(strategy_name)
+
+ # Remove from symbol strategy map
+ strategies = self.spread_strategy_map[strategy.spread_name]
+ strategies.remove(strategy)
+
+ # Remove from strategies
+ self.strategies.pop(strategy_name)
+
+ return True
+
def init_strategy(self, strategy_name: str):
""""""
strategy = self.strategies[strategy_name]
@@ -758,28 +824,48 @@ class SpreadStrategyEngine:
return
self.call_strategy_func(strategy, strategy.on_stop)
- strategy.trading = False
-
+
strategy.stop_all_algos()
strategy.cancel_all_orders()
+ strategy.trading = False
+
self.put_strategy_event(strategy)
def init_all_strategies(self):
""""""
- for strategy in self.strategies.values():
+ for strategy in self.strategies.keys():
self.init_strategy(strategy)
def start_all_strategies(self):
""""""
- for strategy in self.strategies.values():
+ for strategy in self.strategies.keys():
self.start_strategy(strategy)
def stop_all_strategies(self):
""""""
- for strategy in self.strategies.values():
+ for strategy in self.strategies.keys():
self.stop_strategy(strategy)
+ def get_strategy_class_parameters(self, class_name: str):
+ """
+ Get default parameters of a strategy class.
+ """
+ strategy_class = self.classes[class_name]
+
+ parameters = {}
+ for name in strategy_class.parameters:
+ parameters[name] = getattr(strategy_class, name)
+
+ return parameters
+
+ def get_strategy_parameters(self, strategy_name):
+ """
+ Get parameters of a strategy.
+ """
+ strategy = self.strategies[strategy_name]
+ return strategy.get_parameters()
+
def start_algo(
self,
strategy: SpreadStrategyTemplate,
@@ -864,7 +950,8 @@ class SpreadStrategyEngine:
""""""
order = self.main_engine.get_order(vt_orderid)
if not order:
- self.write_strategy_log(strategy, "撤单失败,找不到委托{}".format(vt_orderid))
+ self.write_strategy_log(
+ strategy, "撤单失败,找不到委托{}".format(vt_orderid))
return
req = order.create_cancel_request()
@@ -876,7 +963,8 @@ class SpreadStrategyEngine:
def put_strategy_event(self, strategy: SpreadStrategyTemplate):
""""""
- event = Event(EVENT_SPREAD_STRATEGY, strategy)
+ data = strategy.get_data()
+ event = Event(EVENT_SPREAD_STRATEGY, data)
self.event_engine.put(event)
def write_strategy_log(self, strategy: SpreadStrategyTemplate, msg: str):
diff --git a/vnpy/app/spread_trading/strategies/__init__.py b/vnpy/app/spread_trading/strategies/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/vnpy/app/spread_trading/strategies/basic_spread_strategy.py b/vnpy/app/spread_trading/strategies/basic_spread_strategy.py
new file mode 100644
index 00000000..b1213531
--- /dev/null
+++ b/vnpy/app/spread_trading/strategies/basic_spread_strategy.py
@@ -0,0 +1,168 @@
+from vnpy.app.spread_trading import (
+ SpreadStrategyTemplate,
+ SpreadAlgoTemplate,
+ SpreadData,
+ OrderData,
+ TradeData
+)
+
+
+class BasicSpreadStrategy(SpreadStrategyTemplate):
+ """"""
+
+ author = "用Python的交易员"
+
+ buy_price = 0.0
+ sell_price = 0.0
+ cover_price = 0.0
+ short_price = 0.0
+ max_pos = 0.0
+ payup = 10
+ interval = 5
+
+ spread_pos = 0.0
+ buy_algoid = ""
+ sell_algoid = ""
+ short_algoid = ""
+ cover_algoid = ""
+
+ parameters = [
+ "buy_price",
+ "sell_price",
+ "cover_price",
+ "short_price",
+ "max_pos",
+ "payup",
+ "interval"
+ ]
+ variables = [
+ "spread_pos",
+ "buy_algoid",
+ "sell_algoid",
+ "short_algoid",
+ "cover_algoid",
+ ]
+
+ def __init__(
+ self,
+ strategy_engine,
+ strategy_name: str,
+ spread: SpreadData,
+ setting: dict
+ ):
+ """"""
+ super().__init__(
+ strategy_engine, strategy_name, spread, setting
+ )
+
+ def on_init(self):
+ """
+ Callback when strategy is inited.
+ """
+ self.write_log("策略初始化")
+
+ def on_start(self):
+ """
+ Callback when strategy is started.
+ """
+ self.write_log("策略启动")
+
+ def on_stop(self):
+ """
+ Callback when strategy is stopped.
+ """
+ self.write_log("策略停止")
+
+ self.buy_algoid = ""
+ self.sell_algoid = ""
+ self.short_algoid = ""
+ self.cover_algoid = ""
+ self.put_event()
+
+ def on_spread_data(self):
+ """
+ Callback when spread price is updated.
+ """
+ self.spread_pos = self.get_spread_pos()
+
+ # No position
+ if not self.spread_pos:
+ # Start open algos
+ if not self.buy_algoid:
+ self.buy_algoid = self.start_long_algo(
+ self.buy_price, self.max_pos, self.payup, self.interval
+ )
+
+ if not self.short_algoid:
+ self.short_algoid = self.start_short_algo(
+ self.short_price, self.max_pos, self.payup, self.interval
+ )
+
+ # Stop close algos
+ if self.sell_algoid:
+ self.stop_algo(self.sell_algoid)
+
+ if self.cover_algoid:
+ self.stop_algo(self.cover_algoid)
+
+ # Long position
+ elif self.spread_pos > 0:
+ # Start sell close algo
+ if not self.sell_algoid:
+ self.sell_algoid = self.start_short_algo(
+ self.sell_price, self.spread_pos, self.payup, self.interval
+ )
+
+ # Stop buy open algo
+ if self.buy_algoid:
+ self.stop_algo(self.buy_algoid)
+
+ # Short position
+ elif self.spread_pos < 0:
+ # Start cover close algo
+ if not self.cover_algoid:
+ self.cover_algoid = self.start_long_algo(
+ self.cover_price, abs(
+ self.spread_pos), self.payup, self.interval
+ )
+
+ # Stop short open algo
+ if self.short_algoid:
+ self.stop_algo(self.short_algoid)
+
+ self.put_event()
+
+ def on_spread_pos(self):
+ """
+ Callback when spread position is updated.
+ """
+ self.spread_pos = self.get_spread_pos()
+ self.put_event()
+
+ def on_spread_algo(self, algo: SpreadAlgoTemplate):
+ """
+ Callback when algo status is updated.
+ """
+ if not algo.is_active():
+ if self.buy_algoid == algo.algoid:
+ self.buy_algoid = ""
+ elif self.sell_algoid == algo.algoid:
+ self.sell_algoid = ""
+ elif self.short_algoid == algo.algoid:
+ self.short_algoid = ""
+ else:
+ self.cover_algoid = ""
+
+ self.put_event()
+
+ def on_order(self, order: OrderData):
+ """
+ Callback when order status is updated.
+ """
+ pass
+
+ def on_trade(self, trade: TradeData):
+ """
+ Callback when new trade data is received.
+ """
+ pass
diff --git a/vnpy/app/spread_trading/template.py b/vnpy/app/spread_trading/template.py
index 88f572ed..de4ca343 100644
--- a/vnpy/app/spread_trading/template.py
+++ b/vnpy/app/spread_trading/template.py
@@ -147,11 +147,11 @@ class SpreadAlgoTemplate:
def update_timer(self):
""""""
self.count += 1
- if self.count < self.interval:
- return
- self.count = 0
+ if self.count > self.interval:
+ self.count = 0
+ self.on_interval()
- self.on_interval()
+ self.put_event()
def put_event(self):
""""""
@@ -358,7 +358,7 @@ class SpreadStrategyTemplate:
Callback when algo status is updated.
"""
if not algo.is_active() and algo.algoid in self.algoids:
- self.algoids.pop(algo.algoid)
+ self.algoids.remove(algo.algoid)
self.on_spread_algo(algo)
@@ -367,7 +367,7 @@ class SpreadStrategyTemplate:
Callback when order status is updated.
"""
if not order.is_active() and order.vt_orderid in self.vt_orderids:
- self.vt_orderids.pop(order.vt_orderid)
+ self.vt_orderids.remove(order.vt_orderid)
self.on_order(order)
@@ -461,7 +461,7 @@ class SpreadStrategyTemplate:
volume: float,
payup: int,
interval: int,
- lock: bool
+ lock: bool = False
) -> str:
""""""
return self.start_algo(Direction.LONG, price, volume, payup, interval, lock)
@@ -472,7 +472,7 @@ class SpreadStrategyTemplate:
volume: float,
payup: int,
interval: int,
- lock: bool
+ lock: bool = False
) -> str:
""""""
return self.start_algo(Direction.SHORT, price, volume, payup, interval, lock)
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index c5038f3d..35415892 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -34,6 +34,7 @@ class SpreadManager(QtWidgets.QWidget):
self.main_engine = main_engine
self.event_engine = event_engine
+
self.spread_engine = main_engine.get_engine(APP_NAME)
self.init_ui()
@@ -43,8 +44,8 @@ class SpreadManager(QtWidgets.QWidget):
self.setWindowTitle("价差交易")
self.algo_dialog = SpreadAlgoWidget(self.spread_engine)
- algo_tab = self.create_tab("交易", self.algo_dialog)
- algo_tab.setMaximumWidth(300)
+ algo_group = self.create_group("交易", self.algo_dialog)
+ algo_group.setMaximumWidth(300)
self.data_monitor = SpreadDataMonitor(
self.main_engine,
@@ -63,13 +64,13 @@ class SpreadManager(QtWidgets.QWidget):
)
grid = QtWidgets.QGridLayout()
- grid.addWidget(self.create_tab("价差", self.data_monitor), 0, 0)
- grid.addWidget(self.create_tab("日志", self.log_monitor), 1, 0)
- grid.addWidget(self.create_tab("算法", self.algo_monitor), 0, 1)
- grid.addWidget(self.create_tab("策略", self.strategy_monitor), 1, 1)
+ grid.addWidget(self.create_group("价差", self.data_monitor), 0, 0)
+ grid.addWidget(self.create_group("日志", self.log_monitor), 1, 0)
+ grid.addWidget(self.create_group("算法", self.algo_monitor), 0, 1)
+ grid.addWidget(self.create_group("策略", self.strategy_monitor), 1, 1)
hbox = QtWidgets.QHBoxLayout()
- hbox.addWidget(algo_tab)
+ hbox.addWidget(algo_group)
hbox.addLayout(grid)
self.setLayout(hbox)
@@ -77,14 +78,20 @@ class SpreadManager(QtWidgets.QWidget):
def show(self):
""""""
self.spread_engine.start()
-
+ self.algo_dialog.update_class_combo()
self.showMaximized()
- def create_tab(self, title: str, widget: QtWidgets.QWidget):
+ def create_group(self, title: str, widget: QtWidgets.QWidget):
""""""
- tab = QtWidgets.QTabWidget()
- tab.addTab(widget, title)
- return tab
+ group = QtWidgets.QGroupBox()
+
+ vbox = QtWidgets.QVBoxLayout()
+ vbox.addWidget(widget)
+
+ group.setLayout(vbox)
+ group.setTitle(title)
+
+ return group
class SpreadDataMonitor(BaseMonitor):
@@ -145,7 +152,7 @@ class SpreadLogMonitor(QtWidgets.QTextEdit):
def process_log_event(self, event: Event):
""""""
log = event.data
- msg = f"{log.time.strftime('%H:%M:%S')}:{log.msg}"
+ msg = f"{log.time.strftime('%H:%M:%S')}\t{log.msg}"
self.append(msg)
@@ -205,7 +212,6 @@ class SpreadAlgoWidget(QtWidgets.QFrame):
self.strategy_engine: SpreadStrategyEngine = spread_engine.strategy_engine
self.init_ui()
- self.update_class_combo()
def init_ui(self):
""""""
@@ -314,7 +320,8 @@ class SpreadAlgoWidget(QtWidgets.QFrame):
def remove_spread(self):
""""""
- pass
+ dialog = SpreadRemoveDialog(self.spread_engine)
+ dialog.exec_()
def update_class_combo(self):
""""""
@@ -470,7 +477,44 @@ class SpreadDataDialog(QtWidgets.QDialog):
self.accept()
-class SpreadStrategyMonitor(QtWidgets.QScrollArea):
+class SpreadRemoveDialog(QtWidgets.QDialog):
+ """"""
+
+ def __init__(self, spread_engine: SpreadEngine):
+ """"""
+ super().__init__()
+
+ self.spread_engine: SpreadEngine = spread_engine
+
+ self.init_ui()
+
+ def init_ui(self):
+ """"""
+ self.setWindowTitle("移除价差")
+ self.setMinimumWidth(300)
+
+ self.name_combo = QtWidgets.QComboBox()
+ spreads = self.spread_engine.get_all_spreads()
+ for spread in spreads:
+ self.name_combo.addItem(spread.name)
+
+ button_remove = QtWidgets.QPushButton("移除")
+ button_remove.clicked.connect(self.remove_spread)
+
+ hbox = QtWidgets.QHBoxLayout()
+ hbox.addWidget(self.name_combo)
+ hbox.addWidget(button_remove)
+
+ self.setLayout(hbox)
+
+ def remove_spread(self):
+ """"""
+ spread_name = self.name_combo.currentText()
+ self.spread_engine.remove_spread(spread_name)
+ self.accept()
+
+
+class SpreadStrategyMonitor(QtWidgets.QWidget):
""""""
signal_strategy = QtCore.pyqtSignal(Event)
@@ -495,8 +539,13 @@ class SpreadStrategyMonitor(QtWidgets.QScrollArea):
scroll_widget = QtWidgets.QWidget()
scroll_widget.setLayout(self.scroll_layout)
- self.setWidgetResizable(True)
- self.setWidget(scroll_widget)
+ scroll_area = QtWidgets.QScrollArea()
+ scroll_area.setWidgetResizable(True)
+ scroll_area.setWidget(scroll_widget)
+
+ vbox = QtWidgets.QVBoxLayout()
+ vbox.addWidget(scroll_area)
+ self.setLayout(vbox)
def register_event(self):
""""""
@@ -517,10 +566,15 @@ class SpreadStrategyMonitor(QtWidgets.QScrollArea):
manager = self.managers[strategy_name]
manager.update_data(data)
else:
- manager = SpreadStrategyWidget(self.strategy_engine, data)
+ manager = SpreadStrategyWidget(self, self.strategy_engine, data)
self.scroll_layout.insertWidget(0, manager)
self.managers[strategy_name] = manager
+ def remove_strategy(self, strategy_name):
+ """"""
+ manager = self.managers.pop(strategy_name)
+ manager.deleteLater()
+
class SpreadStrategyWidget(QtWidgets.QFrame):
"""
@@ -529,12 +583,14 @@ class SpreadStrategyWidget(QtWidgets.QFrame):
def __init__(
self,
+ strategy_monitor: SpreadStrategyMonitor,
strategy_engine: SpreadStrategyEngine,
data: dict
):
""""""
super().__init__()
+ self.strategy_monitor = strategy_monitor
self.strategy_engine = strategy_engine
self.strategy_name = data["strategy_name"]
@@ -629,7 +685,7 @@ class SpreadStrategyWidget(QtWidgets.QFrame):
# Only remove strategy gui manager if it has been removed from engine
if result:
- self.spread_manager.remove_strategy(self.strategy_name)
+ self.strategy_monitor.remove_strategy(self.strategy_name)
class StrategyDataMonitor(QtWidgets.QTableWidget):
@@ -698,11 +754,11 @@ class SettingEditor(QtWidgets.QDialog):
""""""
form = QtWidgets.QFormLayout()
- # Add vt_symbol and name edit if add new strategy
+ # Add spread_name and name edit if add new strategy
if self.class_name:
self.setWindowTitle(f"添加策略:{self.class_name}")
button_text = "添加"
- parameters = {"strategy_name": "", "vt_symbol": ""}
+ parameters = {"strategy_name": "", "spread_name": ""}
parameters.update(self.parameters)
else:
self.setWindowTitle(f"参数编辑:{self.strategy_name}")
From 06797b475f54a0d2c1256b058c80869685b3ce82 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 22:48:20 +0800
Subject: [PATCH 18/41] [Mod] update README.md
---
.github/ISSUE_TEMPLATE.md | 4 +-
README.md | 114 +++++++++++++++++++++-----------------
vnpy/__init__.py | 2 +-
3 files changed, 67 insertions(+), 53 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index d0d205ee..f37c5876 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,8 +1,8 @@
## 环境
* 操作系统: 如Windows 10或者Ubuntu 18.04
-* Anaconda版本: 如Anaconda 18.12 Python 3.7 64位
-* vn.py版本: 如v2.0发行版或者dev branch 20190101(下载日期)
+* Python版本: 如VNStudio-2.0.6
+* vn.py版本: 如v2.0.5发行版或者dev branch 20190101(下载日期)
## Issue类型
三选一:Bug/Enhancement/Question
diff --git a/README.md b/README.md
index 4ed0dc18..93e04168 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -20,9 +20,9 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
-在使用vn.py进行二次开发(策略、模块等)的过程中有任何疑问,请查看[**vn.py项目文档**](https://www.vnpy.com/docs/cn/index.html),如果无法解决请前往[**官方社区论坛**](https://www.vnpy.com/forum/)的【提问求助】板块寻求帮助,也欢迎在【经验分享】板块分享你的使用心得!
+在使用vn.py进行二次开发(策略、模块等)的过程中有任何疑问,请查看[**vn.py项目文档**](https://www.vnpy.com/docs/cn/index.html),如果无法解决请前往[**官方社区论坛**](https://www.vnpy.com/forum/)的【提问求助】板块寻求帮助,也欢迎在【经验分享】板块分享你的使用心得!
-2.0版本基于Python 3.7全新重构开发,如需Python 2上的版本请点击:[长期支持版本v1.9.2 LTS](https://github.com/vnpy/vnpy/tree/v1.9.2-LTS)。
+2.0版本基于Python 3.7全新重构开发,如需Python 2上的版本请点击:[长期支持版本v1.9.2 LTS](https://github.com/vnpy/vnpy/tree/v1.9.2-LTS)。
## 功能特点
@@ -32,49 +32,61 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
* 国内市场
- * CTP(ctp):国内期货、期权
+ * CTP(ctp):国内期货、期权
- * CTP Mini(mini):国内期货、期权
+ * CTP Mini(mini):国内期货、期权
- * 飞马(femas):国内期货
+ * CTP证券(sopt):ETF期权
- * 宽睿(oes):国内证券(A股)
+ * 飞马(femas):国内期货
- * 中泰XTP(xtp):国内证券(A股)
+ * 宽睿(oes):国内证券(A股)
- * 华鑫奇点(tora):国内证券(A股)
+ * 中泰XTP(xtp):国内证券(A股)
+
+ * 华鑫奇点(tora):国内证券(A股)
+
+ * 鑫管家(xgj):期货资管
+
+ * 融航(rohon):期货资管
* 海外市场
-
- * 富途证券(futu):港股、美股
- * 老虎证券(tiger):全球证券、期货、期权、外汇等
+ * 富途证券(futu):港股、美股
- * Interactive Brokers(ib):全球证券、期货、期权、外汇等
+ * 老虎证券(tiger):全球证券、期货、期权、外汇等
- * 易盛9.0外盘(tap):全球期货
+ * Interactive Brokers(ib):全球证券、期货、期权、外汇等
+
+ * 易盛9.0外盘(tap):全球期货
+
+ * 直达期货(da):全球期货
* 数字货币
- * BitMEX(bitmex):数字货币期货、期权、永续合约
+ * BitMEX(bitmex):数字货币期货、期权、永续合约
- * OKEX合约(okexf):数字货币期货
+ * OKEX永续(okexs):数字货币永续合约
- * 火币合约(hbdm):数字货币期货
+ * OKEX合约(okexf):数字货币期货
- * 币安(binance):数字货币现货
+ * 火币合约(hbdm):数字货币期货
- * OKEX(okex):数字货币现货
+ * 币安(binance):数字货币现货
- * 火币(huobi):数字货币现货
+ * OKEX(okex):数字货币现货
- * Bitfinex(bitfinex):数字货币现货
+ * 火币(huobi):数字货币现货
- * 1Token(onetoken):数字货币券商(现货、期货)
+ * Bitfinex(bitfinex):数字货币现货
+
+ * Coinbase(coinbase):数字货币现货
+
+ * 1Token(onetoken):数字货币券商(现货、期货)
* 特殊应用
- * RPC服务(rpc):跨进程通讯接口,用于分布式架构
+ * RPC服务(rpc):跨进程通讯接口,用于分布式架构
3. 开箱即用的各类量化策略交易应用(vnpy.app):
@@ -82,6 +94,8 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
* cta_backtester:CTA策略回测模块,无需使用Jupyter Notebook,直接使用图形界面直接进行策略回测分析、参数优化等相关工作
+ * spread_trading:价差交易模块,支持自定义价差,实时计算价差行情和持仓,支持半自动价差算法交易以及全自动价差策略交易两种模式
+
* algo_trading:算法交易模块,提供多种常用的智能交易算法:TWAP、Sniper、Iceberg、BestLimit等等,支持常用算法配置保存
* script_trader:脚本策略模块,针对多标的组合类交易策略设计,同时也可以直接在命令行中实现REPL指令形式的交易,不支持回测功能
@@ -102,19 +116,19 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
7. Python高性能K线图表(vnpy.chart),支持大数据量图表显示以及实时数据更新功能。
-8. [社区论坛](http://www.vnpy.com)和[知乎专栏](http://zhuanlan.zhihu.com/vn-py),内容包括vn.py项目的开发教程和Python在量化交易领域的应用研究等内容。
+8. [社区论坛](http://www.vnpy.com)和[知乎专栏](http://zhuanlan.zhihu.com/vn-py),内容包括vn.py项目的开发教程和Python在量化交易领域的应用研究等内容。
9. 官方交流群262656087(QQ),管理严格(定期清除长期潜水的成员),入群费将捐赠给vn.py社区基金。
## 环境准备
-* 推荐使用vn.py团队为量化交易专门打造的Python发行版[VNStudio-2.0.4](https://download.vnpy.com/vnstudio-2.0.4-r.exe),内置了最新版的vn.py框架以及VN Station量化管理平台,无需手动安装
+* 推荐使用vn.py团队为量化交易专门打造的Python发行版[VNStudio-2.0.6](https://download.vnpy.com/vnstudio-2.0.6.exe),内置了最新版的vn.py框架以及VN Station量化管理平台,无需手动安装
* 支持的系统版本:Windows 7以上/Windows Server 2008以上/Ubuntu 18.04 LTS
* 支持的Python版本:Python 3.7 64位(**注意必须是Python 3.7 64位版本**)
## 安装步骤
-在[这里](https://github.com/vnpy/vnpy/releases)下载最新版本,解压后运行以下命令安装:
+在[这里](https://github.com/vnpy/vnpy/releases)下载最新版本,解压后运行以下命令安装:
**Windows**
@@ -127,9 +141,9 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
## 使用指南
-1. 在[SimNow](http://www.simnow.com.cn/)注册CTP仿真账号,并在[该页面](http://www.simnow.com.cn/product.action)获取经纪商代码以及交易行情服务器地址。
+1. 在[SimNow](http://www.simnow.com.cn/)注册CTP仿真账号,并在[该页面](http://www.simnow.com.cn/product.action)获取经纪商代码以及交易行情服务器地址。
-2. 在[vn.py社区论坛](https://www.vnpy.com/forum/)注册获得VN Station账号密码(论坛账号密码即是)
+2. 在[vn.py社区论坛](https://www.vnpy.com/forum/)注册获得VN Station账号密码(论坛账号密码即是)
3. 启动VN Station(安装VNConda后会在桌面自动创建快捷方式),输入上一步的账号密码登录
@@ -153,24 +167,24 @@ from vnpy.gateway.ctp import CtpGateway
from vnpy.app.cta_strategy import CtaStrategyApp
from vnpy.app.cta_backtester import CtaBacktesterApp
-def main():
+def main():
"""Start VN Trader"""
- qapp = create_qapp()
+ qapp = create_qapp()
- event_engine = EventEngine()
- main_engine = MainEngine(event_engine)
+ event_engine = EventEngine()
+ main_engine = MainEngine(event_engine)
- main_engine.add_gateway(CtpGateway)
- main_engine.add_app(CtaStrategyApp)
- main_engine.add_app(CtaBacktesterApp)
+ main_engine.add_gateway(CtpGateway)
+ main_engine.add_app(CtaStrategyApp)
+ main_engine.add_app(CtaBacktesterApp)
- main_window = MainWindow(main_engine, event_engine)
- main_window.showMaximized()
+ main_window = MainWindow(main_engine, event_engine)
+ main_window.showMaximized()
- qapp.exec()
+ qapp.exec()
if __name__ == "__main__":
- main()
+ main()
```
在该目录下打开CMD(按住Shift->点击鼠标右键->在此处打开命令窗口/PowerShell)后运行下列命令启动VN Trader:
@@ -179,27 +193,27 @@ if __name__ == "__main__":
## 贡献代码
-vn.py使用Github托管其源代码,如果希望贡献代码请使用github的PR(Pull Request)的流程:
+vn.py使用Github托管其源代码,如果希望贡献代码请使用github的PR(Pull Request)的流程:
-1. [创建 Issue](https://github.com/vnpy/vnpy/issues/new) - 对于较大的改动(如新功能,大型重构等)最好先开issue讨论一下,较小的improvement(如文档改进,bugfix等)直接发PR即可
+1. [创建 Issue](https://github.com/vnpy/vnpy/issues/new) - 对于较大的改动(如新功能,大型重构等)最好先开issue讨论一下,较小的improvement(如文档改进,bugfix等)直接发PR即可
-2. Fork [vn.py](https://github.com/vnpy/vnpy) - 点击右上角**Fork**按钮
+2. Fork [vn.py](https://github.com/vnpy/vnpy) - 点击右上角**Fork**按钮
3. Clone你自己的fork: ```git clone https://github.com/$userid/vnpy.git```
- * 如果你的fork已经过时,需要手动sync:[https://help.github.com/articles/syncing-a-fork/](https://help.github.com/articles/syncing-a-fork/)
+ * 如果你的fork已经过时,需要手动sync:[https://help.github.com/articles/syncing-a-fork/](https://help.github.com/articles/syncing-a-fork/)
4. 从**dev**创建你自己的feature branch: ```git checkout -b $my_feature_branch dev```
5. 在$my_feature_branch上修改并将修改push到你的fork上
-6. 创建从你的fork的$my_feature_branch分支到主项目的**dev**分支的[Pull Request] - [在此](https://github.com/vnpy/vnpy/compare?expand=1)点击**compare across forks**,选择需要的fork和branch创建PR
+6. 创建从你的fork的$my_feature_branch分支到主项目的**dev**分支的[Pull Request] - [在此](https://github.com/vnpy/vnpy/compare?expand=1)点击**compare across forks**,选择需要的fork和branch创建PR
7. 等待review, 需要继续改进,或者被Merge!
在提交代码的时候,请遵守以下规则,以提高代码质量:
- * 使用[autopep8](https://github.com/hhatto/autopep8)格式化你的代码。运行```autopep8 --in-place --recursive . ```即可。
- * 使用[flake8](https://pypi.org/project/flake8/)检查你的代码,确保没有error和warning。在项目根目录下运行```flake8```即可。
+ * 使用[autopep8](https://github.com/hhatto/autopep8)格式化你的代码。运行```autopep8 --in-place --recursive . ```即可。
+ * 使用[flake8](https://pypi.org/project/flake8/)检查你的代码,确保没有error和warning。在项目根目录下运行```flake8```即可。
@@ -217,10 +231,10 @@ vn.py使用Github托管其源代码,如果希望贡献代码请使用github的
## 其他内容
-* [获取帮助](https://github.com/vnpy/vnpy/blob/dev/docs/SUPPORT.md)
-* [社区行为准侧](https://github.com/vnpy/vnpy/blob/dev/docs/CODE_OF_CONDUCT.md)
-* [Issue模板](https://github.com/vnpy/vnpy/blob/dev/docs/ISSUE_TEMPLATE.md)
-* [PR模板](https://github.com/vnpy/vnpy/blob/dev/docs/PULL_REQUEST_TEMPLATE.md)
+* [获取帮助](https://github.com/vnpy/vnpy/blob/dev/docs/SUPPORT.md)
+* [社区行为准侧](https://github.com/vnpy/vnpy/blob/dev/docs/CODE_OF_CONDUCT.md)
+* [Issue模板](https://github.com/vnpy/vnpy/blob/dev/docs/ISSUE_TEMPLATE.md)
+* [PR模板](https://github.com/vnpy/vnpy/blob/dev/docs/PULL_REQUEST_TEMPLATE.md)
diff --git a/vnpy/__init__.py b/vnpy/__init__.py
index ff6ef86d..962c851b 100644
--- a/vnpy/__init__.py
+++ b/vnpy/__init__.py
@@ -1 +1 @@
-__version__ = "2.0.6"
+__version__ = "2.0.7"
From 1053eb2d239b9d5480f939b17a7bb3c7947b6e84 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 22:56:40 +0800
Subject: [PATCH 19/41] [Mod] close #1938
---
vnpy/gateway/ctp/ctp_gateway.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/vnpy/gateway/ctp/ctp_gateway.py b/vnpy/gateway/ctp/ctp_gateway.py
index d08f3864..a72f4598 100644
--- a/vnpy/gateway/ctp/ctp_gateway.py
+++ b/vnpy/gateway/ctp/ctp_gateway.py
@@ -717,6 +717,10 @@ class CtpTdApi(TdApi):
"""
self.order_ref += 1
+ if req.offset not in OFFSET_VT2CTP:
+ self.gateway.write_log("请选择开平方向")
+ return ""
+
ctp_req = {
"InstrumentID": req.symbol,
"ExchangeID": req.exchange.value,
From cf2a7ad2f7004f7bf96635dea72c683c3fbb3a50 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:01:28 +0800
Subject: [PATCH 20/41] [Mod] close #1950
---
vnpy/gateway/hbdm/hbdm_gateway.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/vnpy/gateway/hbdm/hbdm_gateway.py b/vnpy/gateway/hbdm/hbdm_gateway.py
index 663feaf0..b8de5b76 100644
--- a/vnpy/gateway/hbdm/hbdm_gateway.py
+++ b/vnpy/gateway/hbdm/hbdm_gateway.py
@@ -63,6 +63,9 @@ ORDERTYPE_VT2HBDM = {
ORDERTYPE_HBDM2VT = {v: k for k, v in ORDERTYPE_VT2HBDM.items()}
ORDERTYPE_HBDM2VT[1] = OrderType.LIMIT
ORDERTYPE_HBDM2VT[3] = OrderType.MARKET
+ORDERTYPE_HBDM2VT[4] = OrderType.MARKET
+ORDERTYPE_HBDM2VT[5] = OrderType.STOP
+ORDERTYPE_HBDM2VT[6] = OrderType.LIMIT
DIRECTION_VT2HBDM = {
Direction.LONG: "buy",
From 1356aac89447008c810e694850f87395994d36d9 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:11:17 +0800
Subject: [PATCH 21/41] [Mod] close #1937
---
.../cta_strategy/strategies/turtle_signal_strategy.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/vnpy/app/cta_strategy/strategies/turtle_signal_strategy.py b/vnpy/app/cta_strategy/strategies/turtle_signal_strategy.py
index aa608b95..bcbe766d 100644
--- a/vnpy/app/cta_strategy/strategies/turtle_signal_strategy.py
+++ b/vnpy/app/cta_strategy/strategies/turtle_signal_strategy.py
@@ -78,7 +78,12 @@ class TurtleSignalStrategy(CtaTemplate):
if not self.am.inited:
return
- self.entry_up, self.entry_down = self.am.donchian(self.entry_window)
+ # Only calculates new entry channel when no position holding
+ if not self.pos:
+ self.entry_up, self.entry_down = self.am.donchian(
+ self.entry_window
+ )
+
self.exit_up, self.exit_down = self.am.donchian(self.exit_window)
if not self.pos:
@@ -92,13 +97,13 @@ class TurtleSignalStrategy(CtaTemplate):
self.send_buy_orders(self.entry_up)
self.send_short_orders(self.entry_down)
elif self.pos > 0:
- self.send_buy_orders(self.long_entry)
+ self.send_buy_orders(self.entry_up)
sell_price = max(self.long_stop, self.exit_down)
self.sell(sell_price, abs(self.pos), True)
elif self.pos < 0:
- self.send_short_orders(self.short_entry)
+ self.send_short_orders(self.entry_down)
cover_price = min(self.short_stop, self.exit_up)
self.cover(cover_price, abs(self.pos), True)
From 8baafc5cf914b604dfd0e43f6b4e6db133a37cc0 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:14:43 +0800
Subject: [PATCH 22/41] [Add] CBOE exchange enum
---
vnpy/trader/constant.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/vnpy/trader/constant.py b/vnpy/trader/constant.py
index 1ebb2aa8..f7159a53 100644
--- a/vnpy/trader/constant.py
+++ b/vnpy/trader/constant.py
@@ -100,6 +100,7 @@ class Exchange(Enum):
HKFE = "HKFE" # Hong Kong Futures Exchange
SGX = "SGX" # Singapore Global Exchange
CBOT = "CBT" # Chicago Board of Trade
+ CBOE = "CBOE" # Chicago Board Options Exchange
DME = "DME" # Dubai Mercantile Exchange
EUREX = "EUX" # Eurex Exchange
APEX = "APEX" # Asia Pacific Exchange
From 0c1dc817b9b456d1f8cd344439f2f7e39bcada00 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:28:36 +0800
Subject: [PATCH 23/41] [Fic] close #2021
---
examples/vn_trader/run.py | 2 +-
vnpy/app/cta_backtester/engine.py | 28 +++++++++++++++++-----------
2 files changed, 18 insertions(+), 12 deletions(-)
diff --git a/examples/vn_trader/run.py b/examples/vn_trader/run.py
index 229679e6..f6f7a376 100644
--- a/examples/vn_trader/run.py
+++ b/examples/vn_trader/run.py
@@ -76,7 +76,7 @@ def main():
main_engine.add_gateway(CoinbaseGateway)
main_engine.add_app(CtaStrategyApp)
- # main_engine.add_app(CtaBacktesterApp)
+ main_engine.add_app(CtaBacktesterApp)
# main_engine.add_app(CsvLoaderApp)
# main_engine.add_app(AlgoTradingApp)
# main_engine.add_app(DataRecorderApp)
diff --git a/vnpy/app/cta_backtester/engine.py b/vnpy/app/cta_backtester/engine.py
index b64045a2..c501f350 100644
--- a/vnpy/app/cta_backtester/engine.py
+++ b/vnpy/app/cta_backtester/engine.py
@@ -354,18 +354,24 @@ class BacktesterEngine(BaseEngine):
contract = self.main_engine.get_contract(vt_symbol)
- # If history data provided in gateway, then query
- if contract and contract.history_data:
- data = self.main_engine.query_history(req, contract.gateway_name)
- # Otherwise use RQData to query data
- else:
- data = rqdata_client.query_history(req)
+ try:
+ # If history data provided in gateway, then query
+ if contract and contract.history_data:
+ data = self.main_engine.query_history(
+ req, contract.gateway_name
+ )
+ # Otherwise use RQData to query data
+ else:
+ data = rqdata_client.query_history(req)
- if data:
- database_manager.save_bar_data(data)
- self.write_log(f"{vt_symbol}-{interval}历史数据下载完成")
- else:
- self.write_log(f"数据下载失败,无法获取{vt_symbol}的历史数据")
+ if data:
+ database_manager.save_bar_data(data)
+ self.write_log(f"{vt_symbol}-{interval}历史数据下载完成")
+ else:
+ self.write_log(f"数据下载失败,无法获取{vt_symbol}的历史数据")
+ except Exception:
+ msg = f"数据下载失败,触发异常:\n{traceback.format_exc()}"
+ self.write_log(msg)
# Clear thread object handler.
self.thread = None
From 38d090bed4965d2afa0c7d2035d5df1b27fb5a9d Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:30:30 +0800
Subject: [PATCH 24/41] [Mod] close #2019
---
vnpy/gateway/okexf/okexf_gateway.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/vnpy/gateway/okexf/okexf_gateway.py b/vnpy/gateway/okexf/okexf_gateway.py
index e1b9d3cd..abcb660b 100644
--- a/vnpy/gateway/okexf/okexf_gateway.py
+++ b/vnpy/gateway/okexf/okexf_gateway.py
@@ -561,7 +561,7 @@ class OkexfRestApi(RestClient):
for l in data:
ts, o, h, l, c, v, _ = l
- dt = datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S.%fZ")
+ dt = utc_to_local(ts)
bar = BarData(
symbol=req.symbol,
exchange=req.exchange,
From 69f9a21c3e5ad3ea3cc10a279edfd1f757c23205 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:41:10 +0800
Subject: [PATCH 25/41] [Fix] close #2037
---
examples/vn_trader/run.py | 4 ++--
vnpy/gateway/bitfinex/bitfinex_gateway.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/examples/vn_trader/run.py b/examples/vn_trader/run.py
index f6f7a376..1c31e879 100644
--- a/examples/vn_trader/run.py
+++ b/examples/vn_trader/run.py
@@ -18,7 +18,7 @@ from vnpy.gateway.bitmex import BitmexGateway
# from vnpy.gateway.oes import OesGateway
# from vnpy.gateway.okex import OkexGateway
# from vnpy.gateway.huobi import HuobiGateway
-# from vnpy.gateway.bitfinex import BitfinexGateway
+from vnpy.gateway.bitfinex import BitfinexGateway
# from vnpy.gateway.onetoken import OnetokenGateway
from vnpy.gateway.okexf import OkexfGateway
from vnpy.gateway.okexs import OkexsGateway
@@ -63,7 +63,7 @@ def main():
# main_engine.add_gateway(OesGateway)
# main_engine.add_gateway(OkexGateway)
# main_engine.add_gateway(HuobiGateway)
- # main_engine.add_gateway(BitfinexGateway)
+ main_engine.add_gateway(BitfinexGateway)
# main_engine.add_gateway(OnetokenGateway)
# main_engine.add_gateway(OkexfGateway)
# main_engine.add_gateway(HbdmGateway)
diff --git a/vnpy/gateway/bitfinex/bitfinex_gateway.py b/vnpy/gateway/bitfinex/bitfinex_gateway.py
index 47d58fdd..ac2d614e 100644
--- a/vnpy/gateway/bitfinex/bitfinex_gateway.py
+++ b/vnpy/gateway/bitfinex/bitfinex_gateway.py
@@ -558,7 +558,7 @@ class BitfinexWebsocketApi(WebsocketClient):
# ASK
ask_keys = ask.keys()
- askPriceList = sorted(ask_keys, reverse=True)
+ askPriceList = sorted(ask_keys)
tick.ask_price_1 = askPriceList[0]
tick.ask_price_2 = askPriceList[1]
From 15bdeea4007481f1e2d4662fc6287f4301f1c6e0 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:46:05 +0800
Subject: [PATCH 26/41] [Mod] close #2060
---
vnpy/app/cta_backtester/ui/widget.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/vnpy/app/cta_backtester/ui/widget.py b/vnpy/app/cta_backtester/ui/widget.py
index 70752e05..6412a99a 100644
--- a/vnpy/app/cta_backtester/ui/widget.py
+++ b/vnpy/app/cta_backtester/ui/widget.py
@@ -332,7 +332,7 @@ class BacktesterManager(QtWidgets.QWidget):
end_date = self.end_date_edit.date()
start = datetime(start_date.year(), start_date.month(), start_date.day())
- end = datetime(end_date.year(), end_date.month(), end_date.day())
+ end = datetime(end_date.year(), end_date.month(), end_date.day(), 23, 59, 59)
self.backtester_engine.start_downloading(
vt_symbol,
From 6279ab4ddf6baf554bdbed2ce8949e3c636289bf Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Tue, 17 Sep 2019 23:58:16 +0800
Subject: [PATCH 27/41] [Mod] update README.md
---
README.md | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 93e04168..b6ed4ad0 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,9 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
-在使用vn.py进行二次开发(策略、模块等)的过程中有任何疑问,请查看[**vn.py项目文档**](https://www.vnpy.com/docs/cn/index.html),如果无法解决请前往[**官方社区论坛**](https://www.vnpy.com/forum/)的【提问求助】板块寻求帮助,也欢迎在【经验分享】板块分享你的使用心得!
+在使用vn.py进行二次开发(策略、模块等)的过程中有任何疑问,请查看[**vn.py项目文档**](https://www.vnpy.com/docs/cn/index.html),如果无法解决请前往[**官方社区论坛**](https://www.vnpy.com/forum/)的【提问求助】板块寻求帮助,也欢迎在【经验分享】板块分享你的使用心得!
-2.0版本基于Python 3.7全新重构开发,如需Python 2上的版本请点击:[长期支持版本v1.9.2 LTS](https://github.com/vnpy/vnpy/tree/v1.9.2-LTS)。
+2.0版本基于Python 3.7全新重构开发,如需Python 2上的版本请点击:[长期支持版本v1.9.2 LTS](https://github.com/vnpy/vnpy/tree/v1.9.2-LTS)。
## 功能特点
@@ -116,19 +116,19 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
7. Python高性能K线图表(vnpy.chart),支持大数据量图表显示以及实时数据更新功能。
-8. [社区论坛](http://www.vnpy.com)和[知乎专栏](http://zhuanlan.zhihu.com/vn-py),内容包括vn.py项目的开发教程和Python在量化交易领域的应用研究等内容。
+8. [社区论坛](http://www.vnpy.com)和[知乎专栏](http://zhuanlan.zhihu.com/vn-py),内容包括vn.py项目的开发教程和Python在量化交易领域的应用研究等内容。
9. 官方交流群262656087(QQ),管理严格(定期清除长期潜水的成员),入群费将捐赠给vn.py社区基金。
## 环境准备
-* 推荐使用vn.py团队为量化交易专门打造的Python发行版[VNStudio-2.0.6](https://download.vnpy.com/vnstudio-2.0.6.exe),内置了最新版的vn.py框架以及VN Station量化管理平台,无需手动安装
+* 推荐使用vn.py团队为量化交易专门打造的Python发行版[VNStudio-2.0.6](https://download.vnpy.com/vnstudio-2.0.6.exe),内置了最新版的vn.py框架以及VN Station量化管理平台,无需手动安装
* 支持的系统版本:Windows 7以上/Windows Server 2008以上/Ubuntu 18.04 LTS
* 支持的Python版本:Python 3.7 64位(**注意必须是Python 3.7 64位版本**)
## 安装步骤
-在[这里](https://github.com/vnpy/vnpy/releases)下载最新版本,解压后运行以下命令安装:
+在[这里](https://github.com/vnpy/vnpy/releases)下载最新版本,解压后运行以下命令安装:
**Windows**
@@ -141,9 +141,9 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
## 使用指南
-1. 在[SimNow](http://www.simnow.com.cn/)注册CTP仿真账号,并在[该页面](http://www.simnow.com.cn/product.action)获取经纪商代码以及交易行情服务器地址。
+1. 在[SimNow](http://www.simnow.com.cn/)注册CTP仿真账号,并在[该页面](http://www.simnow.com.cn/product.action)获取经纪商代码以及交易行情服务器地址。
-2. 在[vn.py社区论坛](https://www.vnpy.com/forum/)注册获得VN Station账号密码(论坛账号密码即是)
+2. 在[vn.py社区论坛](https://www.vnpy.com/forum/)注册获得VN Station账号密码(论坛账号密码即是)
3. 启动VN Station(安装VNConda后会在桌面自动创建快捷方式),输入上一步的账号密码登录
@@ -195,25 +195,25 @@ if __name__ == "__main__":
vn.py使用Github托管其源代码,如果希望贡献代码请使用github的PR(Pull Request)的流程:
-1. [创建 Issue](https://github.com/vnpy/vnpy/issues/new) - 对于较大的改动(如新功能,大型重构等)最好先开issue讨论一下,较小的improvement(如文档改进,bugfix等)直接发PR即可
+1. [创建 Issue](https://github.com/vnpy/vnpy/issues/new) - 对于较大的改动(如新功能,大型重构等)最好先开issue讨论一下,较小的improvement(如文档改进,bugfix等)直接发PR即可
-2. Fork [vn.py](https://github.com/vnpy/vnpy) - 点击右上角**Fork**按钮
+2. Fork [vn.py](https://github.com/vnpy/vnpy) - 点击右上角**Fork**按钮
3. Clone你自己的fork: ```git clone https://github.com/$userid/vnpy.git```
- * 如果你的fork已经过时,需要手动sync:[https://help.github.com/articles/syncing-a-fork/](https://help.github.com/articles/syncing-a-fork/)
+ * 如果你的fork已经过时,需要手动sync:[同步方法](https://help.github.com/articles/syncing-a-fork/)
4. 从**dev**创建你自己的feature branch: ```git checkout -b $my_feature_branch dev```
5. 在$my_feature_branch上修改并将修改push到你的fork上
-6. 创建从你的fork的$my_feature_branch分支到主项目的**dev**分支的[Pull Request] - [在此](https://github.com/vnpy/vnpy/compare?expand=1)点击**compare across forks**,选择需要的fork和branch创建PR
+6. 创建从你的fork的$my_feature_branch分支到主项目的**dev**分支的[Pull Request] - [在此](https://github.com/vnpy/vnpy/compare?expand=1)点击**compare across forks**,选择需要的fork和branch创建PR
7. 等待review, 需要继续改进,或者被Merge!
在提交代码的时候,请遵守以下规则,以提高代码质量:
- * 使用[autopep8](https://github.com/hhatto/autopep8)格式化你的代码。运行```autopep8 --in-place --recursive . ```即可。
- * 使用[flake8](https://pypi.org/project/flake8/)检查你的代码,确保没有error和warning。在项目根目录下运行```flake8```即可。
+ * 使用[autopep8](https://github.com/hhatto/autopep8)格式化你的代码。运行```autopep8 --in-place --recursive . ```即可。
+ * 使用[flake8](https://pypi.org/project/flake8/)检查你的代码,确保没有error和warning。在项目根目录下运行```flake8```即可。
@@ -231,10 +231,10 @@ vn.py使用Github托管其源代码,如果希望贡献代码请使用github的
## 其他内容
-* [获取帮助](https://github.com/vnpy/vnpy/blob/dev/docs/SUPPORT.md)
-* [社区行为准侧](https://github.com/vnpy/vnpy/blob/dev/docs/CODE_OF_CONDUCT.md)
-* [Issue模板](https://github.com/vnpy/vnpy/blob/dev/docs/ISSUE_TEMPLATE.md)
-* [PR模板](https://github.com/vnpy/vnpy/blob/dev/docs/PULL_REQUEST_TEMPLATE.md)
+* [获取帮助](https://github.com/vnpy/vnpy/blob/dev/docs/SUPPORT.md)
+* [社区行为准侧](https://github.com/vnpy/vnpy/blob/dev/docs/CODE_OF_CONDUCT.md)
+* [Issue模板](https://github.com/vnpy/vnpy/blob/dev/docs/ISSUE_TEMPLATE.md)
+* [PR模板](https://github.com/vnpy/vnpy/blob/dev/docs/PULL_REQUEST_TEMPLATE.md)
From c02b37bde22d3644315fc34ce2d6f8ddd3a7db29 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Wed, 18 Sep 2019 11:44:11 +0800
Subject: [PATCH 28/41] [Mod] auto subscribe market data when add new legs
---
README.md | 2 +-
vnpy/app/spread_trading/engine.py | 26 ++++++++++++++++++++------
2 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index b6ed4ad0..6fbc5446 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index c3157b6a..87310395 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -211,6 +211,24 @@ class SpreadDataEngine:
event = Event(EVENT_SPREAD_POS, spread)
self.event_engine.put(event)
+ def get_leg(self, vt_symbol: str) -> LegData:
+ """"""
+ leg = self.legs.get(vt_symbol, None)
+
+ if not leg:
+ leg = LegData(vt_symbol)
+ self.legs[vt_symbol] = leg
+
+ # Subscribe market data
+ contract = self.main_engine.get_contract(vt_symbol)
+ req = SubscribeRequest(
+ contract.symbol,
+ contract.exchange
+ )
+ self.main_engine.subscribe(req, contract.gateway_name)
+
+ return leg
+
def add_spread(
self,
name: str,
@@ -229,11 +247,7 @@ class SpreadDataEngine:
for leg_setting in leg_settings:
vt_symbol = leg_setting["vt_symbol"]
-
- leg = self.legs.get(vt_symbol, None)
- if not leg:
- leg = LegData(vt_symbol)
- self.legs[vt_symbol] = leg
+ leg = self.get_leg(vt_symbol)
legs.append(leg)
price_multipliers[vt_symbol] = leg_setting["price_multiplier"]
@@ -824,7 +838,7 @@ class SpreadStrategyEngine:
return
self.call_strategy_func(strategy, strategy.on_stop)
-
+
strategy.stop_all_algos()
strategy.cancel_all_orders()
From 2448ef072dcd5e69a77e963c6c8bc8bc931e718d Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Wed, 18 Sep 2019 12:02:04 +0800
Subject: [PATCH 29/41] [Mod] use decimal.Decimal to ensure round_to precision,
close #2002
---
vnpy/trader/utility.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/vnpy/trader/utility.py b/vnpy/trader/utility.py
index 2c7f4b50..38a6e7db 100644
--- a/vnpy/trader/utility.py
+++ b/vnpy/trader/utility.py
@@ -5,6 +5,7 @@ General utility functions.
import json
from pathlib import Path
from typing import Callable
+from decimal import Decimal
import numpy as np
import talib
@@ -109,11 +110,13 @@ def save_json(filename: str, data: dict):
)
-def round_to(value: float, target: float):
+def round_to(value: float, target: float) -> float:
"""
Round price to price tick value.
"""
- rounded = int(round(value / target)) * target
+ value = Decimal(str(value))
+ target = Decimal(str(target))
+ rounded = float(int(round(value / target)) * target)
return rounded
From 99ea570b6c285251629dcd0a4dacb77483036c80 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Wed, 18 Sep 2019 12:57:17 +0800
Subject: [PATCH 30/41] [Add] resubscribe logic to solve websocket data push
stop problem, close #2045
---
vnpy/gateway/bitfinex/bitfinex_gateway.py | 52 +++++++++++++++++------
1 file changed, 39 insertions(+), 13 deletions(-)
diff --git a/vnpy/gateway/bitfinex/bitfinex_gateway.py b/vnpy/gateway/bitfinex/bitfinex_gateway.py
index ac2d614e..de1aff81 100644
--- a/vnpy/gateway/bitfinex/bitfinex_gateway.py
+++ b/vnpy/gateway/bitfinex/bitfinex_gateway.py
@@ -11,6 +11,8 @@ from datetime import datetime, timedelta
from urllib.parse import urlencode
from vnpy.api.rest import Request, RestClient
from vnpy.api.websocket import WebsocketClient
+from vnpy.event import Event
+from vnpy.trader.event import EVENT_TIMER
from vnpy.trader.constant import (
Direction,
@@ -92,6 +94,9 @@ class BitfinexGateway(BaseGateway):
"""Constructor"""
super(BitfinexGateway, self).__init__(event_engine, "BITFINEX")
+ self.timer_count = 0
+ self.resubscribe_interval = 60
+
self.rest_api = BitfinexRestApi(self)
self.ws_api = BitfinexWebsocketApi(self)
@@ -104,9 +109,10 @@ class BitfinexGateway(BaseGateway):
proxy_port = setting["proxy_port"]
self.rest_api.connect(key, secret, session, proxy_host, proxy_port)
-
self.ws_api.connect(key, secret, proxy_host, proxy_port)
+ self.event_engine.register(EVENT_TIMER, self.process_timer_event)
+
def subscribe(self, req: SubscribeRequest):
""""""
self.ws_api.subscribe(req)
@@ -136,6 +142,16 @@ class BitfinexGateway(BaseGateway):
self.rest_api.stop()
self.ws_api.stop()
+ def process_timer_event(self, event: Event):
+ """"""
+ self.timer_count += 1
+
+ if self.timer_count < self.resubscribe_interval:
+ return
+
+ self.timer_count = 0
+ self.ws_api.resubscribe()
+
class BitfinexRestApi(RestClient):
"""
@@ -359,11 +375,12 @@ class BitfinexWebsocketApi(WebsocketClient):
self.accounts = {}
self.orders = {}
self.trades = set()
- self.tickDict = {}
- self.bidDict = {}
- self.askDict = {}
- self.orderLocalDict = {}
- self.channelDict = {} # channel_id : (Channel, Symbol)
+ self.ticks = {}
+ self.bids = {}
+ self.asks = {}
+ self.channels = {} # channel_id : (Channel, Symbol)
+
+ self.subscribed = {}
def connect(
self, key: str, secret: str, proxy_host: str, proxy_port: int
@@ -378,12 +395,16 @@ class BitfinexWebsocketApi(WebsocketClient):
"""
Subscribe to tick data upate.
"""
+ if req.symbol not in self.subscribed:
+ self.subscribed[req.symbol] = req
+
d = {
"event": "subscribe",
"channel": "book",
"symbol": req.symbol,
}
self.send_packet(d)
+
d = {
"event": "subscribe",
"channel": "ticker",
@@ -393,6 +414,11 @@ class BitfinexWebsocketApi(WebsocketClient):
return int(round(time.time() * 1000))
+ def resubscribe(self):
+ """"""
+ for req in self.subscribed.values():
+ self.subscribe(req)
+
def _gen_unqiue_cid(self):
self.order_id += 1
local_oid = time.strftime("%y%m%d") + str(self.order_id)
@@ -463,7 +489,7 @@ class BitfinexWebsocketApi(WebsocketClient):
if data["event"] == "subscribed":
symbol = str(data["symbol"].replace("t", ""))
- self.channelDict[data["chanId"]] = (data["channel"], symbol)
+ self.channels[data["chanId"]] = (data["channel"], symbol)
def on_update(self, data):
""""""
@@ -480,12 +506,12 @@ class BitfinexWebsocketApi(WebsocketClient):
def on_data_update(self, data):
""""""
channel_id = data[0]
- channel, symbol = self.channelDict[channel_id]
+ channel, symbol = self.channels[channel_id]
symbol = str(symbol.replace("t", ""))
# Get the Tick object
- if symbol in self.tickDict:
- tick = self.tickDict[symbol]
+ if symbol in self.ticks:
+ tick = self.ticks[symbol]
else:
tick = TickData(
symbol=symbol,
@@ -495,7 +521,7 @@ class BitfinexWebsocketApi(WebsocketClient):
gateway_name=self.gateway_name,
)
- self.tickDict[symbol] = tick
+ self.ticks[symbol] = tick
l_data1 = data[1]
@@ -509,8 +535,8 @@ class BitfinexWebsocketApi(WebsocketClient):
# Update deep quote
elif channel == "book":
- bid = self.bidDict.setdefault(symbol, {})
- ask = self.askDict.setdefault(symbol, {})
+ bid = self.bids.setdefault(symbol, {})
+ ask = self.asks.setdefault(symbol, {})
if len(l_data1) > 3:
for price, count, amount in l_data1:
From 8ec0f439ee2e1ac931341df235e1df592c9ad35c Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Wed, 18 Sep 2019 13:03:13 +0800
Subject: [PATCH 31/41] [Add] CFE exchange, close #2096
---
vnpy/gateway/ib/ib_gateway.py | 1 +
vnpy/trader/constant.py | 1 +
2 files changed, 2 insertions(+)
diff --git a/vnpy/gateway/ib/ib_gateway.py b/vnpy/gateway/ib/ib_gateway.py
index 1f0387d7..2e4b0d87 100644
--- a/vnpy/gateway/ib/ib_gateway.py
+++ b/vnpy/gateway/ib/ib_gateway.py
@@ -64,6 +64,7 @@ EXCHANGE_VT2IB = {
Exchange.ICE: "ICE",
Exchange.SEHK: "SEHK",
Exchange.HKFE: "HKFE",
+ Exchange.CFE: "CFE"
}
EXCHANGE_IB2VT = {v: k for k, v in EXCHANGE_VT2IB.items()}
diff --git a/vnpy/trader/constant.py b/vnpy/trader/constant.py
index f7159a53..8cbfbd21 100644
--- a/vnpy/trader/constant.py
+++ b/vnpy/trader/constant.py
@@ -101,6 +101,7 @@ class Exchange(Enum):
SGX = "SGX" # Singapore Global Exchange
CBOT = "CBT" # Chicago Board of Trade
CBOE = "CBOE" # Chicago Board Options Exchange
+ CFE = "CFE" # CBOE Futures Exchange
DME = "DME" # Dubai Mercantile Exchange
EUREX = "EUX" # Eurex Exchange
APEX = "APEX" # Asia Pacific Exchange
From 6967a825f2539184d5d3c880766a14e4337853b3 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Wed, 18 Sep 2019 22:51:25 +0800
Subject: [PATCH 32/41] [Fix] typo of vt_symbol
---
vnpy/app/spread_trading/ui/widget.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/vnpy/app/spread_trading/ui/widget.py b/vnpy/app/spread_trading/ui/widget.py
index 35415892..00ef63a5 100644
--- a/vnpy/app/spread_trading/ui/widget.py
+++ b/vnpy/app/spread_trading/ui/widget.py
@@ -439,12 +439,12 @@ class SpreadDataDialog(QtWidgets.QDialog):
leg_settings = {}
for d in self.leg_widgets:
try:
- spread_name = d["symbol"].text()
+ vt_symbol = d["symbol"].text()
price_multiplier = int(d["price"].text())
trading_multiplier = int(d["trading"].text())
- leg_settings[spread_name] = {
- "spread_name": spread_name,
+ leg_settings[vt_symbol] = {
+ "vt_symbol": vt_symbol,
"price_multiplier": price_multiplier,
"trading_multiplier": trading_multiplier
}
From 6f3a2b49c88c0c80a4cf3283a07fac8731732277 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Thu, 19 Sep 2019 12:30:38 +0800
Subject: [PATCH 33/41] [Del] remove unused ci config files
---
.gitlab-ci.yml | 142 ----------------------
.travis.yml | 101 ---------------
vnpy/gateway/bitfinex/bitfinex_gateway.py | 2 +-
3 files changed, 1 insertion(+), 244 deletions(-)
delete mode 100644 .gitlab-ci.yml
delete mode 100644 .travis.yml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index d12c40ca..00000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,142 +0,0 @@
-# This file is a template, and might need editing before it works on your project.
-# Official language image. Look for the different tagged releases at:
-# https://hub.docker.com/r/library/python/tags/
-image: registry.cn-shanghai.aliyuncs.com/vnpy-ci/gcc-7-python-3.7:latest
-
-.services:
- services: &services
- - postgres:latest
- - mysql:latest
- - mongo:latest
-
-# Change pip's cache directory to be inside the project directory since we can
-# only cache local items.
-variables: &variables
- GIT_DEPTH: "1"
- PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
- POSTGRES_DB: &db_name "vnpy"
- POSTGRES_USER: "postgres"
- POSTGRES_PASSWORD: &db_password "1234"
- VNPY_TEST_POSTGRESQL_PASSWORD: *db_password
- MYSQL_DATABASE: *db_name
- MYSQL_ROOT_PASSWORD: *db_password
- VNPY_TEST_MYSQL_PASSWORD: *db_password
- VNPY_BUILD_PARALLEL: "auto"
-
-# Pip's cache doesn't store the python packages
-# https://pip.pypa.io/en/stable/reference/pip_install/#caching
-#
-# If you want to also cache the installed packages, you have to install
-# them in a virtualenv and cache it as well.
-.default_cache:
- cache:
- <<: &cache
- key: "pip_and_venv"
- untracked: false
- policy: pull
- paths:
- - .cache/pip
- - venv/
-
-
-
-before_script:
- - echo $PWD
- - python -V
- - gcc --version
- - free
- - date
-
- # venv
- - pip install virtualenv
- - virtualenv venv
- - source venv/bin/activate
-
- # some envs
- - source ci/env.sh
-
-.scripts:
- script:
- - &install_scripts |
- date
- python -m pip --version
- python -m pip install --upgrade pip wheel setuptools
- python -m pip install https://pip.vnpy.com/colletion/ibapi-9.75.1-001-py3-none-any.whl
- bash ci/gitlab_pre_install.sh
-
- date
- bash ./install.sh
- date
-
- - &test_scripts |
- date
- cd tests
- python test_all.py
- date
-##################################
-# stages
-
-stages: # I use anchors for IDE hints only
- - &single_module single_module
- - &build_all build_all
-
-
-###########################################
-## jobs:
-flake8:
- stage: *single_module
- image: python:3.7
- cache:
- key: 'flake8'
- paths:
- - .cache/pip
- - venv/
- script:
- - pip install flake8
- - flake8
-
-ctp:
- <<: &test_single_module
- stage: *single_module
- image: registry.cn-shanghai.aliyuncs.com/vnpy-ci/gcc-8-python-3.7:latest
- services: *services
- cache:
- <<: *cache
- script:
- - *install_scripts
- - *test_scripts
- variables:
- <<: *variables
- VNPY_BUILD_CTP: 1
-
-oes:
- <<: *test_single_module
- variables:
- <<: *variables
- VNPY_BUILD_OES: 1
-
-no_building:
- <<: *test_single_module
- cache:
- <<: *cache
- policy: pull-push
- variables:
- <<: *variables
- VNPY_BUILD_OES: 0
- VNPY_BUILD_CTP: 0
-
-build-all-gcc8:
- stage: *build_all
- variables:
- <<: *variables
- image: registry.cn-shanghai.aliyuncs.com/vnpy-ci/gcc-8-python-3.7:latest
- services: *services
- cache:
- key: "build-all"
- paths: []
- script:
- - unset VNPY_BUILD_CTP
- - unset VNPY_BUILD_OES
- - *install_scripts
- - *test_scripts
-
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 10ca263c..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,101 +0,0 @@
-language: python
-
-dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069)
-
-cache: pip
-
-git:
- depth: 1
-
-env:
- - >
- VNPY_BUILD_PARALLEL=1
- VNPY_BUILD_CTP=1
- VNPY_BUILD_OES=1
-
-python:
- - "3.7"
-
-services:
- - mongodb
- - mysql
- - postgresql
-
-before_script:
- - psql -d postgresql://postgres:${VNPY_TEST_POSTGRESQL_PASSWORD}@localhost -c "create database vnpy;"
- - mysql -u root --password=${VNPY_TEST_MYSQL_PASSWORD} -e 'CREATE DATABASE vnpy;'
- - source ci/env.sh;
-
-script:
- - cd tests;
- - python test_all.py
-
-matrix:
- include:
- - name: "code quality analysis: flake8"
- before_install:
- - python -m pip install flake8
- install:
- - "" # prevent running "pip install -r requirements.txt"
- script:
- - flake8
-
- - name: "pip install under Ubuntu: gcc-8"
- addons:
- apt:
- sources:
- - ubuntu-toolchain-r-test
- packages:
- - g++-8
- before_install:
- - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-8 90
- - sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-8 90
- - sudo update-alternatives --install /usr/bin/gcc cc /usr/bin/gcc-8 90
- install:
- # update pip & setuptools
- - python -m pip install --upgrade pip wheel setuptools
- # Linux install script
- - python -m pip install https://pip.vnpy.com/colletion/ibapi-9.75.1-001-py3-none-any.whl
- - bash ./install.sh
-
- - name: "sdist install under Ubuntu: gcc-7"
- addons:
- apt:
- sources:
- - ubuntu-toolchain-r-test
- packages:
- - g++-7
- before_install:
- - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-7 90
- - sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-7 90
- - sudo update-alternatives --install /usr/bin/gcc cc /usr/bin/gcc-7 90
- install:
- # Linux install script
- - python -m pip install --upgrade pip wheel setuptools
- - pushd /tmp
- - wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
- - tar -xf ta-lib-0.4.0-src.tar.gz
- - cd ta-lib
- - ./configure --prefix=/usr
- - make # -j under gcc-7 failed!!?
- - sudo make install
- - popd
- - python -m pip install numpy
- - python -m pip install --pre --extra-index-url https://rquser:ricequant99@py.ricequant.com/simple/ rqdatac
- - python -m pip install https://pip.vnpy.com/colletion/ibapi-9.75.1-001-py3-none-any.whl
- - python setup.py sdist
- - python -m pip install dist/`ls dist`
-
- - name: "pip install under osx"
- os: osx
- language: shell # osx supports only shell
- services: []
- before_install: []
- install:
- - python3 -m pip install https://pip.vnpy.com/colletion/ibapi-9.75.1-001-py3-none-any.whl
- - bash ./install_osx.sh
- before_script: []
- script:
- - source ci/env.sh;
- - cd tests;
- - VNPY_TEST_ONLY_SQLITE=1 python3 test_all.py
diff --git a/vnpy/gateway/bitfinex/bitfinex_gateway.py b/vnpy/gateway/bitfinex/bitfinex_gateway.py
index de1aff81..a37dd995 100644
--- a/vnpy/gateway/bitfinex/bitfinex_gateway.py
+++ b/vnpy/gateway/bitfinex/bitfinex_gateway.py
@@ -145,7 +145,7 @@ class BitfinexGateway(BaseGateway):
def process_timer_event(self, event: Event):
""""""
self.timer_count += 1
-
+
if self.timer_count < self.resubscribe_interval:
return
From 5e8a64887472e0beaa045e7e5770dd68aa822fab Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Thu, 19 Sep 2019 15:18:51 +0800
Subject: [PATCH 34/41] [Fix] bug when __thread not created
---
vnpy/rpc/__init__.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/vnpy/rpc/__init__.py b/vnpy/rpc/__init__.py
index d1b4877a..b74e1f4e 100644
--- a/vnpy/rpc/__init__.py
+++ b/vnpy/rpc/__init__.py
@@ -1,3 +1,4 @@
+from zmq.backend.cython.constants import NOBLOCK
import signal
import threading
import traceback
@@ -7,10 +8,11 @@ from typing import Any, Callable
import zmq
-_ = lambda x: x
+
+def _(x): return x
# Achieve Ctrl-c interrupt recv
-from zmq.backend.cython.constants import NOBLOCK
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
@@ -100,7 +102,7 @@ class RpcServer:
def join(self):
# Wait for RpcServer thread to exit
- if self.__thread.isAlive():
+ if self.__thread and self.__thread.is_alive():
self.__thread.join()
self.__thread = None
From 2322406a931926e186afc7a0b23dd199c66681a3 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Thu, 19 Sep 2019 16:07:32 +0800
Subject: [PATCH 35/41] [Fix] only subscribe if contract data found
---
vnpy/app/spread_trading/engine.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/vnpy/app/spread_trading/engine.py b/vnpy/app/spread_trading/engine.py
index 87310395..837d299e 100644
--- a/vnpy/app/spread_trading/engine.py
+++ b/vnpy/app/spread_trading/engine.py
@@ -221,11 +221,12 @@ class SpreadDataEngine:
# Subscribe market data
contract = self.main_engine.get_contract(vt_symbol)
- req = SubscribeRequest(
- contract.symbol,
- contract.exchange
- )
- self.main_engine.subscribe(req, contract.gateway_name)
+ if contract:
+ req = SubscribeRequest(
+ contract.symbol,
+ contract.exchange
+ )
+ self.main_engine.subscribe(req, contract.gateway_name)
return leg
From 7de6e0fb428f1662d7f31835cffe15c228492bf7 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Thu, 19 Sep 2019 21:55:33 +0800
Subject: [PATCH 36/41] [Mod] add wmi into requirements
---
requirements.txt | 3 ++-
vnpy/rpc/__init__.py | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 66137198..4f68c40d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,4 +18,5 @@ rqdatac
ta-lib
ibapi
deap
-pyzmq
\ No newline at end of file
+pyzmq
+wmi
\ No newline at end of file
diff --git a/vnpy/rpc/__init__.py b/vnpy/rpc/__init__.py
index b74e1f4e..f29bc00b 100644
--- a/vnpy/rpc/__init__.py
+++ b/vnpy/rpc/__init__.py
@@ -239,7 +239,7 @@ class RpcClient:
def join(self):
# Wait for RpcClient thread to exit
- if self.__thread.isAlive():
+ if self.__thread and self.__thread.is_alive():
self.__thread.join()
self.__thread = None
From bb0362762712f0d51800c91ef6df91d3a6372d72 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Thu, 19 Sep 2019 22:23:41 +0800
Subject: [PATCH 37/41] [Mod] update vnstudio link
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 6fbc5446..25959d96 100644
--- a/README.md
+++ b/README.md
@@ -122,7 +122,7 @@ vn.py是一套基于Python的开源量化交易系统开发框架,于2015年1
## 环境准备
-* 推荐使用vn.py团队为量化交易专门打造的Python发行版[VNStudio-2.0.6](https://download.vnpy.com/vnstudio-2.0.6.exe),内置了最新版的vn.py框架以及VN Station量化管理平台,无需手动安装
+* 推荐使用vn.py团队为量化交易专门打造的Python发行版[VNStudio-2.0.7](https://download.vnpy.com/vnstudio-2.0.7.exe),内置了最新版的vn.py框架以及VN Station量化管理平台,无需手动安装
* 支持的系统版本:Windows 7以上/Windows Server 2008以上/Ubuntu 18.04 LTS
* 支持的Python版本:Python 3.7 64位(**注意必须是Python 3.7 64位版本**)
From 846ea95093c54b05f0be2fde374ce61e91ee1bc9 Mon Sep 17 00:00:00 2001
From: "vn.py"
Date: Fri, 20 Sep 2019 11:37:32 +0800
Subject: [PATCH 38/41] [Fix] close #1997
---
examples/vn_trader/run.py | 4 +-
vnpy/gateway/ctp/ctp_gateway.py | 70 ++++++++++++-----------
vnpy/gateway/ctptest/ctptest_gateway.py | 70 ++++++++++++-----------
vnpy/gateway/mini/mini_gateway.py | 2 +-
vnpy/gateway/minitest/minitest_gateway.py | 2 +-
5 files changed, 76 insertions(+), 72 deletions(-)
diff --git a/examples/vn_trader/run.py b/examples/vn_trader/run.py
index 1c31e879..f7c23125 100644
--- a/examples/vn_trader/run.py
+++ b/examples/vn_trader/run.py
@@ -8,7 +8,7 @@ from vnpy.trader.ui import MainWindow, create_qapp
from vnpy.gateway.bitmex import BitmexGateway
# from vnpy.gateway.futu import FutuGateway
# from vnpy.gateway.ib import IbGateway
-# from vnpy.gateway.ctp import CtpGateway
+from vnpy.gateway.ctp import CtpGateway
# from vnpy.gateway.ctptest import CtptestGateway
# from vnpy.gateway.mini import MiniGateway
# from vnpy.gateway.sopt import SoptGateway
@@ -50,7 +50,7 @@ def main():
main_engine = MainEngine(event_engine)
# main_engine.add_gateway(BinanceGateway)
- # main_engine.add_gateway(CtpGateway)
+ main_engine.add_gateway(CtpGateway)
# main_engine.add_gateway(CtptestGateway)
# main_engine.add_gateway(MiniGateway)
# main_engine.add_gateway(SoptGateway)
diff --git a/vnpy/gateway/ctp/ctp_gateway.py b/vnpy/gateway/ctp/ctp_gateway.py
index a72f4598..ab7919bf 100644
--- a/vnpy/gateway/ctp/ctp_gateway.py
+++ b/vnpy/gateway/ctp/ctp_gateway.py
@@ -479,46 +479,48 @@ class CtpTdApi(TdApi):
if not data:
return
- # Get buffered position object
- key = f"{data['InstrumentID'], data['PosiDirection']}"
- position = self.positions.get(key, None)
- if not position:
- position = PositionData(
- symbol=data["InstrumentID"],
- exchange=symbol_exchange_map[data["InstrumentID"]],
- direction=DIRECTION_CTP2VT[data["PosiDirection"]],
- gateway_name=self.gateway_name
- )
- self.positions[key] = position
+ # Check if contract data received
+ if data["InstrumentID"] in symbol_exchange_map:
+ # Get buffered position object
+ key = f"{data['InstrumentID'], data['PosiDirection']}"
+ position = self.positions.get(key, None)
+ if not position:
+ position = PositionData(
+ symbol=data["InstrumentID"],
+ exchange=symbol_exchange_map[data["InstrumentID"]],
+ direction=DIRECTION_CTP2VT[data["PosiDirection"]],
+ gateway_name=self.gateway_name
+ )
+ self.positions[key] = position
- # For SHFE position data update
- if position.exchange == Exchange.SHFE:
- if data["YdPosition"] and not data["TodayPosition"]:
- position.yd_volume = data["Position"]
- # For other exchange position data update
- else:
- position.yd_volume = data["Position"] - data["TodayPosition"]
+ # For SHFE position data update
+ if position.exchange == Exchange.SHFE:
+ if data["YdPosition"] and not data["TodayPosition"]:
+ position.yd_volume = data["Position"]
+ # For other exchange position data update
+ else:
+ position.yd_volume = data["Position"] - data["TodayPosition"]
- # Get contract size (spread contract has no size value)
- size = symbol_size_map.get(position.symbol, 0)
+ # Get contract size (spread contract has no size value)
+ size = symbol_size_map.get(position.symbol, 0)
- # Calculate previous position cost
- cost = position.price * position.volume * size
+ # Calculate previous position cost
+ cost = position.price * position.volume * size
- # Update new position volume
- position.volume += data["Position"]
- position.pnl += data["PositionProfit"]
+ # Update new position volume
+ position.volume += data["Position"]
+ position.pnl += data["PositionProfit"]
- # Calculate average position price
- if position.volume and size:
- cost += data["PositionCost"]
- position.price = cost / (position.volume * size)
+ # Calculate average position price
+ if position.volume and size:
+ cost += data["PositionCost"]
+ position.price = cost / (position.volume * size)
- # Get frozen volume
- if position.direction == Direction.LONG:
- position.frozen += data["ShortFrozen"]
- else:
- position.frozen += data["LongFrozen"]
+ # Get frozen volume
+ if position.direction == Direction.LONG:
+ position.frozen += data["ShortFrozen"]
+ else:
+ position.frozen += data["LongFrozen"]
if last:
for position in self.positions.values():
diff --git a/vnpy/gateway/ctptest/ctptest_gateway.py b/vnpy/gateway/ctptest/ctptest_gateway.py
index a25f9dec..e22ef0a6 100644
--- a/vnpy/gateway/ctptest/ctptest_gateway.py
+++ b/vnpy/gateway/ctptest/ctptest_gateway.py
@@ -478,46 +478,48 @@ class CtpTdApi(TdApi):
if not data:
return
- # Get buffered position object
- key = f"{data['InstrumentID'], data['PosiDirection']}"
- position = self.positions.get(key, None)
- if not position:
- position = PositionData(
- symbol=data["InstrumentID"],
- exchange=symbol_exchange_map[data["InstrumentID"]],
- direction=DIRECTION_CTP2VT[data["PosiDirection"]],
- gateway_name=self.gateway_name
- )
- self.positions[key] = position
+ # Check if contract data received
+ if data["InstrumentID"] in symbol_exchange_map:
+ # Get buffered position object
+ key = f"{data['InstrumentID'], data['PosiDirection']}"
+ position = self.positions.get(key, None)
+ if not position:
+ position = PositionData(
+ symbol=data["InstrumentID"],
+ exchange=symbol_exchange_map[data["InstrumentID"]],
+ direction=DIRECTION_CTP2VT[data["PosiDirection"]],
+ gateway_name=self.gateway_name
+ )
+ self.positions[key] = position
- # For SHFE position data update
- if position.exchange == Exchange.SHFE:
- if data["YdPosition"] and not data["TodayPosition"]:
- position.yd_volume = data["Position"]
- # For other exchange position data update
- else:
- position.yd_volume = data["Position"] - data["TodayPosition"]
+ # For SHFE position data update
+ if position.exchange == Exchange.SHFE:
+ if data["YdPosition"] and not data["TodayPosition"]:
+ position.yd_volume = data["Position"]
+ # For other exchange position data update
+ else:
+ position.yd_volume = data["Position"] - data["TodayPosition"]
- # Get contract size (spread contract has no size value)
- size = symbol_size_map.get(position.symbol, 0)
+ # Get contract size (spread contract has no size value)
+ size = symbol_size_map.get(position.symbol, 0)
- # Calculate previous position cost
- cost = position.price * position.volume * size
+ # Calculate previous position cost
+ cost = position.price * position.volume * size
- # Update new position volume
- position.volume += data["Position"]
- position.pnl += data["PositionProfit"]
+ # Update new position volume
+ position.volume += data["Position"]
+ position.pnl += data["PositionProfit"]
- # Calculate average position price
- if position.volume and size:
- cost += data["PositionCost"]
- position.price = cost / (position.volume * size)
+ # Calculate average position price
+ if position.volume and size:
+ cost += data["PositionCost"]
+ position.price = cost / (position.volume * size)
- # Get frozen volume
- if position.direction == Direction.LONG:
- position.frozen += data["ShortFrozen"]
- else:
- position.frozen += data["LongFrozen"]
+ # Get frozen volume
+ if position.direction == Direction.LONG:
+ position.frozen += data["ShortFrozen"]
+ else:
+ position.frozen += data["LongFrozen"]
if last:
for position in self.positions.values():
diff --git a/vnpy/gateway/mini/mini_gateway.py b/vnpy/gateway/mini/mini_gateway.py
index 1811396f..ff41b911 100644
--- a/vnpy/gateway/mini/mini_gateway.py
+++ b/vnpy/gateway/mini/mini_gateway.py
@@ -491,7 +491,7 @@ class MiniTdApi(TdApi):
def onRspQryInvestorPosition(self, data: dict, error: dict, reqid: int, last: bool):
""""""
- if data:
+ if data and data["InstrumentID"] in symbol_exchange_map:
# Get buffered position object
key = f"{data['InstrumentID'], data['PosiDirection']}"
position = self.positions.get(key, None)
diff --git a/vnpy/gateway/minitest/minitest_gateway.py b/vnpy/gateway/minitest/minitest_gateway.py
index cd315540..0a96ef87 100644
--- a/vnpy/gateway/minitest/minitest_gateway.py
+++ b/vnpy/gateway/minitest/minitest_gateway.py
@@ -491,7 +491,7 @@ class MiniTdApi(TdApi):
def onRspQryInvestorPosition(self, data: dict, error: dict, reqid: int, last: bool):
""""""
- if data:
+ if data and data["InstrumentID"] in symbol_exchange_map:
# Get buffered position object
key = f"{data['InstrumentID'], data['PosiDirection']}"
position = self.positions.get(key, None)
From 391d94411ad000fa8fbac362c65920854d61431d Mon Sep 17 00:00:00 2001
From: nanoric
Date: Fri, 20 Sep 2019 14:30:57 +0800
Subject: [PATCH 39/41] [Mod] TapGateway: roll back custom callback_wrapper to
fix invalid pointer BUG.
---
vnpy/api/tap/vntap.pyd | Bin 1982464 -> 1984000 bytes
vnpy/api/tap/vntap/custom/custom_wrappers.hpp | 301 +++++++++---------
vnpy/api/tap/vntap/vntap.vcxproj | 16 +-
vnpy/api/tap/vntap/vntap.vcxproj.filters | 3 -
4 files changed, 159 insertions(+), 161 deletions(-)
diff --git a/vnpy/api/tap/vntap.pyd b/vnpy/api/tap/vntap.pyd
index 699dff7360e768d53347007a355fd0ba028aa61f..ce2c9f272b357b7895fe7be2719682ef30801241 100644
GIT binary patch
delta 546670
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