Skip to content

Commit 6de68c9

Browse files
authored
release/5.5.4 (#923)
* Add position queue management and related tests Integrate a new PositionQueue class to handle trade tracking, splits, and delisting. Update existing code for proper queue handling in position models. Add comprehensive tests for stock, futures, splits, delisting, and short-selling scenarios to ensure accurate position queue functionality. * Fix position queue rounding issue during stock splits Ensure position queue quantity aligns with expected values after splits by addressing rounding discrepancies. Updated stock split handling logic and added corresponding test cases to verify the fix. * Rqsdk 789 (#922) * fix get_secutities_margin * update test workflow ubuntu version * 修改测试数据 --------- Co-authored-by: Cuizi7 <[email protected]> * update version --------- Co-authored-by: 崔子琦 <[email protected]> Co-authored-by: Cuizi7 <[email protected]>
2 parents cde5274 + 12defd7 commit 6de68c9

File tree

9 files changed

+555
-16
lines changed

9 files changed

+555
-16
lines changed

rqalpha/apis/api_rqdatac.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,8 @@ def get_securities_margin(
429429
order_book_ids, # type: Union[str, Iterable[str]]
430430
count=1, # type: Optional[int]
431431
fields=None, # type: Optional[str]
432-
expect_df=False # type: Optional[bool]
433-
): # type: (...) -> Union[pd.Series, pd.DataFrame, pd.Panel]
432+
expect_df=True # type: Optional[bool]
433+
): # type: (...) -> Union[pd.Series, pd.DataFrame]
434434
"""
435435
获取融资融券信息。包括 `深证融资融券数据 <http://www.szse.cn/main/disclosure/rzrqxx/rzrqjy/>`_ 以及 `上证融资融券数据 <http://www.sse.com.cn/market/othersdata/margin/detail/>`_ 情况。既包括个股数据,也包括市场整体数据。需要注意,融资融券的开始日期为2010年3月31日。
436436
@@ -524,11 +524,15 @@ def get_securities_margin(
524524
start_dt = dt
525525
else:
526526
start_dt = data_proxy.get_previous_trading_date(dt, count - 1)
527-
527+
528+
overall_code = {"XSHG", "XSHE", "sh", "sz"}
528529
if isinstance(order_book_ids, six.string_types):
529-
order_book_ids = assure_order_book_id(order_book_ids)
530+
if order_book_ids not in overall_code:
531+
order_book_ids = assure_order_book_id(order_book_ids)
530532
else:
531-
order_book_ids = [assure_order_book_id(i) for i in order_book_ids]
533+
overall_code_list = [i for i in order_book_ids if i in overall_code]
534+
order_book_ids = [assure_order_book_id(i) for i in order_book_ids if i not in overall_code]
535+
order_book_ids = order_book_ids + overall_code_list
532536

533537
return rqdatac.get_securities_margin(order_book_ids, start_dt, dt, fields=fields, expect_df=expect_df)
534538

rqalpha/environment.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。
1616
# 详细的授权流程,请联系 [email protected] 获取。
1717

18-
from datetime import datetime
18+
from datetime import date, datetime
1919
from typing import Optional, Dict, List
2020
from itertools import chain
2121
from typing import TYPE_CHECKING
@@ -53,8 +53,8 @@ def __init__(self, config, rqdatac_init):
5353
self.user_system_log = user_system_log
5454
self.event_bus = EventBus()
5555
self.portfolio = None # type: Optional[rqalpha.portfolio.Portfolio]
56-
self.calendar_dt = None # type: Optional[datetime]
57-
self.trading_dt = None # type: Optional[datetime]
56+
self.calendar_dt: datetime = datetime.combine(config.base.start_date, datetime.min.time())
57+
self.trading_dt: datetime = datetime.combine(config.base.start_date, datetime.min.time())
5858
self.mod_dict = None
5959
self.user_strategy = None
6060
self._frontend_validators = {} # type: Dict[str, List]

rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# 否则米筐科技有权追究相应的知识产权侵权责任。
1515
# 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。
1616
# 详细的授权流程,请联系 [email protected] 获取。
17-
17+
from collections import deque
1818
from datetime import date
1919

2020
from decimal import Decimal
@@ -150,6 +150,7 @@ def settlement(self, trading_date):
150150
if self.cash_return_by_stock_delisted:
151151
delta_cash = self.market_value
152152
self._quantity = self._old_quantity = 0
153+
self._queue.clear()
153154
return delta_cash
154155

155156
@cached_property
@@ -212,7 +213,7 @@ def _handle_split(self, trading_date, data_proxy):
212213
# int(6000 * 1.15) -> 6899
213214
self._old_quantity = self._quantity = round(Decimal(self._quantity) * ratio)
214215
self._logical_old_quantity = round(Decimal(self._logical_old_quantity) * ratio)
215-
216+
self._queue.handle_split(ratio, self._quantity)
216217

217218
class FuturePosition(Position):
218219
__repr_properties__ = (
@@ -282,6 +283,7 @@ def apply_trade(self, trade):
282283
self._transaction_cost += trade.transaction_cost
283284
self._quantity -= trade.last_quantity
284285
self._trade_cost -= trade.last_price * trade.last_quantity
286+
self._queue.handle_trade(-trade.last_quantity, self._env.trading_dt.date(), close_today=True)
285287
else:
286288
super(FuturePosition, self).apply_trade(trade)
287289

@@ -325,6 +327,7 @@ def settlement(self, trading_date):
325327
)
326328
self._env.event_bus.publish_event(Event(EVENT.TRADE, account=account, trade=trade, order=None))
327329
self._quantity = self._old_quantity = 0
330+
self._queue.clear()
328331
return delta_cash
329332

330333
def post_settlement(self):

rqalpha/portfolio/position.py

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
# 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。
1616
# 详细的授权流程,请联系 [email protected] 获取。
1717

18-
from collections import UserDict
18+
from collections import UserDict, deque
1919
from datetime import date
20-
from typing import Dict, Iterable, Tuple, Optional
20+
from decimal import Decimal
21+
from typing import Dict, Iterable, Tuple, Optional, Deque, List
2122

2223
from rqalpha.const import POSITION_DIRECTION, POSITION_EFFECT
2324
from rqalpha.environment import Environment
@@ -91,6 +92,10 @@ def __init__(self, order_book_id, direction, init_quantity=0, init_price=None):
9192

9293
self._direction_factor = 1 if direction == POSITION_DIRECTION.LONG else -1
9394

95+
self._queue = PositionQueue()
96+
if init_quantity:
97+
self._queue.handle_trade(init_quantity, self._env.trading_dt.date())
98+
9499
@property
95100
def order_book_id(self):
96101
# type: () -> str
@@ -193,7 +198,8 @@ def get_state(self):
193198
"avg_price": self._avg_price,
194199
"trade_cost": self._trade_cost,
195200
"transaction_cost": self._transaction_cost,
196-
"prev_close": self._prev_close
201+
"prev_close": self._prev_close,
202+
"position_queue": list(self._queue.queue)
197203
}
198204

199205
def set_state(self, state):
@@ -209,6 +215,7 @@ def set_state(self, state):
209215
self._trade_cost = state.get("trade_cost", 0)
210216
self._transaction_cost = state.get("transaction_cost", 0)
211217
self._prev_close = state.get("prev_close")
218+
self._queue.set_sate(state.get("position_queue", []))
212219

213220
def before_trading(self, trading_date):
214221
# type: (date) -> float
@@ -225,6 +232,7 @@ def apply_trade(self, trade):
225232
# 返回总资金的变化量
226233
self._transaction_cost += trade.transaction_cost
227234
if trade.position_effect == POSITION_EFFECT.OPEN:
235+
self._queue.handle_trade(trade.last_quantity, self._env.trading_dt.date())
228236
if self._quantity < 0:
229237
self._avg_price = trade.last_price if self._quantity + trade.last_quantity > 0 else 0
230238
else:
@@ -235,6 +243,7 @@ def apply_trade(self, trade):
235243
return (-1 * trade.last_price * trade.last_quantity) - trade.transaction_cost
236244
elif trade.position_effect == POSITION_EFFECT.CLOSE:
237245
# 先平昨,后平今
246+
self._queue.handle_trade(-trade.last_quantity, self._env.trading_dt.date())
238247
self._old_quantity -= min(trade.last_quantity, self._old_quantity)
239248
self._quantity -= trade.last_quantity
240249
self._trade_cost -= trade.last_price * trade.last_quantity
@@ -255,6 +264,15 @@ def update_last_price(self, price):
255264
def calc_close_today_amount(self, trade_amount, position_effect):
256265
return 0
257266

267+
@property
268+
def position_queue(self):
269+
# type: () -> deque[tuple[date, int]]
270+
"""
271+
获取持仓队列,返回每笔持仓的开仓时间和数量
272+
格式为 [(开仓日期, 持仓数量), ...]
273+
"""
274+
return self._queue.queue
275+
258276
@property
259277
def _open_orders(self):
260278
# type: () -> Iterable[Order]
@@ -263,6 +281,69 @@ def _open_orders(self):
263281
yield order
264282

265283

284+
class PositionQueue:
285+
def __init__(self):
286+
self._queue: Deque[Tuple[date, int]] = deque()
287+
288+
def set_sate(self, state: List[Tuple[date, int]]):
289+
self._queue = deque(state)
290+
291+
def handle_trade(self, delta_quantity, trading_date: date, close_today: bool = False):
292+
if delta_quantity == 0:
293+
return
294+
if self._queue and self._queue[0][1] * delta_quantity < 0:
295+
# 从正平仓到负或者从负平仓到正
296+
if close_today:
297+
# 强制先平今
298+
d, qty_in_queue = self._queue[-1]
299+
if d >= trading_date:
300+
if abs(qty_in_queue) <= abs(delta_quantity):
301+
delta_quantity += qty_in_queue
302+
self._queue.pop()
303+
else:
304+
self._queue[-1] = (d, qty_in_queue + delta_quantity)
305+
delta_quantity = 0
306+
while delta_quantity and self._queue:
307+
d, qty_in_queue = self._queue[0]
308+
if abs(qty_in_queue) <= abs(delta_quantity):
309+
# 当前持仓批次全部平掉
310+
delta_quantity += qty_in_queue
311+
self._queue.popleft()
312+
else:
313+
# 当前持仓批次部分平掉
314+
self._queue[0] = (d, qty_in_queue + delta_quantity)
315+
delta_quantity = 0
316+
if delta_quantity != 0:
317+
# queue 消耗光了,delta_quantity 还没消耗完
318+
self._queue.append((trading_date, delta_quantity))
319+
else:
320+
# 开仓
321+
if self._queue and self._queue[-1][0] == trading_date:
322+
# 当日已有开仓,更新数量
323+
_, quantity = self._queue.pop()
324+
self._queue.append((trading_date, quantity + delta_quantity))
325+
else:
326+
# 当日首次开仓
327+
self._queue.append((trading_date, delta_quantity))
328+
329+
def handle_split(self, ratio: Decimal, expected_quantity):
330+
self._queue = deque(
331+
(d, round(Decimal(q) * ratio)) for d, q in self._queue
332+
)
333+
diff = expected_quantity - sum(q for _, q in self._queue)
334+
if diff != 0:
335+
# 有可能因为 round 的原因导致差1股
336+
self._queue[-1] = (self._queue[-1][0], self._queue[-1][1] + diff)
337+
338+
339+
def clear(self):
340+
self._queue.clear()
341+
342+
@property
343+
def queue(self):
344+
return self._queue.copy()
345+
346+
266347
class PositionProxy(metaclass=PositionProxyMeta):
267348
__repr_properties__ = (
268349
"order_book_id", "positions"

rqalpha/utils/testing/fixtures.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import pickle
33
from contextlib import contextmanager
4+
from datetime import date
45

56
import six
67

@@ -18,7 +19,11 @@ def init_fixture(self):
1819
class EnvironmentFixture(RQAlphaFixture):
1920
def __init__(self, *args, **kwargs):
2021
super(EnvironmentFixture, self).__init__(*args, **kwargs)
21-
self.env_config = {}
22+
self.env_config = {
23+
"base":{
24+
"start_date": date(2016, 1, 1)
25+
}
26+
}
2227
self.env = None
2328

2429
def init_fixture(self):
@@ -72,7 +77,8 @@ def __init__(self, *args, **kwargs):
7277

7378
self.env_config = {
7479
"base": {
75-
"accounts": {"STOCK": 100}
80+
"accounts": {"STOCK": 100},
81+
"start_date": date(2016, 1, 1),
7682
}
7783
}
7884

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
[metadata]
77
name = rqalpha
8-
version = 5.5.3
8+
version = 5.5.4
99

1010
[versioneer]
1111
VCS = git

0 commit comments

Comments
 (0)