Skip to content

Commit c4e006f

Browse files
committed
Enhance order target portfolio functionality and add integration tests
- Introduced `last_prices` parameter in `OrderTargetPortfolio` to allow for more accurate order calculations based on recent price data. - Updated `order_target_portfolio_smart` function to handle both algorithmic orders and price dictionaries, improving flexibility in order submissions. - Added comprehensive integration tests for `order_target_portfolio_smart`, ensuring correct behavior under various scenarios and validating position adjustments. - Refactored existing logic to improve clarity and maintainability, including adjustments to safety calculations and error handling for target weights.
1 parent e40cf0c commit c4e006f

4 files changed

Lines changed: 166 additions & 35 deletions

File tree

rqalpha/apis/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@
1515
from rqalpha.apis.api_abstract import *
1616
from rqalpha.apis.api_base import *
1717
from rqalpha.apis.api_rqdatac import *
18+
19+
from rqalpha.mod.rqalpha_mod_sys_accounts.api.api_stock import *
20+
from rqalpha.mod.rqalpha_mod_sys_accounts.api.api_stock import *

rqalpha/mod/rqalpha_mod_sys_accounts/api/order_target_portfolio.py

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
from typing import Mapping, NamedTuple, cast, Optional, Union
2+
from collections import defaultdict
23
from operator import itemgetter
34

45
from pandas import Series, Index, DataFrame
56
from numpy import sign, round as np_round, inf
67

78
from rqalpha.api import export_as_api
89
from rqalpha.apis.api_base import assure_instrument
9-
from rqalpha.model.order import AlgoOrder, MarketOrder
10+
from rqalpha.model.order import AlgoOrder, MarketOrder, LimitOrder, OrderStyle
1011
from rqalpha.model.order import Order
1112
from rqalpha.environment import Environment
1213
from rqalpha.const import EXECUTION_PHASE, POSITION_DIRECTION, INSTRUMENT_TYPE, MARKET, DEFAULT_ACCOUNT_TYPE, SIDE, POSITION_EFFECT
1314
from rqalpha.core.execution_context import ExecutionContext
14-
from rqalpha.utils.exception import RQApiNotSupportedError
15+
from rqalpha.utils.exception import RQApiNotSupportedError, RQInvalidArgument
1516
from rqalpha.utils.functools import lru_cache
1617
from rqalpha.utils.i18n import gettext as _
1718

@@ -34,12 +35,15 @@ def __init__(
3435
target_weights: Series,
3536
current_quantities: Series,
3637
current_closable: Series,
38+
last_prices: Series | None,
3739
env: Environment,
3840
):
39-
if target_weights[target_weights < 0].any():
40-
raise ValueError("target_weights contains negative value: {}".format(target_weights[target_weights < 0]))
41-
4241
index = Index(target_weights.index.union(current_quantities.index).union(current_closable.index))
42+
if last_prices is not None:
43+
last_prices = last_prices.reindex(index)
44+
if last_prices.isna().any():
45+
raise RQInvalidArgument(_("prices of {} is not provided").format(last_prices.isna().index[last_prices.isna()]))
46+
4347
self._target_weights = target_weights.reindex(index, fill_value=0)
4448
self._current_quantities = current_quantities.reindex(index, fill_value=0)
4549
self._current_closable = current_closable.reindex(index, fill_value=0)
@@ -74,17 +78,22 @@ def __init__(
7478
self._prices = DataFrame(index=index, columns=["last", "limit_up", "limit_down"], dtype=float) # type: ignore
7579
if phase == EXECUTION_PHASE.OPEN_AUCTION:
7680
# 集合竞价阶段,最近的价格是昨收
77-
prev_date = env.data_proxy.get_previous_trading_date(env.trading_dt)
78-
for order_book_id in index:
79-
bars = env.data_proxy.history_bars(order_book_id, 1, "1d", ["close"], prev_date)
80-
self._prices.loc[order_book_id, "last"] = bars["close"][0]
81+
if last_prices is not None:
82+
self._prices["last"] = last_prices
83+
else:
84+
prev_date = env.data_proxy.get_previous_trading_date(env.trading_dt)
85+
for order_book_id in index:
86+
bars = env.data_proxy.history_bars(order_book_id, 1, "1d", ["close"], prev_date)
87+
self._prices.loc[order_book_id, "last"] = bars["close"][0]
8188
elif phase == EXECUTION_PHASE.ON_BAR:
8289
if env.config.base.frequency == "1d":
8390
# TODO:根据算法时间选择最近的分钟线作为估值
8491
# 当前先选择开盘价
8592
for order_book_id in index:
8693
bars = env.data_proxy.history_bars(order_book_id, 1, "1d", ["open", "limit_up", "limit_down"], env.trading_dt)
87-
self._prices.loc[order_book_id] = bars[0]
94+
self._prices.loc[order_book_id] = list(bars[0])
95+
if last_prices is not None:
96+
self._prices["last"] = last_prices
8897
elif env.config.base.frequency == "1m":
8998
raise NotImplementedError
9099
else:
@@ -148,8 +157,12 @@ def __call__(
148157

149158
if self._current_quantities.empty and self._target_weights.empty:
150159
return Series()
151-
152-
safety = self.SAFETY
160+
161+
if self._target_weights.sum() > 0.95:
162+
# 如果目标是满仓或者接近满仓,则使用一个较高的 safety 开始下降
163+
safety = self.SAFETY
164+
else:
165+
safety = 1.
153166
last_proportion_diff = inf
154167
last_diff = None
155168
prices = self._prices_settle_ccy
@@ -175,7 +188,7 @@ def __call__(
175188
proportion_diff = abs(total_proportion - self._target_weights.sum())
176189
if cash_consumed < cash_available:
177190
# TODO: 分别计算 A H 股的可用资金
178-
if proportion_diff >= last_proportion_diff and last_diff is not None:
191+
if proportion_diff > last_proportion_diff and last_diff is not None:
179192
return last_diff
180193
last_diff = diff
181194
last_proportion_diff = proportion_diff
@@ -187,50 +200,67 @@ def __call__(
187200
EXECUTION_PHASE.OPEN_AUCTION,
188201
EXECUTION_PHASE.ON_BAR,
189202
)
190-
def order_target_portfolio_smart(target_portfolio: Union[Mapping[str, float], Series], algo: Optional[AlgoOrder] = None):
203+
def order_target_portfolio_smart(
204+
target_portfolio: Union[Mapping[str, float], Series],
205+
algo_or_prices: Union[AlgoOrder, dict[str, float], None] = None
206+
):
191207
from rqalpha.mod.rqalpha_mod_sys_accounts.api.api_stock import _order_value
192208

193209
env = Environment.get_instance()
194-
target_weights = {
210+
target_weights = Series({
195211
assure_instrument(id_or_ins).order_book_id: percent for id_or_ins, percent in target_portfolio.items()
196-
}
212+
})
197213
account = env.portfolio.accounts[DEFAULT_ACCOUNT_TYPE.STOCK]
198214
quantities, closable = {}, {}
199215
for position in account.get_positions():
200216
quantities[position.order_book_id] = position.quantity
201217
closable[position.order_book_id] = position.closable
218+
if isinstance(algo_or_prices, dict):
219+
style_map: dict[str, OrderStyle] = {
220+
order_book_id: LimitOrder(price) for order_book_id, price in algo_or_prices.items()
221+
}
222+
def _get_style(order_book_id) -> OrderStyle:
223+
try:
224+
return style_map[order_book_id]
225+
except KeyError:
226+
raise RQInvalidArgument(_("price of {} is needed, which is not provided in the algo_or_prices").format(order_book_id))
227+
prices = Series(algo_or_prices)
228+
else:
229+
style = algo_or_prices or MarketOrder()
230+
_get_style = lambda order_book_id: style
231+
prices = None
232+
233+
if target_weights[target_weights < 0].any():
234+
raise ValueError("target_weights contains negative value: {}".format(target_weights[target_weights < 0]))
235+
current_quantities = Series(quantities)
236+
current_quantities = cast(Series, current_quantities[current_quantities != 0])
237+
current_closable = Series(closable).reindex(current_quantities.index, fill_value=0)
238+
202239
adjusting = OrderTargetPortfolio(
203240
target_weights=Series(target_weights),
204-
current_quantities=Series(quantities),
205-
current_closable=Series(closable),
241+
current_quantities=current_quantities,
242+
current_closable=current_closable,
243+
last_prices=prices,
206244
env=env
207245
)(
208246
target_value = env.portfolio.total_value,
209247
cash_available = account.cash,
210248
)
211249

212250
orders = []
213-
style = algo or MarketOrder()
251+
214252
# 先平
215253
for order_book_id, delta_quantity in cast(Series, (adjusting[adjusting < 0])).items():
216254
order = env.submit_order(Order.__from_create__(
217-
order_book_id,
218-
abs(delta_quantity),
219-
SIDE.SELL,
220-
style,
221-
POSITION_EFFECT.CLOSE
255+
order_book_id, abs(delta_quantity), SIDE.SELL, _get_style(order_book_id), POSITION_EFFECT.CLOSE
222256
))
223257
if order is not None:
224258
orders.append(order)
225259
# 后开
226260
for order_book_id, delta_quantity in cast(Series, (adjusting[adjusting > 0])).items():
227261
order_book_id = cast(str, order_book_id)
228262
order_to_be_submitted = Order.__from_create__(
229-
order_book_id,
230-
delta_quantity,
231-
SIDE.BUY,
232-
style,
233-
POSITION_EFFECT.OPEN
263+
order_book_id, delta_quantity, SIDE.BUY, _get_style(order_book_id), POSITION_EFFECT.OPEN
234264
)
235265
order = env.submit_order(order_to_be_submitted)
236266
if order is None:
@@ -241,7 +271,7 @@ def order_target_portfolio_smart(target_portfolio: Union[Mapping[str, float], Se
241271
account.get_position(order_book_id),
242272
env.data_proxy.instrument_not_none(order_book_id),
243273
account.cash,
244-
style,
274+
_get_style(order_book_id),
245275
zero_amount_as_exception=False
246276
)
247277
if order is not None:
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# -*- coding: utf-8 -*-
2+
# 版权所有 2019 深圳米筐科技有限公司(下称“米筐科技”)
3+
#
4+
# 除非遵守当前许可,否则不得使用本软件。
5+
#
6+
# * 非商业用途(非商业用途指个人出于非商业目的使用本软件,或者高校、研究所等非营利机构出于教育、科研等目的使用本软件):
7+
# 遵守 Apache License 2.0(下称“Apache 2.0 许可”),
8+
# 您可以在以下位置获得 Apache 2.0 许可的副本:http://www.apache.org/licenses/LICENSE-2.0。
9+
# 除非法律有要求或以书面形式达成协议,否则本软件分发时需保持当前许可“原样”不变,且不得附加任何条件。
10+
#
11+
# * 商业用途(商业用途指个人出于任何商业目的使用本软件,或者法人或其他组织出于任何目的使用本软件):
12+
# 未经米筐科技授权,任何个人不得出于任何商业目的使用本软件(包括但不限于向第三方提供、销售、出租、出借、转让本软件、
13+
# 本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务),任何法人或其他组织不得出于任何目的使用本软件,
14+
# 否则米筐科技有权追究相应的知识产权侵权责任。
15+
# 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。
16+
# 详细的授权流程,请联系 public@ricequant.com 获取。
17+
18+
19+
from rqalpha import run_func
20+
from rqalpha.apis import get_position, is_st_stock, order_target_portfolio_smart
21+
22+
23+
def test_order_target_portfolio():
24+
config = {
25+
"base": {
26+
"start_date": "2019-07-30",
27+
"end_date": "2019-08-05",
28+
"accounts": {
29+
"stock": 1000000
30+
},
31+
},
32+
"extra": {
33+
"log_level": "error",
34+
},
35+
}
36+
37+
def init(context):
38+
context.counter = 0
39+
40+
def handle_bar(context, bar_dict):
41+
context.counter += 1
42+
if context.counter == 1:
43+
order_target_portfolio_smart({
44+
"000001.XSHE": 0.1,
45+
"000004.XSHE": 0.2,
46+
})
47+
# 开盘价计算目标仓位
48+
assert get_position("000001.XSHE").quantity == 7000 # (1000000 * 0.1) / 14.31 = 6988.12
49+
assert get_position("000004.XSHE").quantity == 10800 # (1000000 * 0.2) / 18.5 = 10810.81
50+
elif context.counter == 2:
51+
order_target_portfolio_smart({
52+
"000004.XSHE": 0.1,
53+
"000005.XSHE": 0.2,
54+
"600519.XSHG": 0.6,
55+
}, {
56+
"000001.XSHE": 14,
57+
"000004.XSHE": 18,
58+
"000005.XSHE": 2.92,
59+
"600519.XSHG": 970,
60+
})
61+
assert get_position("000001.XSHE").quantity == 0 # 清仓
62+
assert get_position("000004.XSHE").quantity == 5500 # (993695.7496 * 0.1) / 18 = 5520.53
63+
assert get_position("000005.XSHE").quantity == 68100 # (993695.7496 * 0.2) / 2.92 = 68061.35
64+
assert get_position("600519.XSHG").quantity == 0 # 970 低于 收盘价 无法买进
65+
66+
return run_func(config=config, init=init, handle_bar=handle_bar)
67+
68+
69+
def test_order_target_portfolio_in_signal_mode():
70+
config = {
71+
"base": {
72+
"start_date": "2019-07-30",
73+
"end_date": "2019-08-05",
74+
"accounts": {
75+
"stock": 1000000
76+
}
77+
},
78+
"mod": {
79+
"sys_simulation": {
80+
"signal": True
81+
}
82+
}
83+
}
84+
85+
def init(context):
86+
context.counter = 0
87+
88+
def handle_bar(context, handle_bar):
89+
context.counter += 1
90+
if context.counter == 1:
91+
order_target_portfolio_smart({
92+
"000001.XSHE": 0.1,
93+
"000004.XSHE": 0.2,
94+
}, {
95+
"000001.XSHE": 14,
96+
"000004.XSHE": 10,
97+
})
98+
assert get_position("000001.XSHE").quantity == 7100 # (1000000 * 0.1) / 14= 7142.86
99+
assert get_position("000001.XSHE").avg_price == 14
100+
assert get_position("000004.XSHE").quantity == 0 # 价格低过跌停价,被拒单
101+
102+
return run_func(config=config, init=init, handle_bar=handle_bar)
103+

tests/integration_tests/test_backtest_results/test_order_target_portfolio_smart.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@ def open_auction(context, bar_dict):
5252
},
5353
"extra": {
5454
"log_level": "error",
55-
},
56-
"mod": {
57-
"sys_analyser": {
58-
"plot": True
59-
}
6055
}
6156
}
6257

0 commit comments

Comments
 (0)