rqalpha.mod.rqalpha_mod_sys_accounts.position_model 源代码

# -*- coding: utf-8 -*-
# 版权所有 2019 深圳米筐科技有限公司(下称“米筐科技”)
#
# 除非遵守当前许可,否则不得使用本软件。
#
#     * 非商业用途(非商业用途指个人出于非商业目的使用本软件,或者高校、研究所等非营利机构出于教育、科研等目的使用本软件):
#         遵守 Apache License 2.0(下称“Apache 2.0 许可”),
#         您可以在以下位置获得 Apache 2.0 许可的副本:http://www.apache.org/licenses/LICENSE-2.0。
#         除非法律有要求或以书面形式达成协议,否则本软件分发时需保持当前许可“原样”不变,且不得附加任何条件。
#
#     * 商业用途(商业用途指个人出于任何商业目的使用本软件,或者法人或其他组织出于任何目的使用本软件):
#         未经米筐科技授权,任何个人不得出于任何商业目的使用本软件(包括但不限于向第三方提供、销售、出租、出借、转让本软件、
#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务),任何法人或其他组织不得出于任何目的使用本软件,
#         否则米筐科技有权追究相应的知识产权侵权责任。
#         在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。
#         详细的授权流程,请联系 public@ricequant.com 获取。
from datetime import date
from typing import Optional, Deque, Tuple
from collections import deque

from decimal import Decimal, ROUND_HALF_UP
from numpy import ndarray, isclose

from rqalpha.interface import TransactionCost
from rqalpha.model.trade import Trade
from rqalpha.const import POSITION_DIRECTION, SIDE, POSITION_EFFECT, DEFAULT_ACCOUNT_TYPE, INSTRUMENT_TYPE, TRADING_CALENDAR_TYPE
from rqalpha.environment import Environment
from rqalpha.portfolio.position import Position, PositionProxy
from rqalpha.data.data_proxy import DataProxy
from rqalpha.utils import INST_TYPE_IN_STOCK_ACCOUNT, is_valid_price
from rqalpha.utils.datetime_func import convert_date_to_date_int
from rqalpha.utils.logger import user_system_log, system_log
from rqalpha.utils.class_helper import deprecated_property
from rqalpha.utils.i18n import gettext as _
from rqalpha.core.events import EVENT, Event
from rqalpha.utils.class_helper import cached_property
from .trade_utils import get_amount_from_value


def _int_to_date(d):
    r, d = divmod(d, 100)
    y, m = divmod(r, 100)
    return date(year=y, month=m, day=d)


[文档]class StockPosition(Position): # 注意,涉及到非人民币的持仓: # 1. before_trading 和 settlement 返回的资金变化为本币 # 2. 命名带 local 后缀的 property 返回的值为本币计价 # 3. 其他 property 返回的值应为结算货币计价 __repr_properties__ = ( "order_book_id", "direction", "quantity", "market_value", "trading_pnl", "position_pnl", "last_price" ) __instrument_types__ = INST_TYPE_IN_STOCK_ACCOUNT dividend_reinvestment = False dividend_tax_rate: float = 0. cash_return_by_stock_delisted = True t_plus_enabled = True calendar_type = TRADING_CALENDAR_TYPE.CN_STOCK def __init__(self, order_book_id, direction, init_quantity=0, init_price=None): super(StockPosition, self).__init__(order_book_id, direction, init_quantity, init_price) self._dividend_receivable: Deque[Tuple[date, float]] = deque() self._pending_transform = None self._non_closable = 0 # 当日发生的拆分和分红,用于 position_pnl 的计算 self._daily_dividend: float = 0. self._daily_split: float = 1. self._unadjusted_prev_close = None @property def unadjusted_prev_close(self) -> float: if self._unadjusted_prev_close is None: self._unadjusted_prev_close = self._env.data_proxy.get_prev_close(self._order_book_id, self._env.trading_dt, "none") return self._unadjusted_prev_close @property def dividend_receivable(self): # type: () -> float """ 应收分红 """ return sum(v for _, v in self._dividend_receivable) @property def equity(self): # type: () -> float """ 持仓权益 """ return super(StockPosition, self).equity + self.dividend_receivable @property def market_value_local(self): return self.market_value @property def trading_pnl(self) -> float: trade_quantity = self._quantity - self._logical_old_quantity * self._daily_split return (trade_quantity * self.last_price - self._trade_cost) * self._direction_factor @property def position_pnl(self) -> float: if not self._logical_old_quantity: # 新股第一天,没有 prev_close return 0 return (self._logical_old_quantity * self._daily_split * ( self.last_price - self.unadjusted_prev_close / self._daily_split ) + self._daily_dividend) * self._direction_factor @property def closable(self): # type: () -> int order_quantity = sum(o.unfilled_quantity for o in self._open_orders if o.position_effect in ( POSITION_EFFECT.CLOSE, POSITION_EFFECT.CLOSE_TODAY, POSITION_EFFECT.EXERCISE )) if self.t_plus_enabled: return self._quantity - order_quantity - self._non_closable return self._quantity - order_quantity def set_state(self, state): super(StockPosition, self).set_state(state) self._dividend_receivable = state.get("dividend_receivable") self._pending_transform = state.get("pending_transform") self._non_closable = state.get("non_closable", 0) def get_state(self): state = super(StockPosition, self).get_state() state.update({ "dividend_receivable": self._dividend_receivable, "pending_transform": self._pending_transform, "non_closable": self._non_closable }) return state def before_trading(self, trading_date): # type: (date) -> float delta_cash = super(StockPosition, self).before_trading(trading_date) self._unadjusted_prev_close = self.last_price if self._quantity == 0 and not self._dividend_receivable: return delta_cash if self.direction != POSITION_DIRECTION.LONG: raise RuntimeError("direction of stock position {} is not supposed to be short".format(self._order_book_id)) data_proxy = self._env.data_proxy self._daily_dividend = self._handle_dividend_book_closure(trading_date, data_proxy) # 需要先执行拆股后再执行分红支付操作,否则当开启分红再投资时,会将当日到账的票也进行拆股 self._daily_split = self._handle_split(trading_date, data_proxy) delta_cash += self._handle_dividend_payable(trading_date) return delta_cash def apply_trade(self, trade): # type: (Trade) -> float # 返回总资金的变化量 delta_cash = super(StockPosition, self).apply_trade(trade) if trade.position_effect == POSITION_EFFECT.OPEN and self._market_tplus >= 1: # type: ignore self._non_closable += trade.last_quantity return delta_cash def settlement(self, trading_date): # type: (date) -> float super(StockPosition, self).settlement(trading_date) if self._quantity == 0: return 0 if self.direction != POSITION_DIRECTION.LONG: raise RuntimeError("direction of stock position {} is not supposed to be short".format(self._order_book_id)) next_date = self._env.data_proxy.get_next_trading_date(trading_date, trading_calendar_type=self.calendar_type) delta_cash = 0 if self._instrument.de_listed_at(next_date): try: transform_data = self._env.data_proxy.get_share_transformation(self._order_book_id) except NotImplementedError: pass else: if transform_data is not None: successor, conversion_ratio = transform_data self._env.portfolio.get_account(successor).apply_trade(Trade.__from_create__( order_id=None, price=self.avg_price / conversion_ratio, amount=self._quantity * conversion_ratio, side=SIDE.BUY, position_effect=POSITION_EFFECT.OPEN, order_book_id=successor, transaction_cost=TransactionCost.zero() )) for direction in POSITION_DIRECTION: successor_position = self._env.portfolio.get_position(successor, direction) successor_position.update_last_price(self._last_price / conversion_ratio) # 把购买 successor 消耗的 cash 补充回来 delta_cash = self.market_value_local if self.cash_return_by_stock_delisted: delta_cash = self.market_value_local self._trade_cost = -self.market_value # 相当于卖掉了,所以给一个负成本 self._quantity = self._old_quantity = 0 self._queue.clear() return delta_cash @cached_property def _market_tplus(self): return self._instrument.market_tplus @cached_property def _all_dividends(self) -> Optional[ndarray]: dividends = self._env.data_proxy.get_dividend(self._order_book_id) return dividends @cached_property def _all_splits(self) -> Optional[ndarray]: splits = self._env.data_proxy.get_split(self._order_book_id) if splits is None: return None splits = splits.copy() splits["ex_date"] = splits["ex_date"] // 1000000 return splits def _get_dividends_or_splits(self, events: Optional[ndarray], trading_date: date, date_field: str): if events is None: return None last_date = self._env.data_proxy.get_previous_trading_date(trading_date) last_date_int = convert_date_to_date_int(last_date) today_int = convert_date_to_date_int(trading_date) events_dates = events[date_field] left_pos = events_dates.searchsorted(last_date_int, side="right") right_pos = events_dates.searchsorted(today_int, side="right") events = events[left_pos: right_pos] return events def _handle_dividend_book_closure(self, trading_date: date, data_proxy: DataProxy) -> float: dividends = self._get_dividends_or_splits(self._all_dividends, trading_date, "ex_dividend_date") # type: ignore[reportIncompatibleVariableOverride] if dividends is None or len(dividends) == 0: return 0 dividend_per_share: float = (dividends["dividend_cash_before_tax"] / dividends["round_lot"]).sum() * (1 - self.dividend_tax_rate) self._avg_price -= dividend_per_share # 前一天结算发生了除息, 此时 last_price 还是前一个交易日的收盘价,需要改为 除息后收盘价, 否则影响在before_trading中查看盈亏 self._last_price -= dividend_per_share # type: ignore # FIXME: 这里隐含了获取的多条 dividend 的 payable_date 都相同的假设 payable_date = _int_to_date(dividends["payable_date"][-1]) self._dividend_receivable.append((payable_date, self._quantity * dividend_per_share)) return self._quantity * dividend_per_share def _handle_dividend_payable(self, trading_date: date) -> float: # 返回总资金的变化量 if not self._dividend_receivable: return 0 payable_value = 0. while self._dividend_receivable and self._dividend_receivable[0][0] <= trading_date: _, dividend_value = self._dividend_receivable.popleft() payable_value += dividend_value if payable_value and self.dividend_reinvestment: last_price = self.last_price account = self._env.get_account(self._order_book_id) amount = get_amount_from_value(payable_value, self._instrument, last_price, self._env, account.cash) if amount > 0: trade = Trade.__from_create__( None, last_price, amount, SIDE.BUY, POSITION_EFFECT.OPEN, self._order_book_id, ) self._env.event_bus.publish_event(Event(EVENT.TRADE, account=account, trade=trade, order=None)) return payable_value - amount * last_price - trade.transaction_cost return payable_value else: return payable_value def _get_split_ratio(self, splits) -> Decimal: # rqalpha 6.1.0 修改了 bundle 的 splits_factor 的数据格式,需要向前兼容 if 'split_coefficient_to' not in splits.dtype.names: return Decimal(splits["split_factor"].cumprod()[-1]) for field in ["split_coefficient_to", "split_coefficient_from"]: if not all(isclose(splits[field] % 1, 0)): ratio = splits["split_coefficient_to"] / splits["split_coefficient_from"] return Decimal(ratio.cumprod()[-1]) coefficient_to = (splits["split_coefficient_to"].astype(int)).cumprod()[-1] coefficient_from = (splits["split_coefficient_from"].astype(int)).cumprod()[-1] return Decimal(int(coefficient_to)) / Decimal(int(coefficient_from)) def _handle_split(self, trading_date, data_proxy) -> float: splits = self._get_dividends_or_splits(self._all_splits, trading_date, "ex_date") # type: ignore[reportIncompatibleVariableOverride] if splits is None or len(splits) == 0: return 1. ratio_decimal = self._get_split_ratio(splits) ratio = float(ratio_decimal) self._avg_price /= ratio self._last_price /= ratio # type: ignore # int(6000 * 1.15) -> 6899 self._old_quantity = self._quantity = int((Decimal(self._quantity) * ratio_decimal).quantize(Decimal("1"), rounding=ROUND_HALF_UP)) self._queue.handle_split(ratio_decimal, self._quantity) return ratio
[文档]class FuturePosition(Position): __repr_properties__ = ( "order_book_id", "direction", "old_quantity", "quantity", "margin", "market_value", "trading_pnl", "position_pnl", "last_price" ) __instrument_types__ = [INSTRUMENT_TYPE.FUTURE] old_quantity = property(lambda self: self._old_quantity) today_quantity = property(lambda self: self._quantity - self._old_quantity) @cached_property def contract_multiplier(self): return self._instrument.contract_multiplier @property def margin_rate(self): # type: () -> float if self.direction == POSITION_DIRECTION.LONG: margin_ratio = self._instrument.get_long_margin_ratio(self._env.trading_dt.date()) elif self.direction == POSITION_DIRECTION.SHORT: margin_ratio = self._instrument.get_short_margin_ratio(self._env.trading_dt.date()) return margin_ratio * self._env.config.base.margin_multiplier @property def equity(self): # type: () -> float """""" return self._quantity * (self.last_price - self._avg_price) * self.contract_multiplier * self._direction_factor @property def margin(self): # rtpe: () -> float """ 保证金 = 持仓量 * 最新价 * 合约乘数 * 保证金率 """ return self.margin_rate * self.market_value @property def market_value(self): # type: () -> float return self.contract_multiplier * super(FuturePosition, self).market_value @property def trading_pnl(self): # type: () -> float return self.contract_multiplier * super(FuturePosition, self).trading_pnl @property def position_pnl(self): # type: () -> float return self.contract_multiplier * super(FuturePosition, self).position_pnl @property def pnl(self): # type: () -> float return super(FuturePosition, self).pnl * self.contract_multiplier def calc_close_today_amount(self, trade_amount, position_effect): if position_effect == POSITION_EFFECT.CLOSE_TODAY: return trade_amount if trade_amount <= self.today_quantity else self.today_quantity else: return max(trade_amount - self._old_quantity, 0) def apply_trade(self, trade): if trade.position_effect == POSITION_EFFECT.CLOSE_TODAY: self._transaction_cost += trade.transaction_cost self._quantity -= trade.last_quantity self._trade_cost -= trade.last_price * trade.last_quantity self._queue.handle_trade(-trade.last_quantity, self._env.trading_dt.date(), close_today=True) else: super(FuturePosition, self).apply_trade(trade) if trade.position_effect == POSITION_EFFECT.OPEN: return -1 * trade.transaction_cost else: return -1 * trade.transaction_cost + ( trade.last_price - self._avg_price ) * trade.last_quantity * self.contract_multiplier * self._direction_factor @property def prev_close(self): if not is_valid_price(self._prev_close): if self._env.config.mod.sys_accounts.futures_settlement_price_type == "settlement": self._prev_close = self._env.data_proxy.get_prev_settlement(self._order_book_id, self._env.trading_dt) else: self._prev_close = super().prev_close return self._prev_close def settlement(self, trading_date): # type: (date) -> float delta_cash = super(FuturePosition, self).settlement(trading_date) if self._quantity == 0: return delta_cash data_proxy = self._env.data_proxy next_date = data_proxy.get_next_trading_date(trading_date) if self._env.config.mod.sys_accounts.futures_settlement_price_type == "settlement": # 逐日盯市按照结算价结算 settle_price = self._env.data_proxy.get_settle_price(self._order_book_id, self._env.trading_dt) if not is_valid_price(settle_price): system_log.warning(f"{self._order_book_id} is missing settlement data for {self._env.trading_dt}, close price will be used.") else: self._last_price = settle_price delta_cash += self.equity self._avg_price = self.last_price if self._instrument.de_listed_at(next_date): user_system_log.warn(_(u"{order_book_id} is expired, close all positions by system").format( order_book_id=self._order_book_id )) account = self._env.get_account(self._order_book_id) side = SIDE.SELL if self.direction == POSITION_DIRECTION.LONG else SIDE.BUY trade = Trade.__from_create__( None, self.last_price, self._quantity, side, POSITION_EFFECT.CLOSE, self._order_book_id, transaction_cost=TransactionCost.zero() ) self._env.event_bus.publish_event(Event(EVENT.TRADE, account=account, trade=trade, order=None)) self._quantity = self._old_quantity = 0 self._queue.clear() return delta_cash def post_settlement(self): try: del self.__dict__["margin_ratio"] except KeyError: pass
class StockPositionProxy(PositionProxy): __repr_properties__ = ( "order_book_id", "quantity", "avg_price", "market_value" ) __instrument_types__ = INST_TYPE_IN_STOCK_ACCOUNT @property def type(self): return "STOCK" @property def quantity(self): return self._long.quantity @property def sellable(self): """ [int] 该仓位可卖出股数。T+1 的市场中sellable = 所有持仓 - 今日买入的仓位 - 已冻结 """ return self._long.closable @property def avg_price(self): """ [float] 平均开仓价格 """ return self._long.avg_price @property def value_percent(self): """ [float] 获得该持仓的实时市场价值在股票投资组合价值中所占比例,取值范围[0, 1] """ accounts = Environment.get_instance().portfolio.accounts if DEFAULT_ACCOUNT_TYPE.STOCK not in accounts: return 0 total_value = accounts[DEFAULT_ACCOUNT_TYPE.STOCK].total_value return 0 if total_value == 0 else self.market_value / total_value class FuturePositionProxy(PositionProxy): __repr_properties__ = ( "order_book_id", "buy_quantity", "sell_quantity", "buy_market_value", "sell_market_value", "buy_margin", "sell_margin" ) __instrument_types__ = [INSTRUMENT_TYPE.FUTURE] @property def type(self): return "FUTURE" @property def margin_rate(self): return self._long.margin_rate @property def contract_multiplier(self): return self._long.contract_multiplier @property def buy_market_value(self): """ [float] 多方向市值 """ return self._long.market_value @property def sell_market_value(self): """ [float] 空方向市值 """ return self._short.market_value @property def buy_position_pnl(self): """ [float] 多方向昨仓盈亏 """ return self._long.position_pnl @property def sell_position_pnl(self): """ [float] 空方向昨仓盈亏 """ return self._short.position_pnl @property def buy_trading_pnl(self): """ [float] 多方向交易盈亏 """ return self._long.trading_pnl @property def sell_trading_pnl(self): """ [float] 空方向交易盈亏 """ return self._short.trading_pnl @property def buy_daily_pnl(self): """ [float] 多方向每日盈亏 """ return self.buy_position_pnl + self.buy_trading_pnl @property def sell_daily_pnl(self): """ [float] 空方向每日盈亏 """ return self.sell_position_pnl + self.sell_trading_pnl @property def buy_pnl(self): """ [float] 买方向累计盈亏 """ return self._long.pnl @property def sell_pnl(self): """ [float] 空方向累计盈亏 """ return self._short.pnl @property def buy_old_quantity(self): """ [int] 多方向昨仓 """ return self._long.old_quantity @property def sell_old_quantity(self): """ [int] 空方向昨仓 """ return self._short.old_quantity @property def buy_today_quantity(self): """ [int] 多方向今仓 """ return self._long.today_quantity @property def sell_today_quantity(self): """ [int] 空方向今仓 """ return self._short.today_quantity @property def buy_quantity(self): """ [int] 多方向持仓 """ return self.buy_old_quantity + self.buy_today_quantity @property def sell_quantity(self): """ [int] 空方向持仓 """ return self.sell_old_quantity + self.sell_today_quantity @property def margin(self): """ [float] 保证金 保证金 = 持仓量 * 最新价 * 合约乘数 * 保证金率 股票保证金 = 市值 = 持仓量 * 最新价 """ return self._long.margin + self._short.margin @property def buy_margin(self): """ [float] 多方向持仓保证金 """ return self._long.margin @property def sell_margin(self): """ [float] 空方向持仓保证金 """ return self._short.margin @property def buy_avg_open_price(self): """ [float] 多方向平均开仓价格 """ return self._long.avg_price @property def sell_avg_open_price(self): """ [float] 空方向平均开仓价格 """ return self._short.avg_price @property def buy_transaction_cost(self): """ [float] 多方向交易费率 """ return self._long.transaction_cost @property def sell_transaction_cost(self): """ [float] 空方向交易费率 """ return self._short.transaction_cost @property def closable_today_sell_quantity(self): return self._long.today_closable @property def closable_today_buy_quantity(self): return self._long.today_closable @property def closable_buy_quantity(self): """ [float] 可平多方向持仓 """ return self._long.closable @property def closable_sell_quantity(self): """ [float] 可平空方向持仓 """ return self._short.closable holding_pnl = deprecated_property("holding_pnl", "position_pnl") buy_holding_pnl = deprecated_property("buy_holding_pnl", "buy_position_pnl") sell_holding_pnl = deprecated_property("sell_holding_pnl", "sell_position_pnl") realized_pnl = deprecated_property("realized_pnl", "trading_pnl") buy_realized_pnl = deprecated_property("buy_realized_pnl", "buy_trading_pnl") sell_realized_pnl = deprecated_property("sell_realized_pnl", "sell_trading_pnl") buy_avg_holding_price = deprecated_property("buy_avg_holding_price", "buy_avg_open_price") sell_avg_holding_price = deprecated_property("sell_avg_holding_price", "sell_avg_open_price")