From 5a9dfa747d4b6e1441d84deddc58386fd6111186 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Sat, 14 Sep 2019 11:06:08 +0800 Subject: [PATCH 01/17] [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 zcmeI4S!`U_eTNUFgmG;lZ4xDMgFx~_9vsU+mc}AypW$#D4tG*gVn+{c9TZ6GIGF|o z3Ima9!$A5_q(E95c}icJNa%x~g0?nVsF1jbt4MJdhoY>7S}cl*NrRyzo_^e;#gtghhA*_JZ@fP;Beg>oF5Jcm~I3!r_B`k>!e^h&r^D;;9{Alo zpw9JCTnSVRMnuIxBwTSmx?C|9m5lOWKRs~9*bA=W{8iZF?t|h=ptM(0F%lIJ#=YrZ z^?+iS9_TiD;4bb@!U1;=sB?W3egdX{p8s6`ss~2d3;LYBU>fH~51tN{SDcQBZ-E;1 zQP>G=`uyAbfEtJBgD&Imf)#q;ZM^TRID>zS!cL&HXJ0uI6P4`$m1D7Gkef0RlNG)6 z!s&>j9~usJg?3e*;r9d7sE@)+pmG%dqcPL}IQ}1m>W@KsfW3fce-ZC*;C-)hZ&P>) zl=h5$qgBF(f8Ti3GRQ5#UQo$ifb(^D5B&Z!)xvis=1|(H7S8HT|5f?!%O8=!34 zKIk&~pr5e;cG)?ov6K#L-A^#S35y^-M{isP$7^J8;t|2U0z>oxdx5p)HyHQ&yV+|j zN~H%_H*2m+z>5EDTm}Xd?6Y11SBCpZ{O<(2f9!jV(g*AXtbHBc!@WPyT_pnafK9RY zL7XoeU(-ufnUaC(vCjg55PxUDPcQ8mkq-;hI!Qx_9+@ zQ0)b0>4Wb0T{wyV1IoRdVr+!)WSt04)TJ0@sS_Hnlc7O=V|w_#>8_rJqfoB)0Uy5$ zA;w}m4F8kbzq|KCBm5@x(g)quEATe{_bK-_#nc0lsd@n^keW;eVYL?w@V8KRjiMhK zLf!FQq3&Axz@!-ZK>N=)|5*P|*30lY`k>e7gGIc*0ei#vw<+`RUZV-?Hso5ZdDPB{keAIZJKIo$ly6f(Sd+PoJ_s>Uq_!|le@rdCcQXn;3%wUo} zU@wUD)=B)&z)Rtt`cD?7|0MpC_)p?rlUD|#^ubyBpr`&094gV1~@58s}rf4F&l>6vSs51*AY~ww1vtatr;v{Ba;J*p~nmjUC;=MnGFXCT4L!R^R^Yw_$uqRuMe@KDUY&nBLvTDZtZoqxB=lth* z|1$N<5Q8#x%|5D`JK(<=|C)zn@DbO4f%|7b?VAR6mR=Z@TNlc8J)8vzQ}7HpQ?0qs zEFc9^^S}(I$>%8k%a!};nGUsYIyrjbM0s`bInMt9Zu@*^rQbh<|NalDgX%r@QFEaM z|1J1$!M|pM4Ayw<1RTJ<+AAIG3?KKXeCAL|{okv4lVwpY&gEQq?{6h+) z=3^OL#``Pq6!4rhslC$4&I>0w-k+j=1upx1y*%^7IG?oKEC18@9}m+9&eUr!wBo-N z|E>7fSQ)G{{wGw#$;WhW?2}GzkNYIYyL$eggA=gk^Y!8WyxY5dj{k`)|8=vi0#YDL z7W&><`-Rq9c>f7}7S!Ho@VI@uzu)ig6*iTX-Q_rc3HaMiJ@Z4j&U)r;_iEh0e;fYW z@ZUxTB~LdQ)HJ={RA#z2_C|N~kNw>CZjJl!{uZqG{eAcyb-Ui^e3`wU=YP^!NBwM@ zfFFS@VSb1ApTU>d2h@IO;DqFXlR)6Cu&Jo%D#L#Q`T);EG8mkMnTLV@$uRGAR#ZRR zj{o*r_z>uUl4F!LcP3`rK7oI=7aBMLdEmtMZjE~xUk?O&7XyJle2>Dk423Q{6#8Qs z_xPW3R>j(t|4+ediP`q+a33VBdk8h3N6jkt{3YxDinRx-ebB%O=P{oIgMGp#93D{5 zzYpGp^X^+dYkyYM}D*(3Y~^Y~}D2l5e- z=02>i{Rw;hYV^_(3iSzv<$n+34}!t7R~U_FJ`|df;qa`1>3Ei2*c|m<-e>ur&RQxt z2%ZIDjOh+WZ_$wMsAW!TjB-1uo>8A;R6X<#e9c^|wPp{w4~6>io&n#_!3nr)KJycf zciVp+<8nAW9fmCbUM?CgbO=Z^q&xB7iN|fTz-NCq|Nn#g zU_-i<-#Plp!u>su`&|9L+u&dyyag+v&>-F?aJ?Phx&1KvQ8+yIJ$mXJ-22Zw!nc^m z^YAD3`qgM3w*O@M&{}ftkj#@ieD2L;y7Tk+$GeGrZ!e6Ii0NI8q0m4XYftbD^f4OG zd?Yd>qtQ8wwCmh{EuV3AJsckX9rwQtiyq=8>*fFDd!U(qR~a~JlCzxx5_r$oe?Ol| zCg%_D!oN@`|6V#GkwKARU@z-_9q=;5^FD#=o%+u04YePP!my{HC+T&y&kbCLZ-S#K zIeQ5IhZ2o*hu1)Effe5KuZ?qu_TgXHI5+gbNi;erY+^B^2YmFwyX*n$k;n+%XJjmP z(PEBKVb?nu)qT>QM6Fq^oecg)JrnlPID1$$Zr%SR?q6)2J5r8+(PH_x@8yLt8XdyD z&EOu^`UAKUi45U=+;X!M$A$daSu!}Eibga4{e_IG>U6_&i*)M*CdV zB}7uMtPF?^?8-b>o?gL2r&zv_XH;WUXyPU7(q z_Iktr9{S)_yk9Y&`6*dba|te6yw9lFB!d*Md&Fpd(@^f=IXH>`yXu**sYx5oYiln{ z&6YAqkyC!bd*t0b|1ZEfV1Jv!_a(TZ$u9%^U-5|1{H9S|J@Sv$)#fv=p7pvq?S32m zU$FjbY9{}(ws!uby1J{lS6s11yVheSn@zJw@t*u1JdeD)u5RH;=Ki#zMZk+`-5QnTd@Qe>6i zgXf`l zLigCGR3F#WEYJrFBAHw$XI)1d8m>k2n91!=NA+?x*AFC<*H6M4NX^GG zNRi3*1NZ1BRZrWnzNg@oWb(#kpa(Rr4Df%$qZW5IE!EdHjg8ktW8;kq_%B!osWCE0 zk;Be{_V}k&e=F2F;j{3Q#>ShsfF3A$@PE_e26sL^%e~P9$>dU5Q`5~F=muHhq*(vX z11^v|s9sm7HN%(SXH8AFJ_P!p5zjXe-&ds#YYMyn0Yl}rShFV(}|F^Ae zQRWMuU2AQ4Dn<@1KV^;ZB}Cp#2wcC-K}X_ZGKzan9dzclz7g???sb z#%R~MBkx~pYx^Jp1)PGOdZq^!1&;T@Tdcb^?kdh0g*e?icjTS&;Ei{A#$!~ ze*xF`yWc>a^##;fH>9>_%&B=;pLgnr`GQmL&#A2$JJ+o*aB6GDnfjPVl zeWq5oTNjynKlMFV9id)w)s);`eA$}6ar@&|->eF=0qwO_n*GD=&EsidG%#;->SN~Z z?b@2HbKRP)Q>!qX+cD|=)Xdho9-($_&vi;UZ(kNqXWw2tZDyOfZuE{*8h4lS4_zO3 zzHq;qzH9zwjScHC>&$x1I+KrCXBJ@9r?Nkv`2y>@b3?0k0=8=B3#?im&zg^v_7@zJ z=E02Zw;C_-1z9yW%&M7jc6} Date: Sat, 14 Sep 2019 13:20:09 +0800 Subject: [PATCH 02/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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/17] [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}")