Skip to content

Commit 8e4b5d3

Browse files
committed
Refactor data handling and enhance functionality for ex-factor processing
- Updated `GenerateSplitBundle` to correct file naming and improve data writing logic. - Introduced `GenerateExFactorBundle` class for handling ex-factor data, including methods for data retrieval and writing to HDF5. - Enhanced `DataProxy` to support retrieval of ex-cum factors with appropriate error handling. - Updated `BaseDataSource` to register ex-factor stores and adjust data retrieval methods accordingly. - Improved `StockPosition` to incorporate unadjusted previous close and calculate position PnL accurately based on daily dividends and splits. - Refactored `Position` class to enhance trading PnL calculations and streamline cost updates during trade applications.
1 parent 0804858 commit 8e4b5d3

5 files changed

Lines changed: 110 additions & 44 deletions

File tree

rqalpha/data/base_data_source/data_source.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ def _p(name):
8282
# static registered storages
8383
self._future_info_store = FutureInfoStore(_p("future_info.json"), custom_future_info)
8484
self._yield_curve = YieldCurveStore(_p('yield_curve.h5'))
85-
self._ex_cum_factor = SimpleFactorStore(_p('ex_cum_factor.h5'))
8685
self._share_transformation = ShareTransformationStore(_p('share_transformation.json'))
8786
self._suspend_days = [DateSet(_p('suspended_days.h5'))] # type: List[AbstractDateSet]
8887
self._st_stock_days = DateSet(_p('st_stock_days.h5'))
@@ -94,6 +93,7 @@ def _p(name):
9493
self._dividend_stores: Dict[tuple[INSTRUMENT_TYPE, MARKET], AbstractDividendStore] = {}
9594
self._split_stores: Dict[tuple[INSTRUMENT_TYPE, MARKET], AbstractSimpleFactorStore] = {}
9695
self._calendar_stores: Dict[TRADING_CALENDAR_TYPE, AbstractCalendarStore] = {}
96+
self._ex_factor_stores: Dict[tuple[INSTRUMENT_TYPE, MARKET], AbstractSimpleFactorStore] = {}
9797

9898
# instruments
9999
self._id_instrument_map: Dict[str, Instrument] = {}
@@ -116,9 +116,11 @@ def _p(name):
116116
# register dividends and split factors stores
117117
dividend_store = DividendStore(_p('dividends.h5'))
118118
split_store = SimpleFactorStore(_p('split_factor.h5'))
119+
ex_factor_store = SimpleFactorStore(_p('ex_cum_factor.h5'))
119120
for ins_type in [INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.ETF, INSTRUMENT_TYPE.LOF]:
120121
self.register_dividend_store(ins_type, dividend_store)
121122
self.register_split_store(ins_type, split_store)
123+
self.register_ex_factor_store(ins_type, ex_factor_store)
122124

123125
# register calendar stores
124126
self.register_calendar_store(TRADING_CALENDAR_TYPE.CN_STOCK, ExchangeTradingCalendarStore(_p("trading_dates.npy")))
@@ -141,6 +143,9 @@ def register_split_store(self, instrument_type: INSTRUMENT_TYPE, split_store: Ab
141143
def register_calendar_store(self, calendar_type: TRADING_CALENDAR_TYPE, calendar_store: AbstractCalendarStore):
142144
self._calendar_stores[calendar_type] = calendar_store
143145

146+
def register_ex_factor_store(self, instrument_type: INSTRUMENT_TYPE, ex_factor_store: AbstractSimpleFactorStore, market: MARKET = MARKET.CN):
147+
self._ex_factor_stores[instrument_type, market] = ex_factor_store
148+
144149
def append_suspend_date_set(self, date_set):
145150
# type: (AbstractDateSet) -> None
146151
self._suspend_days.append(date_set)
@@ -241,8 +246,13 @@ def _are_fields_valid(fields, valid_fields):
241246
return False
242247
return True
243248

244-
def get_ex_cum_factor(self, order_book_id):
245-
return self._ex_cum_factor.get_factors(order_book_id)
249+
def get_ex_cum_factor(self, instrument: Instrument):
250+
try:
251+
ex_factor_store = self._ex_factor_stores[instrument.type, instrument.market]
252+
except KeyError:
253+
return None
254+
255+
return ex_factor_store.get_factors(instrument.order_book_id)
246256

247257
def _update_weekly_trading_date_index(self, idx):
248258
env = Environment.get_instance()
@@ -320,7 +330,7 @@ def history_bars(
320330
week_bars = self.resample_week_bars(bars, bar_count, fields)
321331
return week_bars if fields is None else week_bars[fields]
322332

323-
adjust_bars_date = adjust_bars(bars, self.get_ex_cum_factor(instrument.order_book_id),
333+
adjust_bars_date = adjust_bars(bars, self.get_ex_cum_factor(instrument),
324334
fields, adjust_type, adjust_orig)
325335
adjust_week_bars = self.resample_week_bars(adjust_bars_date, bar_count, fields)
326336
return adjust_week_bars if fields is None else adjust_week_bars[fields]
@@ -337,7 +347,7 @@ def history_bars(
337347
if isinstance(fields, str) and fields not in FIELDS_REQUIRE_ADJUSTMENT:
338348
return bars if fields is None else bars[fields]
339349

340-
bars = adjust_bars(bars, self.get_ex_cum_factor(instrument.order_book_id),
350+
bars = adjust_bars(bars, self.get_ex_cum_factor(instrument),
341351
fields, adjust_type, adjust_orig)
342352

343353
return bars if fields is None else bars[fields]

rqalpha/data/bundle.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def _get_split(self):
119119
return rqdatac.get_split(stocks)
120120

121121
def _write(self, data_iter: Iterable[tuple[str, np.ndarray]]):
122-
with h5py.File(os.path.join(self.d, 'split_factors.h5'), "w") as h5:
122+
with h5py.File(os.path.join(self.d, 'split_factor.h5'), "w") as h5:
123123
for order_book_id, data in data_iter:
124124
h5.create_dataset(order_book_id, data=data)
125125

@@ -130,32 +130,45 @@ def __call__(self):
130130
split['split_factor'] = split['split_coefficient_to'] / split['split_coefficient_from']
131131
split = split[['split_factor']]
132132
split.reset_index(inplace=True)
133-
split.rename(columns={'ex_dividend_date': 'ex_date'}, inplace=True)
133+
split.rename(columns={'ex_dividend_date': 'ex_date'}, inplace=True) # type: ignore
134134
split['ex_date'] = [convert_date_to_int(d) for d in split['ex_date']]
135135
split.set_index(['order_book_id', 'ex_date'], inplace=True)
136136
self._write([(
137137
order_book_id, split.loc[order_book_id].to_records()
138138
) for order_book_id in split.index.levels[0]]) # type: ignore
139139

140140

141+
class GenerateExFactorBundle:
142+
def __init__(self, d: str):
143+
self.d = d
144+
145+
def _get_ex_factor(self):
146+
stocks = rqdatac.all_instruments().order_book_id.tolist()
147+
return rqdatac.get_ex_factor(stocks)
141148

142-
def gen_ex_factor(d):
143-
stocks = rqdatac.all_instruments().order_book_id.tolist()
144-
ex_factor = rqdatac.get_ex_factor(stocks)
145-
ex_factor.reset_index(inplace=True)
146-
ex_factor['ex_date'] = [convert_date_to_int(d) for d in ex_factor['ex_date']]
147-
ex_factor.rename(columns={'ex_date': 'start_date'}, inplace=True)
148-
ex_factor.set_index(['order_book_id', 'start_date'], inplace=True)
149-
ex_factor = ex_factor[['ex_cum_factor']]
149+
def _write(self, data_iter: Iterable[tuple[str, np.ndarray]]):
150+
with h5py.File(os.path.join(self.d, 'ex_cum_factor.h5'), "w") as h5:
151+
for order_book_id, data in data_iter:
152+
h5.create_dataset(order_book_id, data=data)
153+
154+
def __call__(self):
155+
ex_factor = self._get_ex_factor()
156+
if ex_factor is None:
157+
raise RuntimeError("Got no ex factor data")
158+
ex_factor.reset_index(inplace=True)
159+
ex_factor['ex_date'] = [convert_date_to_int(d) for d in ex_factor['ex_date']]
160+
ex_factor.rename(columns={'ex_date': 'start_date'}, inplace=True)
161+
ex_factor.set_index(['order_book_id', 'start_date'], inplace=True)
162+
ex_factor = ex_factor[['ex_cum_factor']]
150163

151-
dtype = ex_factor.loc[ex_factor.index.levels[0][0]].to_records().dtype
152-
initial = np.empty((1,), dtype=dtype)
153-
initial['start_date'] = 0
154-
initial['ex_cum_factor'] = 1.0
164+
dtype = ex_factor.loc[ex_factor.index.levels[0][0]].to_records().dtype # type: ignore
165+
initial = np.empty((1,), dtype=dtype)
166+
initial['start_date'] = 0
167+
initial['ex_cum_factor'] = 1.0
155168

156-
with h5py.File(os.path.join(d, 'ex_cum_factor.h5'), 'w') as h5:
157-
for order_book_id in ex_factor.index.levels[0]:
158-
h5[order_book_id] = np.concatenate([initial, ex_factor.loc[order_book_id].to_records()])
169+
self._write(((
170+
order_book_id, np.concatenate([initial, ex_factor.loc[order_book_id].to_records()])
171+
) for order_book_id in ex_factor.index.levels[0])) # type: ignore
159172

160173

161174
def gen_share_transformation(d):
@@ -484,7 +497,7 @@ def gather_tasks(path: str, create: bool, enable_compression: bool, **h5_kwargs)
484497
)
485498

486499
gen_file_funcs = (
487-
gen_instruments, gen_trading_dates, gen_ex_factor, gen_st_days,
500+
gen_instruments, gen_trading_dates, gen_st_days,
488501
gen_suspended_days, gen_yield_curve, gen_share_transformation, gen_future_info
489502
)
490503
kwargs = {}
@@ -497,6 +510,7 @@ def gather_tasks(path: str, create: bool, enable_compression: bool, **h5_kwargs)
497510
tasks.append(GenerateFileTask(func, path))
498511
tasks.append(GenerateFileTask(GenerateDividendBundle(path)))
499512
tasks.append(GenerateFileTask(GenerateSplitBundle(path)))
513+
tasks.append(GenerateFileTask(GenerateExFactorBundle(path)))
500514
return tasks
501515

502516

rqalpha/data/data_proxy.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,17 @@ def get_split(self, order_book_id: str) -> np.ndarray | None:
118118
return self._data_source.get_split(instrument)
119119

120120
@lru_cache(10240)
121-
def _get_prev_close(self, order_book_id, dt):
121+
def _get_prev_close(self, order_book_id, dt, adjust_type: str = "pre"):
122122
instrument = self.instrument_not_none(order_book_id)
123123
prev_trading_date = self.get_previous_trading_date(dt)
124124
bar = self._data_source.history_bars(instrument, 1, '1d', 'close', prev_trading_date,
125-
skip_suspended=False, include_now=False, adjust_orig=dt)
125+
skip_suspended=False, include_now=False, adjust_type=adjust_type, adjust_orig=dt)
126126
if bar is None or len(bar) < 1:
127127
return np.nan
128128
return bar[0]
129129

130-
def get_prev_close(self, order_book_id, dt):
130+
def get_prev_close(self, order_book_id, dt, adjust_type: str = "pre"):
131+
# 获取(基于当日前复权过的)昨收价
131132
return self._get_prev_close(order_book_id, dt.replace(hour=0, minute=0, second=0))
132133

133134
@lru_cache(10240)

rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。
1616
# 详细的授权流程,请联系 public@ricequant.com 获取。
1717
from datetime import date
18+
from typing import Optional
1819
from functools import cached_property
1920

2021
from decimal import Decimal
@@ -61,6 +62,17 @@ def __init__(self, order_book_id, direction, init_quantity=0, init_price=None):
6162
self._pending_transform = None
6263
self._non_closable = 0
6364

65+
# 当日发生的拆分和分红,用于 position_pnl 的计算
66+
self._daily_dividend: float = 0.
67+
self._daily_split: float = 1.
68+
self._unadjusted_prev_close = None
69+
70+
@property
71+
def unadjusted_prev_close(self) -> float:
72+
if self._unadjusted_prev_close is None:
73+
self._unadjusted_prev_close = self._env.data_proxy.get_prev_close(self._order_book_id, self._env.trading_dt, "none")
74+
return self._unadjusted_prev_close
75+
6476
@property
6577
def dividend_receivable(self):
6678
# type: () -> float
@@ -83,6 +95,15 @@ def equity(self):
8395
def market_value_local(self):
8496
return self.market_value
8597

98+
@property
99+
def position_pnl(self) -> float:
100+
if not self._logical_old_quantity:
101+
# 新股第一天,没有 prev_close
102+
return 0
103+
return (self._logical_old_quantity * self._daily_split * (
104+
self.last_price - self.unadjusted_prev_close
105+
) + self._daily_dividend) * self._direction_factor
106+
86107
@property
87108
def closable(self):
88109
# type: () -> int
@@ -111,14 +132,15 @@ def get_state(self):
111132
def before_trading(self, trading_date):
112133
# type: (date) -> float
113134
delta_cash = super(StockPosition, self).before_trading(trading_date)
135+
self._unadjusted_prev_close = self.last_price
114136
if self._quantity == 0 and not self._dividend_receivable:
115137
return delta_cash
116138
if self.direction != POSITION_DIRECTION.LONG:
117139
raise RuntimeError("direction of stock position {} is not supposed to be short".format(self._order_book_id))
118140
data_proxy = self._env.data_proxy
119-
self._handle_dividend_book_closure(trading_date, data_proxy)
141+
self._daily_dividend = self._handle_dividend_book_closure(trading_date, data_proxy)
120142
delta_cash += self._handle_dividend_payable(trading_date)
121-
self._handle_split(trading_date, data_proxy)
143+
self._daily_split = self._handle_split(trading_date, data_proxy)
122144
return delta_cash
123145

124146
def apply_trade(self, trade):
@@ -198,11 +220,10 @@ def _get_dividends_or_splits(self, events: ndarray | None, trading_date: date, d
198220
events = events[left_pos: right_pos]
199221
return events
200222

201-
def _handle_dividend_book_closure(self, trading_date, data_proxy):
202-
# type: (date, DataProxy) -> None
223+
def _handle_dividend_book_closure(self, trading_date: date, data_proxy: DataProxy) -> float:
203224
dividends = self._get_dividends_or_splits(self._all_dividends, trading_date, "ex_dividend_date") # type: ignore[reportIncompatibleVariableOverride]
204225
if dividends is None or len(dividends) == 0:
205-
return
226+
return 0
206227
dividend_per_share: float = (dividends["dividend_cash_before_tax"] / dividends["round_lot"]).sum() * (1 - self.dividend_tax_rate)
207228
self._avg_price -= dividend_per_share
208229
# 前一天结算发生了除息, 此时 last_price 还是前一个交易日的收盘价,需要改为 除息后收盘价, 否则影响在before_trading中查看盈亏
@@ -211,6 +232,7 @@ def _handle_dividend_book_closure(self, trading_date, data_proxy):
211232
# FIXME: 这里隐含了获取的多条 dividend 的 payable_date 都相同的假设
212233
payable_date = _int_to_date(dividends["payable_date"][-1])
213234
self._dividend_receivable = (payable_date, self._quantity * dividend_per_share)
235+
return self._quantity * dividend_per_share
214236

215237
def _handle_dividend_payable(self, trading_date):
216238
# type: (date) -> float
@@ -236,18 +258,18 @@ def _handle_dividend_payable(self, trading_date):
236258
else:
237259
return dividend_value
238260

239-
def _handle_split(self, trading_date, data_proxy):
261+
def _handle_split(self, trading_date, data_proxy) -> float:
240262
splits = self._get_dividends_or_splits(self._all_splits, trading_date, "ex_date") # type: ignore[reportIncompatibleVariableOverride]
241263
if splits is None or len(splits) == 0:
242-
return
264+
return 1.
243265
ratio: float = splits["split_factor"].cumprod()[-1]
244266
self._avg_price /= ratio
245267
self._last_price /= ratio
246268
ratio_decimal = Decimal(ratio)
247269
# int(6000 * 1.15) -> 6899
248270
self._old_quantity = self._quantity = round(Decimal(self._quantity) * ratio_decimal)
249-
self._logical_old_quantity = round(Decimal(self._logical_old_quantity) * ratio_decimal)
250271
self._queue.handle_split(ratio_decimal, self._quantity)
272+
return ratio
251273

252274

253275
class FuturePosition(Position):

rqalpha/portfolio/position.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from typing import Dict, Iterable, Tuple, Optional, Deque, List
2222
from functools import cached_property
2323

24-
from rqalpha.const import POSITION_DIRECTION, POSITION_EFFECT, MARKET
24+
from rqalpha.const import POSITION_DIRECTION, POSITION_EFFECT, MARKET, SIDE
2525
from rqalpha.environment import Environment
2626
from rqalpha.interface import AbstractPosition
2727
from rqalpha.model.instrument import Instrument
@@ -123,14 +123,28 @@ def avg_price(self):
123123
return self._avg_price
124124

125125
@property
126-
def trading_pnl(self):
127-
# type: () -> float
126+
def trading_pnl(self) -> float:
127+
"""
128+
交易盈亏
129+
即当日所做的交易相比使用最新价交易产生的盈亏
130+
131+
买方向:sum(t.quantity * (last_price - t.price) for t in trades)
132+
卖方向:sum(t.quantity * (t.price - last_price) for t in trades)
133+
134+
direction = 1 if buy else -1
135+
trading_pnl
136+
= sum(t.quantity * (last_price - t.price) * direction for t in trades)
137+
= sum(t.quantity * last_price * direction for t in trades) - sum(t.quantity * t.price * direction for t in trades)
138+
= net_trade_quantity * last_price * direction - sum(t.quantity * t.price * direction for t in trades)
139+
140+
trade_cost = sum(t.quantity * t.price * direction for t in trades)
141+
trading_pnl = net_trade_quantity * last_price * direction - trade_cost
142+
"""
128143
trade_quantity = self._quantity - self._logical_old_quantity
129144
return (trade_quantity * self.last_price - self._trade_cost) * self._direction_factor
130145

131146
@property
132-
def position_pnl(self):
133-
# type: () -> float
147+
def position_pnl(self) -> float:
134148
if self._logical_old_quantity:
135149
return self._logical_old_quantity * (self.last_price - self.prev_close) * self._direction_factor
136150
else:
@@ -155,8 +169,8 @@ def equity(self):
155169
return self.last_price * self._quantity if self._quantity else 0
156170

157171
@property
158-
def prev_close(self):
159-
# type: () -> float
172+
def prev_close(self) -> float:
173+
# (前复权过的)昨收价
160174
if not is_valid_price(self._prev_close):
161175
self._prev_close = self._env.data_proxy.get_prev_close(self._order_book_id, self._env.trading_dt)
162176
if not is_valid_price(self._prev_close):
@@ -232,10 +246,17 @@ def before_trading(self, trading_date):
232246
self._prev_close = None
233247
return 0
234248

249+
def _update_costs(self, trade: Trade):
250+
self._transaction_cost += trade.transaction_cost
251+
if trade.side == SIDE.BUY:
252+
self._trade_cost += trade.last_price * trade.last_quantity
253+
else:
254+
self._trade_cost -= trade.last_price * trade.last_quantity
255+
235256
def apply_trade(self, trade):
236257
# type: (Trade) -> float
237258
# 返回总资金的变化量
238-
self._transaction_cost += trade.transaction_cost
259+
self._update_costs(trade)
239260
if trade.position_effect == POSITION_EFFECT.OPEN:
240261
self._queue.handle_trade(trade.last_quantity, self._env.trading_dt.date())
241262
if self._quantity < 0:
@@ -244,14 +265,12 @@ def apply_trade(self, trade):
244265
cost = self._quantity * self._avg_price + trade.last_quantity * trade.last_price
245266
self._avg_price = cost / (self._quantity + trade.last_quantity)
246267
self._quantity += trade.last_quantity
247-
self._trade_cost += trade.last_price * trade.last_quantity
248268
return (-1 * trade.last_price * trade.last_quantity) - trade.transaction_cost
249269
elif trade.position_effect == POSITION_EFFECT.CLOSE:
250270
# 先平昨,后平今
251271
self._queue.handle_trade(-trade.last_quantity, self._env.trading_dt.date())
252272
self._old_quantity -= min(trade.last_quantity, self._old_quantity)
253273
self._quantity -= trade.last_quantity
254-
self._trade_cost -= trade.last_price * trade.last_quantity
255274
return trade.last_price * trade.last_quantity - trade.transaction_cost
256275
else:
257276
raise NotImplementedError("{} does not support position effect {}".format(

0 commit comments

Comments
 (0)