From c0d15ad9cbacef31d701ac934726482b70b70ce2 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Wed, 13 Nov 2019 14:34:26 +0800 Subject: [PATCH 1/5] [Add] new portfolio manager app --- vnpy/app/portfolio_manager/__init__.py | 16 + vnpy/app/portfolio_manager/engine.py | 289 +++++++ vnpy/app/portfolio_manager/ui/__init__.py | 1 + vnpy/app/portfolio_manager/ui/portfolio.ico | Bin 0 -> 67646 bytes vnpy/app/portfolio_manager/ui/widget.py | 845 ++++++++++++++++++++ 5 files changed, 1151 insertions(+) create mode 100644 vnpy/app/portfolio_manager/__init__.py create mode 100644 vnpy/app/portfolio_manager/engine.py create mode 100644 vnpy/app/portfolio_manager/ui/__init__.py create mode 100644 vnpy/app/portfolio_manager/ui/portfolio.ico create mode 100644 vnpy/app/portfolio_manager/ui/widget.py diff --git a/vnpy/app/portfolio_manager/__init__.py b/vnpy/app/portfolio_manager/__init__.py new file mode 100644 index 00000000..e6b84909 --- /dev/null +++ b/vnpy/app/portfolio_manager/__init__.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from vnpy.trader.app import BaseApp + +from .engine import PortfolioEngine, APP_NAME + + +class DataRecorderApp(BaseApp): + """""" + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "投资组合" + engine_class = PortfolioEngine + widget_name = "PortfolioManager" + icon_name = "portfolio.ico" diff --git a/vnpy/app/portfolio_manager/engine.py b/vnpy/app/portfolio_manager/engine.py new file mode 100644 index 00000000..556b2ea8 --- /dev/null +++ b/vnpy/app/portfolio_manager/engine.py @@ -0,0 +1,289 @@ +from datetime import datetime +from typing import Dict, List, Set +from collections import defaultdict + +from vnpy.event import Event, EventEngine +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_TICK +from vnpy.trader.constant import Direction, Offset, OrderType +from vnpy.trader.object import ( + OrderRequest, CancelRequest, SubscribeRequest, + OrderData, TradeData, TickData +) +from vnpy.trader.utility import load_json, save_json + + +APP_NAME = "PortfolioManager" + +EVENT_PORTFOLIO_UPDATE = "ePortfioUpdate" + + +class PortfolioEngine(BaseEngine): + """""" + setting_filename = "portfolio_manager_setting.json" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + self.strategies: Dict[str, PortfolioStrategy] = {} + self.symbol_strategy_map: Dict[str, List] = defaultdict(list) + self.order_strategy_map: Dict[str, PortfolioStrategy] = {} + self.active_orders: Set[str] = set() + + self.register_event() + self.load_setting() + + def load_setting(self): + """""" + setting: dict = load_json(self.setting_filename) + + for d in setting.values(): + self.add_strategy( + d["name"], + d["vt_symbol"], + d["size"], + d["net_pos"], + d["open_price"], + d["last_price"], + d["create_time"], + d["note_text"], + ) + + def save_setting(self): + """""" + setting: dict = {} + + for strategy in self.strategies: + setting[strategy.name] = { + "name": strategy.name, + "vt_symbol": strategy.vt_symbol, + "size": strategy.size, + "net_pos": strategy.net_pos, + "open_price": strategy.open_price, + "last_price": strategy.last_price, + "create_time": strategy.create_time, + "note_text": strategy.note_text, + } + + save_json(self.setting_filename, setting) + + def register_event(self): + """""" + 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_TICK, self.process_tick_event) + + def process_order_event(self, event: Event): + """""" + order: OrderData = event.data + + if order.vt_orderid not in self.active_orders: + return + + if not order.is_active(): + self.active_orders.remove(order.vt_orderid) + + def process_trade_event(self, event: Event): + """""" + trade: TradeData = event.data + + strategy: PortfolioStrategy = self.order_strategy_map.get( + trade.vt_orderid) + if strategy: + strategy.update_trade( + trade.trade_direction, + trade.trade_volume, + trade.trade_price + ) + + self.put_strategy_event(strategy.name) + + def process_tick_event(self, event: Event): + """""" + tick: TickData = event.data + + strategies: List = self.symbol_strategy_map[tick.vt_symbol] + for strategy in strategies: + strategy.update_price(tick.last_price) + + self.put_strategy_event(strategy.name) + + def add_strategy( + self, + name: str, + vt_symbol: str, + size: int, + net_pos: int = 0, + open_price: float = 0, + last_price: float = 0, + create_time: str = "", + note_text: str = "" + ): + """""" + if name in self.strategies: + return False + + strategy = PortfolioStrategy( + name, + vt_symbol, + size, + net_pos, + open_price, + last_price, + create_time, + note_text + ) + + self.strategies[strategy.name] = strategy + self.symbol_strategy_map[strategy.vt_symbol].append(strategy) + + self.subscribe_data(vt_symbol) + + def remove_strategy(self, name: str): + """""" + if name not in self.strategies: + return False + + strategy = self.strategies.pop(name) + self.symbol_strategy_map[strategy.vt_symbol].remove(strategy) + + return True + + def subscribe_data(self, vt_symbol: str): + """""" + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + return + + req = SubscribeRequest( + symbol=contract.symbol, + exchange=contract.exchange + ) + self.main_engine.subscribe(req, contract.gateway_name) + + def send_order( + self, + name: str, + price: float, + volume: int, + direction: Direction, + offset: Offset = Offset.NONE + ): + """""" + strategy = self.strategies[name] + vt_symbol = strategy.vt_symbol + + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + return False + + req = OrderRequest( + symbol=contract.symbol, + exchange=contract.exchange, + direction=direction, + type=OrderType.LIMIT, + volume=volume, + price=price, + offset=offset + ) + vt_orderid = self.main_engine.send_order(req, contract.gateway_name) + + self.order_strategy_map[vt_orderid] = strategy + self.active_orders.add(vt_orderid) + + return True + + def cancel_order(self, vt_orderid: str): + """""" + if vt_orderid not in self.order_strategy_map: + return False + + order = self.main_engine.get_order(vt_orderid) + + req = CancelRequest( + orderid=order.orderid, + symbol=order.symbol, + exchange=order.exchange + ) + self.main_engine.cancel_order(req, order.gateway_name) + + return True + + def cancel_all(self, name: str): + """""" + for vt_orderid in self.active_orders: + strategy = self.order_symbol_map[vt_orderid] + if strategy.name == name: + self.cancel_order(vt_orderid) + + def put_strategy_event(self, name: str): + """""" + strategy = self.strategies[name] + event = Event(EVENT_PORTFOLIO_UPDATE, strategy) + self.event_engine.put(event) + + +class PortfolioStrategy: + """""" + + def __init__( + self, + name: str, + vt_symbol: str, + size: int, + net_pos: int, + open_price: float, + last_price: float, + create_time: str, + note_text: str + ): + """""" + self.name: str = name + self.vt_symbol: str = vt_symbol + self.size: int = size + + self.net_pos: int = net_pos + self.open_price: float = open_price + self.last_price: float = last_price + + self.pos_pnl: float = 0 + self.realized_pnl: float = 0 + + self.create_time: str = "" + if create_time: + self.create_time = create_time + else: + self.create_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + self.note_text: str = note_text + + self.calculate_pnl() + + def calculate_pnl(self): + """""" + self.pos_pnl = (self.last_price - self.open_price) * \ + self.net_pos * self.size + + def update_trade( + self, + trade_direction: Direction, + trade_volume: int, + trade_price: float + ): + """""" + if trade_direction == Direction.LONG: + self.net_pos += trade_volume + else: + self.net_pos -= trade_volume + + self.calculate_pnl() + + def update_price(self, last_price: float): + """""" + self.last_price = last_price + self.calculate_pnl() + + def update_note(self, note_text: str): + """""" + self.note_text = note_text diff --git a/vnpy/app/portfolio_manager/ui/__init__.py b/vnpy/app/portfolio_manager/ui/__init__.py new file mode 100644 index 00000000..aed1a423 --- /dev/null +++ b/vnpy/app/portfolio_manager/ui/__init__.py @@ -0,0 +1 @@ +from .widget import PortfolioManager diff --git a/vnpy/app/portfolio_manager/ui/portfolio.ico b/vnpy/app/portfolio_manager/ui/portfolio.ico new file mode 100644 index 0000000000000000000000000000000000000000..09f632d8d2d2f91982bd5789ab22dd32115f7d68 GIT binary patch literal 67646 zcmeHwWq4f4nXZ$p83r<$%m#Lo-Ay*h!rcu_W-^mxW|9G!g_bO4uvivaWJwlUW@ct) zW@cu#hbg(g?tPxS``!1gbL#ZzR!f%Tw&nFB&+|@Ix4O@%uD3p@ufD3&p~E-uUukIv z{QpNCe%9q19bWFxp~KGryu~-w`>e8d&Z~oh@w)y0?LE-m1MNM~-UIDD(B1>>J<#3* z?LF|+_5eItdb3XWCSGfN3w4dZLEVmyc-riJmBXnRpK6^ESMIy2gvBYkV8^he3aqrM?DBeSO|b-9jZZ)s$6IVXjpPEDVHT#}Cy##y*;I#I; zz$?Jt0^c+7eUv{i@k5lv-U;2>yf?wNuuW_m+eomz zY&YA_-@)Ib#}9w29y9i_Vjm~2V+3D$D*w6itU1;)tL2rynZI9it9fMqX)ZG08 z-up)r|Ag`n~XR}=q+QsduIZw~b5b;)b-nuNXw@5y_!4Fuc6 zcCmeIE8EO5_9H-#Aw6dFc(IQY`xv1d^aQ9=I6kxXW|W%ErI?XOMNX~oA)4iKi-#ZU|ZNGw(UOvwv+8;yE%6FTl9F*+&AF7w^e?zXh-@Y!ln|Cct*G&3e4(aiYhF9v}9xK^?-r`6u8X zfFA+c-h3Z*cVmM3v=?C46P@syTzwPx2GE*+{2ss0>+pJ%@uoQESY`XzPRh0`-~4Sq z2dF>oT(dtYKX#tkckDy<=UV{#SaVAb$O*Y3cYT1qKtEsrFc9ztd`$SF^aK2D2tb_} zVVgK^?BnDeynYMd_|Rj6`h??xznSBLW1VB3 zV}gC7$HdQ2|0&=)9w<}P5w8F*11|wD0`A5FdFu#}%jS#)SKhnedtG1p&Py+R@B82T z&UasV<-6Z``Q^XG>+iq#?Qj3^g%@A=`{!SH{>RTf|J*l?uDyL9dP$1YvFyxpZs zmp=jTcJ9*Uz0O@ae*pCE+_`gKpns>%odI(F(9 z2806X`61fc4sVKOWy9;?#Hfed9X39C^}C0^aY`rSp5hpF4H!{7&bto!DJ}7F5SDl+NFEfUjx7F(xdBtcJ0ya-@Eqg_HW&Kb^itMv+mDz|L5+{ z_xMSV7kd6dHx5_z5K%WUU=ojSHAuImtOAiQZJ5)ZsZTp_ijCU_UPr+uaEygKcBh4(gD8S#IgZC-pl*@3|yh$Jz!-&?*XfT)%^zc zU(?sX+P(w&t?M(O-+G1qeK+*(-*+Rhsdqo3&t_mt@4kJudiCwIO`%Vp?OuI)?@;L7 zd#6|L-n$gMdhhn~>b>U!uikq<_<-=*2kiggeT4%kiGvF7c^&%TJ+H$y96|la2k&|v zwZjXqfn(0-jrV#V?d^;sectV((D&WGhZWxId#Im6zk~hW>vypKd;Jd(1K#hyAJ{kG z{Q>(1zCUp9zz+uQ@mBEO?el^6E*~$SoxTdbJN&$Sw-f%o{kHk{_TL)N+kZK`f^Vg6x4ge!zggg+4>k$wiE{33;jR)~@qKO$OUEyPHiuYq{qSRoR8;}jB6 zCi=!pl7$2r;tTjBO0sXFq$nhz1bmVt)n|x88p<@EWJy;@kqjRLnJ6>8QzgqnnhffcLN3Z&?@Y^h(0gZhG>?eW_0 zUjGF+|A^cNg$7G>e5}O8$9V*?xK3<*yu`)* z5ad7>>REy5k`a(<@tx_PDnkclNH!7RKo06T{+R~+G9}kSmgM<5Bj0zZpt1Ld9Uo*v`2Pt z*e$!(?UbEscF6YC+hyCzZL)2}R@t&_vus|nNj5FoC>s}UkoEJ|$@;l#W$m0bvS!vQ zSv_;5teU<;R!m(kD<&_KWfPak(g}-Y$+$(bc+5gsGiF zIa6kjm?5*ur_0Q;X;PO~Eor{V3N^_iWODulnUp(DCgzNl@k2+;xXe*9Hltp~q>Yr( zDYY_cNR5n4s+PKhDyfYdAvLiTQXO3`Rgq;<88KW&gq28nNU@X+E|TFvg;F}GKuY}c zrPwb|3Vm{9n0M3Tz~ukIZ(e`(7vR^GfBJ-WUw}G*ulc^#0o{K0<{LlL{MU`Hmrp&Cda_Yb-Il1qI9N%+X zj_o=oM|T{N!`lwap)CjH;HCqzf8##cw;p3;?QYqLORBR8V7Z19MmUb z9K>TB#9|yoR~Ta;9AhBVF$PKlhe=6*jsJ(}0nPubZ@l(%a7uk{mwnoLE&Hgde|9@S z{m}UjZ@={u=>7+k|GH82^2ukP2{~*PzI4a^M~eIVcftMb`*P=IL+-Dhzb4m!rSon4 z&t5F&k3m-)f}T*^pOh1O!ToM{$cuS3y6l zfPPp8-LPbfY$f*#pc}yb#<}Zd{cLbQbB(N>4(_Ld`^hV0<-}#O9Q-dEw^;GNc+>(} zG;+Qys5Q8sUo~4sW!m^pCI3@o+OSD7HGiT^&K)n4vd77Up<`rx2Kk2`NQE9qh8{?Q z9!LQHag|aVGs2+*!b_!s{0Fg{_nr>)|-qeQ0Hsk&o1|hc$fd_aq!J| zz25(^C;9*Ei!V&s|5QFwvj36X*Rp^6p4_&{{&iFK$vXiLKgX_>^u0cSNvBg{%c}j1EOF9l>P_*)c-dAi=qFEd~;jE|Gl^0 z|I?4bFF7Y%W!{B)$IG;#nH@0K`yu$h?+N+;{L3%pv(Lf*r_lQ!gZ~ddmJjc1{_iW@ zfBVKAxpnQf+_(Z?;SzQKHMw&Bs$4yH75pz%+*ALP|8qyrS-Ss((*4Jv`%V709a6gg zz^45s|B!t*x__(E{hJ{F@C$6Zf32naA^X(*CjU#v8oJ-$-_-r!eqJTzzt+IPH)-|gem?}ux) zY;X|U%o^-|2XOxbPxJqY;vaS&{68T7(Ea!BK==t ztVI_8C-+nL!}crPZ}5Kz{2zqvC;txJZ`l5w%JxI|!}f1oyjiv^gzks!r|#b{d!3>C z75~uvQ%wGq?l*1!DA<1J{)M&h2Vwgu`|~Pi$*7E4q3tK?Qoz4y`zPl?_q*DD>VCz4 zjpDy)+YjGQ*?xon=J=^4m<-9UlYlp5@=}_kFDS|4{L-ZU61-;NSH7 zu3x$?SMB`ITPoz<;{V7w!}crw9k&07-S#{2@38$lY_{Lde@kq?;vaq=`JX#tmegDP zr-1(|gMU}suYJE3*#0u*`hV=WD?+3rXDccX`ysC5Yw%Csuc7T9En{uIpWXH=-_LIQ-S~dZwf#>U{|?*# zxncW1g6)6c-uB|Nae#4Xt3&<>XWOzZ%4XC4>zJ>a_nR?ahwo?D{!6DX39)?sa)}F%Q&=*4iJJQxwx2Oy zJ?}Sdzq`24N;B@mm@ndf&T$_%G2dp|e)2!tEFMLH?7XvBn>jBulCPIrp=z z`84kPIp+OF+{cLfZB}u=maqBPZ9nII)cv!EL;k_PTJOi2KmC8rzgqKAzMmuJ-<&m{ zN3s1j{;##E{F`x~&!7ViY~C-aZ^cOLuY)D_SHUvOCri#AJ*)J;Uhi#a`!6f*FP^+8 zD;BJfA<;t=md;+P=KfCWy>8<^M$DJK|Be+7-_Nb>cUtpv%=;UQ`*7Z`Y(I4WEGPam z75~m_K8X9Ifq%^Vx#nm0{q(%wvF78v=GW4Bzt#ba|08#*P5IZpAJ=-OG<;(l23am0O$H6O-&X!|dKd(D4xEOK{ZQXv1}fBz|SyYt(TfT?8HJ|mAeKYRkcFl)zAFlbh#(kjsDf>?R2WA=a-MpUj(i{HdpvZ_&)N4Q|1(Yb&jt5H-Hj_#e{KCp`S9b9RUD78yoZ6gLd1nr7o=cdrjmEfyDn)D%EJof?6I?Q zRju_y=bwemKYdZoL+;N(H_%5Qmd{%*=?UqQ9-l5tXDw0lz9TzKpMNXXdf{I%*TahU zu0fv9D&%=7yKkHGFGQ}#eB^n}#hl;F^>M8C!0u0SpX+0+_quXF4ct#vxjyx2HIf@- z@Lvl*SI7L_>VB7bzr*$yH^}#C$(oPm-X{MZ;UBpGl9G`oQ)Wz)X|ra?w3*FEb=C+; z_-&Zd`I>ionfSX1smrUDN#iHW#IciP;+TmtVKgvmf{d>RMoy3kKv_YV#D&F4d|14c zd2+Z=j@J>WfE8eNmm%7uLzhVL(Bx)a3(t zHBy&bBeglzQkxA7t&*B7qEf0efeauW`B-Tqq$(9Tc_|fAnOrWFLx3d2=@N8kw66UH^Kpo^U_ctgur@kF!DJEV_hc*IzA8>lqbajc~ayL z_#wxKc|KU{ZBDM2op)U~<^QrL>(*eNH&8$z-@Cvk+W_r-3vIsU-X{MZ;h*v!6&EAJ zii@PMq`2A0h)k8ZSDWD8^abNy3zf{sbScck^>cxoLIc?-O=KozN_23v#01AkR$`_U zW)73W3?SV^8cHG+NHLLIAce#bAgMrx0fmVMh9%@nVFK#$ct0**p)eMuiNYAvqk*VA z2O?3AFcFTD2s4luG*q(u5dQ`80*A`5VB~2HHW7p}5P2KKAQJ&734g%PgfB{hIUX1X zivL!Q0aN}jLH@t%3IDIMrdqBehyV6nzoaTI#`5|vasu-Xp z|K?nvGNx3H?mH?c4`W_;@Pr)Oe_W34#oTW95e2UKWhG@PESR=H_A%#wJ@fr{seJz( zt1!Q3zW;K>ca|cjcd?o8zW}y=9`d@F?>`$p-c026PKTd24Y?gtVDBd*w}biq8dW3C@#zRdM+t?jqT|3y#n&-`!te9g)I=N^GW z@X1o&!g?LzJ(_FQC+ER#XVf|QgM2^r?O5dHZk20@{d4Z8e1FXQnCDHbUbtFvQgRel z%v~-g_F#PMY{8oE5{wI->ka>pYyO<`%>a*_=dYQ%$~o5?^M1ztb*{I&xZkYd(_}^o z#sYGEN2S$D{@`o{`u{4||6#e_D&NO>&CkBq<7wl+x%vNG=bxNE)=BosaoRg^a(wSG z#OV)8${&qbpfxrS58$|9yvK<9fP47=%=0ByEnFqJsksWv!9VAID%abd>+7=SW8ddt zt@)U_zT|%@WS3)fV)kg2>))JQUwhn#^FHSJGuM~=7X;@>!QdRJOR7@2-fn&W$B^r5 zxBI$o>i-L#l>g@N|0QIfdHGrIImde#_aB>Hhx{)i_j45VrCtl*7~uFw|8oNJG&;4Bu;yv*3 zhrAgnyD=xHZ6J27-l5*J&kZ;RCgqM*@jk}<&Oz@p&yyhk`RVzRpO!Dw|J>*G@HHRI z|82RxT=QYR7h^od@Vj_Fj=hA}!eFoKRj$8tzK^lb$D#Y-3z%y@)crF}-OqhqY%h$-&zk+yAhLe4#j8#UK zs5O7>Kis=}U#eqe$!9Q|6$^S6;`^1X5D-{3k_W3lK>kIz5 zzppM~gv7rZ>g1Q?_DNMSUd6vH=I@&8YpwYx-A~&OKM=XT4*qj3{u|rp(;(M3#Cgr< zQGGv-0UQ4>d&2*37XR26%6P85Z?$}Ffd9?$p<~CaZ&|dd!CZi!pI$uCu>Iiw3}m0V zp5$N8`4s=~{q!DBN3QQf_IP5SA8mdT<{);x!+XXfMl}Zea+f3D%X!TQa{*hfuU_*p zY(Myi>|6YYV4jD0pOSz0etON%+UMgo*Vmr!0sXIXeGvm{<~}bi@2;iE|2fG26XBn7 z%{-nm#I@|}d|c-#^3PU#dHETOfAjO@h=sZO0XfJI*}rkG(*NY1xqhn`t(Ky!B88QZ zfBOE1ZTozzT;DbB_W59+C-(X*!XEC-cM~nWPu?|X3$neERc?n~^IcTm$eIt-eB|4)p6+VpKpw+Q38x=(evWb6Wqcg;Ubj)!$K zS2u_cQ-92^mVL&Q#(?5q`+nvg-^ZEj+XVk-pM?CAN9Og8OdQcrmiwfrH9WiS z*JFU+W6p>2#h@oN57Zf4Gvb;K`R5-0HH+3rNp^{p3@tJEr|*v#gR#fEiCkag`*FRe zHogL}NQceW_p+DN0bHl%ejdyB=RQB?dh2`-<@@V-zajgI|CEOOYv0eF@8Pih&hvi5 z_j~lXj~)xA{s;d)k6r&$_785^?{-c3+`p z4co8IcR>8F9{d-DAZ7;`@^9w*G&k2bP}}}Ti~G6of9A3C|MgeGeLa&9*K@Vw>F>m= zby|)Ocf9jG>V?Z#^UWXNJZGQ|pkCmbKIMNcut?>tWbpWL24bV@ScbM~t^7RF_cRPmlKFh&~VpYJnIWI^4W z2J*>V;l+r>GoD2KUzS%UWw~XB{F8s4@6kxE?@pZW!uakmtf|@gCI1{-HL+zXADup- z9uxMmDr&e(zK@aXspCF&+s|B2%73wqfBOD*{u}P`_FS&7op)U~`9J-*`DeUmUe!$J zK86ep`F)h^!P-nyvaiPkV}|FCpOfs~$xhcHPs6cBPCtL`; z{Of%__VZns<5?41-hf+PGp~Od@<&g~*Z=#k*k5$1q3%KZ)BhB&)_AD@Y5N<-eN5ZW zJ>J;kZ}4BB*8XcD|0?d+%=2BWxX&Z5`DpH0n*5)7ocud+rL+z?_r+rGNoX=F&DAfd|o@{57BBL zf~Av?qs;kFv5_3R}2*i>`x}5J~+vj6H-$(E9Zsk57&9`f5@_+Ji$v@|A%-bJ6IKM%> zrx-a7H?H1nw(P61VEU0$3Y_!A>9bR2Czgzr>Y^%)flBbdN}ccJYWum*lV^9Sd_(Z3 zxnxOhIX}I9^N!ngEr*VA9mo7$Ir6nQALJfSC)=-mKl5xiga2XB0h<4ZobBepTwho2 z`P#<+OOM0|zY$`ejIEL8Vj`RvG-e zIp2r-`njgy+8%OW;GK!Jgfnh+5_JIg9WhR6*8y=DL#bG&o`f?T4gPIwK4#oUo$ZEP zU&Viz!G8_pU-^FeY`6Hv&USmWJ>Kqk*YBD9pMd;(oPQ(U^P!5VxX$^ww~za^TB7@{ zu>qaJbK%Az55Rt1@|pNXgp?!|N=;F<#lJq=jr;s~zRTo%*nF4q$Z^Cq{+%nfxs`kC z-mnE~?E>>c*YQ(|9L-s@{ml2^yx&~&(dWBxpQq-ZHh}uS@qIqr=jqtv?WueZ&AToi zC;ybw1Dp0Ky=<3L%Fe9vsp_mBd%w1%&TH@6ut$=i^X=EB9mw)dk($D4Sq=SfKiiM| z)8^A&JADV^hH)g$@wgAc^7+8EYnwQxxd)*nFwe=JY5OxFYqZk;dcEJ_`;Eidj$>uy zkZLJ`{5$c_v)$aT`G9|0zL(nP<3Zn_W5DG9xTocx{q)HvpW0ADr>PJbo2Fa%1yHBQDP~CU{+akH$)V zW*zbanGfKq1BwH3mF$y$EAFGub#>x@m{9*)YkoT4hkLwvwy(Lz+dA8~mFK%?Id80d zT>O9a*T1N_bH=;QF*WMz@mclie5a=5tg*hgzfa$SxqwN~`K~eBTg(y3jPs4 z=K0RXnh$J0&h@PZ|LSaKOZPuM+i#QqW1f_M=6^C)H>Yxjvz~?ACI3EJ$s)%G`|*(g ze~f2BGH1le4FzetzS;^$q+8j)6koY{>p3 zW6e*W?Pu=u()^cd{u^ z-9D~Z&-5B16|wN&2<%xX#~z3BD6C6^`+`91-5Hdv_I3?dd%Z9og5eV)hFuZ_1ahAj z;K6-fuDt8lru-lA6#vQTsWQ5LjHG?w99N?rNP9nC#*P{*lcr6PNmD%tT(5LUVFO=` zahfE^qANFSODftjs%F%qwFUPbH+H;a^mEQ(=KT^dzvMoivgl$N9*KQ@1ax09)_wEA zJyC?1SP6M|v&XN<&b`{_hdqAHc%FmiwYjCI`OhmXkg+AB8ps5>m{2rEwrt-fTefYJ z&D*wm47}#*W!U%8FV)%Kpq#rv+x&%7=Bu_mYCCxES)*ncKB)a(&@Y+D5vj%=pBl_j zxYwr|xh3WBRSE|q{|hJ!D}p_QZU~2N2!o#&iZK#gh*&A~0rdc%<-r_Zz1I_Gdbf6; zXJg~Ry8( zXK`+mNxnZRcZ|wGqp#o6do=cW-kbYThP)XCzrt~E-Y*CF;>__kp6M_e&+;Mv#y)SZ z^SScxbhe*b^U?V}hVS<%dwn(M_R{445aj=n`RB9RBHnhK-9rx82h6KEx&Oo?#d{j- zd;9y$F_=?1-7WvrN1U(oIT5XjIlGAk>U(o6@to3;wsXt$_@ln5!kK+q{zoQONtxom z$dG?q+{c*ryFAm$7Wa9K_TMA?2M!n{F|RqsKl_05@(I{C_c3CDj~egM{oPo}n8LoT zdnM&4qzO?H6G3JpY4^Ra{Q_PUHRAfJ`MKya-UZaWuNxXPHK-YW#6&pdsP!(_J%m;#f&oLJhw!Y*w-Y3IHZ z5R?A{t(AX&zkm)tkpIwkgYIjar%T$tEqIn9PEemPi1 zL=@v0QP&@v-nX}(_gCxEh&wv13n(2BhBKQAP5lquPX2MWLtU(y?_*o@v9I~H_FP{r z>*PJhD-AjKct$POILS@MpAy~i|3Yj0fASB%?+1bX2i*^U2m1dF;~byp{|v$$Hecob zJZ`zu94P)F|4Uaam+@8Oa6V2Z_J1FJBHZgXPzM~wxdjb#Q;d9Z%6vKU1vUT1nveNB zPp%aRU|E=PmV+K&q|MKzolcJm= znTK=yhT%E2t8i|L?w2P~lK=A;FDm{QE?gwLHtu|^Hs9@ULVg;bTgP?p208%pSBSO7 z@`#c~_;7 z{#UGCB?QO9lWv1*i&KuOvro)8cpmmJG2To54c||n>kIjJ&G*5)ugT~6Xx`syTHZD9 z%k7({av%9z+tbI+NOo?n zFlG=aocu5@RvubxobMO$kX&D``yyA<;5}Z&lPsP!-?}t;=lO}X zapgGIc$-?EQuY`&nK4-Q$=&<+B|9JT|BUf}`RY|!zj31i>ut$Db--uHg=4O>lOIT~ zppOd%WHx+`x8CQ&wO{)F;6Aqxm4+_TC&c0MziW6-q^-0;8qS&@HozF{-Y zZvvhM{u%q?eiEJ;<2nXX{t%=5J!8!mIRKXLhx|~){PX)~VSidev*ew6o_kuhBOjK} z%j11rdDp&}#ebe9|IZBnSFT-?Olq5S}@<%{145`(e|JGKSy7# zrT_RHK*j%_2LJEs_3N^A`*sD^p9cA-+*xN=Xg|$Xa&2hn2RH|xXV2K)cdb+2sS~)S zu?2f7Tgtnu{145w_}}@=@PFgFY}>g*;VSA+3;*=LnV*zD!1)XWa?JUlE7$hdns?6Q zHZRqx~{~gbY{PWoZyY}o+VEt+0U&RCupOB$_8qNh=d1q{= z9D77I;+X_IFO$E+Q@=&=k3BD0ITruhJ;{G_&;QrI$&=-cTeoD-zI_U;d-B?kdM)zL zHccoV+u$5K^3NPa$~&KZ%;!##cYa6D;UD$>`un^d<3Eps|BC8LHSebEU3-?`J$!CDtTk#g@I75AGne?InR%~$nZc<+ha z+q5}N-V1G6huGwt1@ojL-8g?ltpyCkx=i^5*|27V?B2ImcJJN$c-xG2Y}~S05|W2_ zod03Pg|ZX=*B<_kCk)-N$A@2ooQN60EmH|2B0`w&@h+B}7}bPxH^Sdd&FGGsFMA z2M^>7<~9WDO>xl{f4lqOzKXpu{>FM+w87%v5&zrxtjIq(KZiYl#M8(>+jRBvL9 zRr}oVzXAOB_@D0oQI>e7-1$ov6JjhE|uPCtI6c=srH`PAuOW@vwwrZc|KP4kg`+v_4{~v#( z)*rYA@c{L<;JB%Kl5@6AwXcoFC;4aW(Z>I}XGQ)${P<&G-WS1oQ(Uyg-`FPRr4nt` zK8yc!Oa7l3{y+NU6P5G990%6hg5##{`82do@qg!z@T^vI{=XLT-~Cze|JlDcZ{JpU zTKT`h^{o*Hm*ibAH7A`+Ba-lIOYbOb%FUk+~*w;`Aw%L(1GywT@`6)g3#DHDv`&>`7>=%DOBcu@8o zJYe9!0rfe4mY*S1ZrFY!uUY2^syspD^64DMoqKkR!tUMHJ$CHcCEIuHG=RKJ?v3H= zHS5<&bbPF~|EoP9{||5t2?`BX*NusfQzh@=7IAnVh>DA88nO605E&cYIAZWOAR;D8 zAv`)#2>seBqw#k@|GmjFDpJCtBAP~IgoH+hKO7O^5)u(+Avio#2>W{>;bH1M{kKNT zuuvHs7V>a}hA1w`hdTs@3>Lq@0OtM@tAK7mYx95k?6{MFx*mPXcC>jP{5LpohyTL+ z{{^lz{x9k!z&C)_^8ZbI*9({i%md~Eb4<)eNz4LfnwWu-m~MjKBc_^|f|8hQViHPX zqKOG8iSZ`Jp(Mtd7=w}+ZDJJ4dJ`j25_KkOQ4%#Ks!OvQZL4O=O|Wv>^labQ5VPiBuCQD2Zef zLr`iALY?xfa`{_20QC3F@^4UfazRJDb_age9lnM4-GO7w9XM{?LH!>5t-WkPAGDNz ze!jhr-H)->^kw^Z+IyhA2ikj}y$9NRpuGp$d!W4s+IyhA2ikj}y$9NRpuGp$d!W4s z+IyhA2ikj}y$9NR;7Rs?wC-Q&H*DX#V>Gz4`BtfJzwwzp`ela>-@+&U;&|Di!*e3` zmmNCD|8ai(_j{aQf2-8_b(_C(Y0o$AIJc Date: Wed, 13 Nov 2019 15:45:27 +0800 Subject: [PATCH 2/5] [Add] widget for portfolio manager --- examples/vn_trader/run.py | 22 +- vnpy/app/portfolio_manager/__init__.py | 2 +- vnpy/app/portfolio_manager/engine.py | 15 + vnpy/app/portfolio_manager/ui/widget.py | 834 ++---------------------- 4 files changed, 99 insertions(+), 774 deletions(-) diff --git a/examples/vn_trader/run.py b/examples/vn_trader/run.py index bcc8366d..e55a1cef 100644 --- a/examples/vn_trader/run.py +++ b/examples/vn_trader/run.py @@ -33,15 +33,16 @@ from vnpy.gateway.bitmex import BitmexGateway # from vnpy.gateway.gateios import GateiosGateway from vnpy.gateway.bybit import BybitGateway -from vnpy.app.cta_strategy import CtaStrategyApp +# from vnpy.app.cta_strategy import CtaStrategyApp # from vnpy.app.csv_loader import CsvLoaderApp # from vnpy.app.algo_trading import AlgoTradingApp -from vnpy.app.cta_backtester import CtaBacktesterApp -from vnpy.app.data_recorder import DataRecorderApp +# from vnpy.app.cta_backtester import CtaBacktesterApp +# from vnpy.app.data_recorder import DataRecorderApp # 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 +# from vnpy.app.script_trader import ScriptTraderApp +# from vnpy.app.rpc_service import RpcServiceApp +# from vnpy.app.spread_trading import SpreadTradingApp +from vnpy.app.portfolio_manager import PortfolioManagerApp def main(): @@ -81,15 +82,16 @@ def main(): # main_engine.add_gateway(GateiosGateway) main_engine.add_gateway(BybitGateway) - 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(DataRecorderApp) # main_engine.add_app(RiskManagerApp) # main_engine.add_app(ScriptTraderApp) # main_engine.add_app(RpcServiceApp) - main_engine.add_app(SpreadTradingApp) + # main_engine.add_app(SpreadTradingApp) + main_engine.add_app(PortfolioManagerApp) main_window = MainWindow(main_engine, event_engine) main_window.showMaximized() diff --git a/vnpy/app/portfolio_manager/__init__.py b/vnpy/app/portfolio_manager/__init__.py index e6b84909..27405d91 100644 --- a/vnpy/app/portfolio_manager/__init__.py +++ b/vnpy/app/portfolio_manager/__init__.py @@ -5,7 +5,7 @@ from vnpy.trader.app import BaseApp from .engine import PortfolioEngine, APP_NAME -class DataRecorderApp(BaseApp): +class PortfolioManagerApp(BaseApp): """""" app_name = APP_NAME app_module = __module__ diff --git a/vnpy/app/portfolio_manager/engine.py b/vnpy/app/portfolio_manager/engine.py index 556b2ea8..d5dbaddf 100644 --- a/vnpy/app/portfolio_manager/engine.py +++ b/vnpy/app/portfolio_manager/engine.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Dict, List, Set from collections import defaultdict +from copy import copy from vnpy.event import Event, EventEngine from vnpy.trader.engine import BaseEngine, MainEngine @@ -16,6 +17,8 @@ from vnpy.trader.utility import load_json, save_json APP_NAME = "PortfolioManager" EVENT_PORTFOLIO_UPDATE = "ePortfioUpdate" +EVENT_PORTFOLIO_ORDER = "ePortfioUpdate" +EVENT_PORTFOLIO_TRADE = "ePortfioUpdate" class PortfolioEngine(BaseEngine): @@ -84,6 +87,13 @@ class PortfolioEngine(BaseEngine): if not order.is_active(): self.active_orders.remove(order.vt_orderid) + strategy: PortfolioStrategy = self.order_strategy_map[order.vt_orderid] + strategy_order = copy(order) + strategy_order.gateway_name = strategy.name + + event = Event(EVENT_PORTFOLIO_ORDER, strategy_order) + self.event_engine.put(event) + def process_trade_event(self, event: Event): """""" trade: TradeData = event.data @@ -99,6 +109,11 @@ class PortfolioEngine(BaseEngine): self.put_strategy_event(strategy.name) + strategy_trade = copy(trade) + strategy_trade.gateway_name = strategy.name + event = Event(EVENT_PORTFOLIO_TRADE, strategy_trade) + self.event_engine.put(event) + def process_tick_event(self, event: Event): """""" tick: TickData = event.data diff --git a/vnpy/app/portfolio_manager/ui/widget.py b/vnpy/app/portfolio_manager/ui/widget.py index 44dda6fe..43c6bf33 100644 --- a/vnpy/app/portfolio_manager/ui/widget.py +++ b/vnpy/app/portfolio_manager/ui/widget.py @@ -1,7 +1,3 @@ -""" -Widget for spread trading. -""" - from vnpy.event import EventEngine, Event from vnpy.trader.engine import MainEngine from vnpy.trader.constant import Direction, Offset @@ -14,18 +10,15 @@ from vnpy.trader.ui.widget import ( ) from ..engine import ( - SpreadEngine, - SpreadStrategyEngine, + PortfolioEngine, APP_NAME, - EVENT_SPREAD_DATA, - EVENT_SPREAD_POS, - EVENT_SPREAD_LOG, - EVENT_SPREAD_ALGO, - EVENT_SPREAD_STRATEGY + EVENT_PORTFOLIO_UPDATE, + EVENT_PORTFOLIO_TRADE, + EVENT_PORTFOLIO_ORDER ) -class SpreadManager(QtWidgets.QWidget): +class PortfolioManager(QtWidgets.QWidget): """""" def __init__(self, main_engine: MainEngine, event_engine: EventEngine): @@ -35,50 +28,31 @@ class SpreadManager(QtWidgets.QWidget): self.main_engine = main_engine self.event_engine = event_engine - self.spread_engine = main_engine.get_engine(APP_NAME) + self.portfolio_engine = main_engine.get_engine(APP_NAME) self.init_ui() def init_ui(self): """""" - self.setWindowTitle("价差交易") + self.setWindowTitle("投资组合") - self.algo_dialog = SpreadAlgoWidget(self.spread_engine) - algo_group = self.create_group("交易", self.algo_dialog) - algo_group.setMaximumWidth(300) + strategy_monitor = PortfolioStrategyMonitor( + self.main_engine, self.event_engine) + order_monitor = PortfolioOrderMonitor( + self.main_engine, self.event_engine) + trade_monitor = PortfolioTradeMonitor( + self.main_engine, self.event_engine) - 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.spread_engine - ) + vbox = QtWidgets.QVBoxLayout() + vbox.addWidget(self.create_group("策略", strategy_monitor)) + vbox.addWidget(self.create_group("委托", order_monitor)) + vbox.addWidget(self.create_group("成交", trade_monitor)) - self.strategy_monitor = SpreadStrategyMonitor( - self.spread_engine - ) - - grid = QtWidgets.QGridLayout() - 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_group) - hbox.addLayout(grid) - - self.setLayout(hbox) + self.setLayout(vbox) def show(self): """""" - self.spread_engine.start() - self.algo_dialog.update_class_combo() + self.portfolio_engine.load_setting() self.showMaximized() def create_group(self, title: str, widget: QtWidgets.QWidget): @@ -94,752 +68,86 @@ class SpreadManager(QtWidgets.QWidget): return group -class SpreadDataMonitor(BaseMonitor): +class PortfolioStrategyMonitor(BaseMonitor): """ - Monitor for spread data. + Monitor for portfolio strategy. """ - event_type = EVENT_SPREAD_DATA + event_type = EVENT_PORTFOLIO_UPDATE data_key = "name" sorting = False headers = { - "name": {"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}, + "name": {"display": "策略名称", "cell": BaseCell, "update": False}, + "vt_symbol": {"display": "交易合约", "cell": BaseCell, "update": False}, + "size": {"display": "合约乘数", "cell": BaseCell, "update": False}, + "net_pos": {"display": "策略持仓", "cell": BaseCell, "update": True}, + "open_price": {"display": "持仓价格", "cell": BaseCell, "update": True}, + "last_price": {"display": "最新价格", "cell": BaseCell, "update": True}, + "pos_pnl": {"display": "持仓盈亏", "cell": PnlCell, "update": True}, + "create_time": {"display": "创建时间", "cell": BaseCell, "update": False}, } - 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): +class PortfolioTradeMonitor(BaseMonitor): """ - 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.strftime('%H:%M:%S')}\t{log.msg}" - self.append(msg) - - -class SpreadAlgoMonitor(BaseMonitor): - """ - Monitor for algo status. + Monitor for trade data. """ - event_type = EVENT_SPREAD_ALGO - data_key = "algoid" - sorting = False + event_type = EVENT_PORTFOLIO_TRADE + data_key = "" headers = { - "algoid": {"display": "算法", "cell": BaseCell, "update": False}, - "spread_name": {"display": "价差", "cell": BaseCell, "update": False}, + "gateway_name": {"display": "接口名称", "cell": BaseCell, "update": False}, + "tradeid": {"display": "成交号 ", "cell": BaseCell, "update": False}, + "orderid": {"display": "委托号", "cell": BaseCell, "update": False}, + "symbol": {"display": "代码", "cell": BaseCell, "update": False}, + "exchange": {"display": "交易所", "cell": EnumCell, "update": False}, "direction": {"display": "方向", "cell": DirectionCell, "update": False}, "offset": {"display": "开平", "cell": EnumCell, "update": False}, "price": {"display": "价格", "cell": BaseCell, "update": False}, - "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}, + "time": {"display": "时间", "cell": BaseCell, "update": False}, } - def __init__(self, spread_engine: SpreadEngine): - """""" - super().__init__(spread_engine.main_engine, spread_engine.event_engine) - self.spread_engine = spread_engine +class PortfolioOrderMonitor(BaseMonitor): + """ + Monitor for order data. + """ + + event_type = EVENT_PORTFOLIO_ORDER + data_key = "vt_orderid" + sorting = True + + headers = { + "gateway_name": {"display": "接口名称", "cell": BaseCell, "update": False}, + "orderid": {"display": "委托号", "cell": BaseCell, "update": False}, + "symbol": {"display": "代码", "cell": BaseCell, "update": False}, + "exchange": {"display": "交易所", "cell": EnumCell, "update": False}, + "type": {"display": "类型", "cell": EnumCell, "update": False}, + "direction": {"display": "方向", "cell": DirectionCell, "update": False}, + "offset": {"display": "开平", "cell": EnumCell, "update": False}, + "price": {"display": "价格", "cell": BaseCell, "update": False}, + "volume": {"display": "总数量", "cell": BaseCell, "update": True}, + "traded": {"display": "已成交", "cell": BaseCell, "update": True}, + "status": {"display": "状态", "cell": EnumCell, "update": True}, + "time": {"display": "时间", "cell": BaseCell, "update": True}, + } def init_ui(self): """ Connect signal. """ - super().init_ui() + super(PortfolioOrderMonitor, self).init_ui() - self.setToolTip("双击单元格停止算法") - self.itemDoubleClicked.connect(self.stop_algo) + self.setToolTip("双击单元格撤单") + self.itemDoubleClicked.connect(self.cancel_order) - def stop_algo(self, cell): + def cancel_order(self, cell): """ - Stop algo if cell double clicked. + Cancel order if cell double clicked. """ - algo = cell.get_data() - self.spread_engine.stop_algo(algo.algoid) - - -class SpreadAlgoWidget(QtWidgets.QFrame): - """""" - - def __init__(self, spread_engine: SpreadEngine): - """""" - super().__init__() - - self.spread_engine: SpreadEngine = spread_engine - self.strategy_engine: SpreadStrategyEngine = spread_engine.strategy_engine - - self.init_ui() - - def init_ui(self): - """""" - self.setWindowTitle("启动算法") - self.setFrameShape(self.Box) - self.setLineWidth(1) - - self.name_line = QtWidgets.QLineEdit() - - self.direction_combo = QtWidgets.QComboBox() - self.direction_combo.addItems( - [Direction.LONG.value, Direction.SHORT.value] - ) - - self.offset_combo = QtWidgets.QComboBox() - self.offset_combo.addItems( - [Offset.NONE.value, Offset.OPEN.value, Offset.CLOSE.value] - ) - - float_validator = QtGui.QDoubleValidator() - - 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) - - self.lock_combo = QtWidgets.QComboBox() - self.lock_combo.addItems( - ["否", "是"] - ) - - 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) - form.addRow("开平", self.offset_combo) - form.addRow("价格", self.price_line) - form.addRow("数量", self.volume_line) - form.addRow("超价", self.payup_line) - form.addRow("间隔", self.interval_line) - form.addRow("锁仓", self.lock_combo) - form.addRow(button_start) - - 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): - """""" - name = self.name_line.text() - direction = Direction(self.direction_combo.currentText()) - offset = Offset(self.offset_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()) - - lock_str = self.lock_combo.currentText() - if lock_str == "是": - lock = True - else: - lock = False - - self.spread_engine.start_algo( - name, direction, offset, price, volume, payup, interval, lock - ) - - def add_spread(self): - """""" - dialog = SpreadDataDialog(self.spread_engine) - dialog.exec_() - - def remove_spread(self): - """""" - dialog = SpreadRemoveDialog(self.spread_engine) - dialog.exec_() - - 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): - """""" - - 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.min_volume_combo = QtWidgets.QComboBox() - self.min_volume_combo.addItems([ - "1", - "0.1", - "0.01", - "0.001", - "0.0001", - "0.00001", - "0.000001", - ]) - - 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, 4) - grid.addWidget(Label("主动腿代码"), 1, 0) - grid.addWidget(self.active_line, 1, 1, 1, 4) - grid.addWidget(Label("最小交易量"), 2, 0) - grid.addWidget(self.min_volume_combo, 2, 1, 1, 4) - - grid.addWidget(Label(""), 3, 0) - grid.addWidget(Label("本地代码"), 4, 1) - grid.addWidget(Label("价格乘数"), 4, 2) - grid.addWidget(Label("交易乘数"), 4, 3) - grid.addWidget(Label("合约模式"), 4, 4) - - 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) - - inverse_combo = QtWidgets.QComboBox() - inverse_combo.addItems(["正向", "反向"]) - - grid.addWidget(Label("腿{}".format(i + 1)), 5 + i, 0) - grid.addWidget(symbol_line, 5 + i, 1) - grid.addWidget(price_line, 5 + i, 2) - grid.addWidget(trading_line, 5 + i, 3) - grid.addWidget(inverse_combo, 5 + i, 4) - - d = { - "symbol": symbol_line, - "price": price_line, - "trading": trading_line, - "inverse": inverse_combo - } - self.leg_widgets.append(d) - - grid.addWidget(Label(""), 5 + leg_count, 0,) - grid.addWidget(button_add, 6 + leg_count, 0, 1, 5) - - 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() - min_volume = float(self.min_volume_combo.currentText()) - - 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()) - - if d["inverse"].currentText() == "正向": - inverse_contract = False - else: - inverse_contract = True - - leg_settings[vt_symbol] = { - "vt_symbol": vt_symbol, - "price_multiplier": price_multiplier, - "trading_multiplier": trading_multiplier, - "inverse_contract": inverse_contract - } - 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, - min_volume - ) - self.accept() - - -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) - - 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) - - 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): - """""" - 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, 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): - """ - Manager for a strategy - """ - - 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"] - 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.strategy_monitor.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 spread_name and name edit if add new strategy - if self.class_name: - self.setWindowTitle(f"添加策略:{self.class_name}") - button_text = "添加" - parameters = {"strategy_name": "", "spread_name": ""} - 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 + order = cell.get_data() + req = order.create_cancel_request() + self.main_engine.cancel_order(req, order.gateway_name) From 9ee00b0a584eda8517e32a72db036561fbd15875 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Wed, 13 Nov 2019 21:13:51 +0800 Subject: [PATCH 3/5] [Mod] strategy trading widget --- vnpy/app/portfolio_manager/engine.py | 41 ++++- vnpy/app/portfolio_manager/ui/widget.py | 233 +++++++++++++++++++++++- 2 files changed, 259 insertions(+), 15 deletions(-) diff --git a/vnpy/app/portfolio_manager/engine.py b/vnpy/app/portfolio_manager/engine.py index d5dbaddf..0d166b4c 100644 --- a/vnpy/app/portfolio_manager/engine.py +++ b/vnpy/app/portfolio_manager/engine.py @@ -5,11 +5,11 @@ from copy import copy from vnpy.event import Event, EventEngine from vnpy.trader.engine import BaseEngine, MainEngine -from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_TICK +from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_TICK, EVENT_CONTRACT from vnpy.trader.constant import Direction, Offset, OrderType from vnpy.trader.object import ( OrderRequest, CancelRequest, SubscribeRequest, - OrderData, TradeData, TickData + OrderData, TradeData, TickData, ContractData ) from vnpy.trader.utility import load_json, save_json @@ -17,8 +17,8 @@ from vnpy.trader.utility import load_json, save_json APP_NAME = "PortfolioManager" EVENT_PORTFOLIO_UPDATE = "ePortfioUpdate" -EVENT_PORTFOLIO_ORDER = "ePortfioUpdate" -EVENT_PORTFOLIO_TRADE = "ePortfioUpdate" +EVENT_PORTFOLIO_ORDER = "ePortfioOrder" +EVENT_PORTFOLIO_TRADE = "ePortfioTrade" class PortfolioEngine(BaseEngine): @@ -35,7 +35,6 @@ class PortfolioEngine(BaseEngine): self.active_orders: Set[str] = set() self.register_event() - self.load_setting() def load_setting(self): """""" @@ -57,7 +56,7 @@ class PortfolioEngine(BaseEngine): """""" setting: dict = {} - for strategy in self.strategies: + for strategy in self.strategies.values(): setting[strategy.name] = { "name": strategy.name, "vt_symbol": strategy.vt_symbol, @@ -76,6 +75,14 @@ class PortfolioEngine(BaseEngine): 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_TICK, self.process_tick_event) + self.event_engine.register(EVENT_CONTRACT, self.process_contract_event) + + def process_contract_event(self, event: Event): + """""" + contract: ContractData = event.data + + if contract.vt_symbol in self.symbol_strategy_map: + self.subscribe_data(contract.vt_symbol) def process_order_event(self, event: Event): """""" @@ -102,9 +109,9 @@ class PortfolioEngine(BaseEngine): trade.vt_orderid) if strategy: strategy.update_trade( - trade.trade_direction, - trade.trade_volume, - trade.trade_price + trade.direction, + trade.volume, + trade.price ) self.put_strategy_event(strategy.name) @@ -128,7 +135,7 @@ class PortfolioEngine(BaseEngine): self, name: str, vt_symbol: str, - size: int, + size: int = 0, net_pos: int = 0, open_price: float = 0, last_price: float = 0, @@ -139,6 +146,12 @@ class PortfolioEngine(BaseEngine): if name in self.strategies: return False + if not size: + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + return False + size = contract.size + strategy = PortfolioStrategy( name, vt_symbol, @@ -152,8 +165,12 @@ class PortfolioEngine(BaseEngine): self.strategies[strategy.name] = strategy self.symbol_strategy_map[strategy.vt_symbol].append(strategy) + self.save_setting() self.subscribe_data(vt_symbol) + self.put_strategy_event(name) + + return True def remove_strategy(self, name: str): """""" @@ -238,6 +255,10 @@ class PortfolioEngine(BaseEngine): event = Event(EVENT_PORTFOLIO_UPDATE, strategy) self.event_engine.put(event) + def stop(self): + """""" + self.save_setting() + class PortfolioStrategy: """""" diff --git a/vnpy/app/portfolio_manager/ui/widget.py b/vnpy/app/portfolio_manager/ui/widget.py index 43c6bf33..3abbea32 100644 --- a/vnpy/app/portfolio_manager/ui/widget.py +++ b/vnpy/app/portfolio_manager/ui/widget.py @@ -1,12 +1,10 @@ -from vnpy.event import EventEngine, Event +from vnpy.event import EventEngine from vnpy.trader.engine import MainEngine from vnpy.trader.constant import Direction, Offset -from vnpy.trader.ui import QtWidgets, QtCore, QtGui +from vnpy.trader.ui import QtWidgets, QtGui from vnpy.trader.ui.widget import ( BaseMonitor, BaseCell, - BidCell, AskCell, - TimeCell, PnlCell, - DirectionCell, EnumCell, + PnlCell, DirectionCell, EnumCell, ) from ..engine import ( @@ -43,10 +41,18 @@ class PortfolioManager(QtWidgets.QWidget): trade_monitor = PortfolioTradeMonitor( self.main_engine, self.event_engine) + trading_widget = StrategyTradingWidget(self.portfolio_engine) + management_widget = StrategyManagementWidget( + self.portfolio_engine, + trading_widget, + ) + vbox = QtWidgets.QVBoxLayout() + vbox.addWidget(management_widget) vbox.addWidget(self.create_group("策略", strategy_monitor)) vbox.addWidget(self.create_group("委托", order_monitor)) vbox.addWidget(self.create_group("成交", trade_monitor)) + vbox.addWidget(trading_widget) self.setLayout(vbox) @@ -151,3 +157,220 @@ class PortfolioOrderMonitor(BaseMonitor): order = cell.get_data() req = order.create_cancel_request() self.main_engine.cancel_order(req, order.gateway_name) + + +class StrategyTradingWidget(QtWidgets.QWidget): + """""" + + def __init__(self, portfolio_engine: PortfolioEngine): + """""" + super().__init__() + + self.portfolio_engine = portfolio_engine + self.init_ui() + self.update_combo() + + def init_ui(self): + """""" + self.name_combo = QtWidgets.QComboBox() + + self.direction_combo = QtWidgets.QComboBox() + self.direction_combo.addItems( + [Direction.LONG.value, Direction.SHORT.value]) + + self.offset_combo = QtWidgets.QComboBox() + self.offset_combo.addItems([offset.value for offset in Offset]) + + double_validator = QtGui.QDoubleValidator() + double_validator.setBottom(0) + + self.price_line = QtWidgets.QLineEdit() + self.price_line.setValidator(double_validator) + + self.volume_line = QtWidgets.QLineEdit() + self.volume_line.setValidator(double_validator) + + for w in [ + self.name_combo, + self.price_line, + self.volume_line, + self.direction_combo, + self.offset_combo + ]: + w.setFixedWidth(150) + + send_button = QtWidgets.QPushButton("委托") + send_button.clicked.connect(self.send_order) + send_button.setFixedWidth(70) + + cancel_button = QtWidgets.QPushButton("全撤") + cancel_button.clicked.connect(self.cancel_all) + cancel_button.setFixedWidth(70) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(QtWidgets.QLabel("策略名称")) + hbox.addWidget(self.name_combo) + hbox.addWidget(QtWidgets.QLabel("方向")) + hbox.addWidget(self.direction_combo) + hbox.addWidget(QtWidgets.QLabel("开平")) + hbox.addWidget(self.offset_combo) + hbox.addWidget(QtWidgets.QLabel("价格")) + hbox.addWidget(self.price_line) + hbox.addWidget(QtWidgets.QLabel("数量")) + hbox.addWidget(self.volume_line) + hbox.addWidget(send_button) + hbox.addWidget(cancel_button) + hbox.addStretch() + + self.setLayout(hbox) + + def send_order(self): + """""" + name = self.name_combo.currentText() + + price_text = self.price_line.text() + volume_text = self.volume_line.text() + + if not price_text or not volume_text: + return + + price = float(price_text) + volume = float(volume_text) + direction = Direction(self.direction_combo.currentText()) + offset = Offset(self.offset_combo.currentText()) + + self.portfolio_engine.send_order( + name, + price, + volume, + direction, + offset + ) + + def cancel_all(self): + """""" + name = self.name_line.text() + self.portfolio_engine.cancel_all(name) + + def update_combo(self): + """""" + strategy_names = list(self.portfolio_engine.strategies.keys()) + + self.name_combo.clear() + self.name_combo.addItems(strategy_names) + + +class StrategyManagementWidget(QtWidgets.QWidget): + """""" + + def __init__( + self, + portfolio_engine: PortfolioEngine, + trading_widget: StrategyTradingWidget + ): + """""" + super().__init__() + + self.portfolio_engine = portfolio_engine + self.trading_widget = trading_widget + + self.init_ui() + self.update_combo() + + def init_ui(self): + """""" + self.name_line = QtWidgets.QLineEdit() + self.symbol_line = QtWidgets.QLineEdit() + self.remove_combo = QtWidgets.QComboBox() + + for w in [ + self.name_line, + self.symbol_line, + self.remove_combo + ]: + w.setFixedWidth(150) + + add_button = QtWidgets.QPushButton("创建策略") + add_button.clicked.connect(self.add_strategy) + + remove_button = QtWidgets.QPushButton("移除策略") + remove_button.clicked.connect(self.remove_strategy) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(QtWidgets.QLabel("策略名称")) + hbox.addWidget(self.name_line) + hbox.addWidget(QtWidgets.QLabel("交易合约")) + hbox.addWidget(self.symbol_line) + hbox.addWidget(add_button) + hbox.addStretch() + hbox.addWidget(self.remove_combo) + hbox.addWidget(remove_button) + + self.setLayout(hbox) + + def add_strategy(self): + """""" + name = self.name_line.text() + vt_symbol = self.symbol_line.text() + + if not name or not vt_symbol: + QtWidgets.QMessageBox.information( + self, + "提示", + "请输入策略名称和交易合约", + QtWidgets.QMessageBox.Ok + ) + + result = self.portfolio_engine.add_strategy(name, vt_symbol) + + if result: + QtWidgets.QMessageBox.information( + self, + "提示", + "策略创建成功", + QtWidgets.QMessageBox.Ok + ) + + self.update_combo() + else: + QtWidgets.QMessageBox.warning( + self, + "提示", + "策略创建失败,存在重名或找不到合约", + QtWidgets.QMessageBox.Ok + ) + + def remove_strategy(self): + """""" + name = self.remove_combo.currentText() + + if not name: + return + + result = self.portfolio_engine.remove_strategy(name) + + if result: + QtWidgets.QMessageBox.information( + self, + "提示", + "策略移除成功", + QtWidgets.QMessageBox.Ok + ) + + self.update_combo() + else: + QtWidgets.QMessageBox.warning( + self, + "提示", + "策略移除失败,不存在该策略", + QtWidgets.QMessageBox.Ok + ) + + def update_combo(self): + """""" + strategy_names = list(self.portfolio_engine.strategies.keys()) + + self.remove_combo.clear() + self.remove_combo.addItems(strategy_names) + + self.trading_widget.update_combo() From e7052df3b017f3622adb35d0cedfaab56f8b10c7 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Wed, 13 Nov 2019 21:56:48 +0800 Subject: [PATCH 4/5] [Add] calculate realized pnl of strategy --- vnpy/app/portfolio_manager/engine.py | 64 +++++++++++++++++++++++-- vnpy/app/portfolio_manager/ui/widget.py | 21 ++++---- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/vnpy/app/portfolio_manager/engine.py b/vnpy/app/portfolio_manager/engine.py index 0d166b4c..f78f70de 100644 --- a/vnpy/app/portfolio_manager/engine.py +++ b/vnpy/app/portfolio_manager/engine.py @@ -5,7 +5,9 @@ from copy import copy from vnpy.event import Event, EventEngine from vnpy.trader.engine import BaseEngine, MainEngine -from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_TICK, EVENT_CONTRACT +from vnpy.trader.event import ( + EVENT_TRADE, EVENT_ORDER, EVENT_TICK, EVENT_CONTRACT, EVENT_TIMER +) from vnpy.trader.constant import Direction, Offset, OrderType from vnpy.trader.object import ( OrderRequest, CancelRequest, SubscribeRequest, @@ -29,6 +31,8 @@ class PortfolioEngine(BaseEngine): """""" super().__init__(main_engine, event_engine, APP_NAME) + self.inited = False + self.strategies: Dict[str, PortfolioStrategy] = {} self.symbol_strategy_map: Dict[str, List] = defaultdict(list) self.order_strategy_map: Dict[str, PortfolioStrategy] = {} @@ -36,6 +40,14 @@ class PortfolioEngine(BaseEngine): self.register_event() + def init_engine(self): + """""" + if self.inited: + return + self.inited = True + + self.load_setting() + def load_setting(self): """""" setting: dict = load_json(self.setting_filename) @@ -76,6 +88,12 @@ class PortfolioEngine(BaseEngine): self.event_engine.register(EVENT_TRADE, self.process_trade_event) self.event_engine.register(EVENT_TICK, self.process_tick_event) self.event_engine.register(EVENT_CONTRACT, self.process_contract_event) + self.event_engine.register(EVENT_TIMER, self.process_timer_event) + + def process_timer_event(self, event: Event): + """""" + if self.inited: + self.save_setting() def process_contract_event(self, event: Event): """""" @@ -121,6 +139,8 @@ class PortfolioEngine(BaseEngine): event = Event(EVENT_PORTFOLIO_TRADE, strategy_trade) self.event_engine.put(event) + self.save_setting() + def process_tick_event(self, event: Event): """""" tick: TickData = event.data @@ -245,7 +265,7 @@ class PortfolioEngine(BaseEngine): def cancel_all(self, name: str): """""" for vt_orderid in self.active_orders: - strategy = self.order_symbol_map[vt_orderid] + strategy = self.order_strategy_map[vt_orderid] if strategy.name == name: self.cancel_order(vt_orderid) @@ -308,10 +328,46 @@ class PortfolioStrategy: trade_price: float ): """""" + old_cost = self.net_pos * self.open_price + if trade_direction == Direction.LONG: - self.net_pos += trade_volume + new_pos = self.net_pos + trade_volume + + # Open new long position + if self.net_pos >= 0: + new_cost = old_cost + trade_volume * trade_price + self.open_price = new_cost / new_pos + # Close short position + else: + close_volume = min(trade_volume, abs(self.net_pos)) + realized_pnl = (trade_price - self.open_price) * \ + close_volume * (-1) + self.realized_pnl += realized_pnl + + if new_pos > 0: + self.open_price = trade_price + + # Update net pos + self.net_pos = new_pos + else: - self.net_pos -= trade_volume + new_pos = self.net_pos - trade_volume + + # Open new short position + if self.net_pos <= 0: + new_cost = old_cost - trade_volume * trade_price + self.open_price = new_cost / new_pos + # Close long position + else: + close_volume = min(trade_volume, abs(self.net_pos)) + realized_pnl = (trade_price - self.open_price) * close_volume + self.realized_pnl += realized_pnl + + if new_pos < 0: + self.open_price = trade_price + + # Update net pos + self.net_pos = new_pos self.calculate_pnl() diff --git a/vnpy/app/portfolio_manager/ui/widget.py b/vnpy/app/portfolio_manager/ui/widget.py index 3abbea32..0126247c 100644 --- a/vnpy/app/portfolio_manager/ui/widget.py +++ b/vnpy/app/portfolio_manager/ui/widget.py @@ -41,24 +41,26 @@ class PortfolioManager(QtWidgets.QWidget): trade_monitor = PortfolioTradeMonitor( self.main_engine, self.event_engine) - trading_widget = StrategyTradingWidget(self.portfolio_engine) - management_widget = StrategyManagementWidget( + self.trading_widget = StrategyTradingWidget(self.portfolio_engine) + self.management_widget = StrategyManagementWidget( self.portfolio_engine, - trading_widget, + self.trading_widget, ) vbox = QtWidgets.QVBoxLayout() - vbox.addWidget(management_widget) + vbox.addWidget(self.management_widget) vbox.addWidget(self.create_group("策略", strategy_monitor)) + vbox.addWidget(self.trading_widget) vbox.addWidget(self.create_group("委托", order_monitor)) vbox.addWidget(self.create_group("成交", trade_monitor)) - vbox.addWidget(trading_widget) self.setLayout(vbox) def show(self): """""" - self.portfolio_engine.load_setting() + self.portfolio_engine.init_engine() + self.management_widget.update_combo() + self.showMaximized() def create_group(self, title: str, widget: QtWidgets.QWidget): @@ -91,6 +93,7 @@ class PortfolioStrategyMonitor(BaseMonitor): "open_price": {"display": "持仓价格", "cell": BaseCell, "update": True}, "last_price": {"display": "最新价格", "cell": BaseCell, "update": True}, "pos_pnl": {"display": "持仓盈亏", "cell": PnlCell, "update": True}, + "realized_pnl": {"display": "平仓盈亏", "cell": PnlCell, "update": True}, "create_time": {"display": "创建时间", "cell": BaseCell, "update": False}, } @@ -104,7 +107,7 @@ class PortfolioTradeMonitor(BaseMonitor): data_key = "" headers = { - "gateway_name": {"display": "接口名称", "cell": BaseCell, "update": False}, + "gateway_name": {"display": "策略名称", "cell": BaseCell, "update": False}, "tradeid": {"display": "成交号 ", "cell": BaseCell, "update": False}, "orderid": {"display": "委托号", "cell": BaseCell, "update": False}, "symbol": {"display": "代码", "cell": BaseCell, "update": False}, @@ -127,7 +130,7 @@ class PortfolioOrderMonitor(BaseMonitor): sorting = True headers = { - "gateway_name": {"display": "接口名称", "cell": BaseCell, "update": False}, + "gateway_name": {"display": "策略名称", "cell": BaseCell, "update": False}, "orderid": {"display": "委托号", "cell": BaseCell, "update": False}, "symbol": {"display": "代码", "cell": BaseCell, "update": False}, "exchange": {"display": "交易所", "cell": EnumCell, "update": False}, @@ -249,7 +252,7 @@ class StrategyTradingWidget(QtWidgets.QWidget): def cancel_all(self): """""" - name = self.name_line.text() + name = self.name_combo.currentText() self.portfolio_engine.cancel_all(name) def update_combo(self): From e44943f3cc6a1b36ee38be4c173b813c80915adb Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Wed, 13 Nov 2019 23:49:11 +0800 Subject: [PATCH 5/5] [Add] remove strategy rows from monitor --- vnpy/app/portfolio_manager/ui/widget.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/vnpy/app/portfolio_manager/ui/widget.py b/vnpy/app/portfolio_manager/ui/widget.py index 0126247c..77b70ae4 100644 --- a/vnpy/app/portfolio_manager/ui/widget.py +++ b/vnpy/app/portfolio_manager/ui/widget.py @@ -45,6 +45,7 @@ class PortfolioManager(QtWidgets.QWidget): self.management_widget = StrategyManagementWidget( self.portfolio_engine, self.trading_widget, + strategy_monitor ) vbox = QtWidgets.QVBoxLayout() @@ -97,6 +98,15 @@ class PortfolioStrategyMonitor(BaseMonitor): "create_time": {"display": "创建时间", "cell": BaseCell, "update": False}, } + def remove_strategy(self, name: str): + """""" + if name not in self.cells: + return + + row_cells = self.cells.pop(name) + row = self.row(row_cells["net_pos"]) + self.removeRow(row) + class PortfolioTradeMonitor(BaseMonitor): """ @@ -269,13 +279,15 @@ class StrategyManagementWidget(QtWidgets.QWidget): def __init__( self, portfolio_engine: PortfolioEngine, - trading_widget: StrategyTradingWidget + trading_widget: StrategyTradingWidget, + strategy_monitor: PortfolioStrategyMonitor ): """""" super().__init__() self.portfolio_engine = portfolio_engine self.trading_widget = trading_widget + self.strategy_monitor = strategy_monitor self.init_ui() self.update_combo() @@ -361,6 +373,8 @@ class StrategyManagementWidget(QtWidgets.QWidget): ) self.update_combo() + + self.strategy_monitor.remove_strategy(name) else: QtWidgets.QMessageBox.warning( self,