1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-14 08:16:54 +08:00

fix calculating base_price

This commit is contained in:
Young
2021-07-08 05:54:36 +00:00
parent e8f5a1e491
commit 17d8b8a7cc
4 changed files with 133 additions and 60 deletions

View File

@@ -3,6 +3,7 @@
import copy import copy
from typing import Dict, List
from qlib.utils import init_instance_by_config from qlib.utils import init_instance_by_config
import warnings import warnings
import pandas as pd import pandas as pd
@@ -248,7 +249,7 @@ class Account:
atomic: bool, atomic: bool,
outer_trade_decision: BaseTradeDecision, outer_trade_decision: BaseTradeDecision,
trade_info: list = None, trade_info: list = None,
inner_order_indicators: Indicator = None, inner_order_indicators: List[Dict[str, pd.Series]] = None,
indicator_config: dict = {}, indicator_config: dict = {},
): ):
"""update account at each trading bar step """update account at each trading bar step
@@ -292,10 +293,15 @@ class Account:
self.indicator.clear() self.indicator.clear()
if atomic: if atomic:
self.indicator.update_order_indicators(trade_start_time, trade_end_time, trade_info, trade_exchange) self.indicator.update_order_indicators(trade_info)
else: else:
self.indicator.agg_order_indicators( self.indicator.agg_order_indicators(
inner_order_indicators, indicator_config=indicator_config, outer_trade_decision=outer_trade_decision trade_start_time,
trade_end_time,
inner_order_indicators,
outer_trade_decision=outer_trade_decision,
trade_exchange=trade_exchange,
indicator_config=indicator_config,
) )
self.indicator.cal_trade_indicators(trade_start_time, self.freq, indicator_config) self.indicator.cal_trade_indicators(trade_start_time, self.freq, indicator_config)

View File

@@ -281,27 +281,27 @@ class Exchange:
return trade_val, trade_cost, trade_price return trade_val, trade_cost, trade_price
def get_quote_info(self, stock_id, start_time, end_time): def get_quote_info(self, stock_id, start_time, end_time, method=ts_data_last):
return resam_ts_data(self.quote[stock_id], start_time, end_time, method=ts_data_last) return resam_ts_data(self.quote[stock_id], start_time, end_time, method=method)
def get_close(self, stock_id, start_time, end_time): def get_close(self, stock_id, start_time, end_time, method=ts_data_last):
return resam_ts_data(self.quote[stock_id]["$close"], start_time, end_time, method=ts_data_last) return resam_ts_data(self.quote[stock_id]["$close"], start_time, end_time, method=method)
def get_volume(self, stock_id, start_time, end_time): def get_volume(self, stock_id, start_time, end_time, method="sum"):
return resam_ts_data(self.quote[stock_id]["$volume"], start_time, end_time, method="sum") return resam_ts_data(self.quote[stock_id]["$volume"], start_time, end_time, method=method)
def get_deal_price(self, stock_id, start_time, end_time, direction: OrderDir): def get_deal_price(self, stock_id, start_time, end_time, direction: OrderDir, method=ts_data_last):
if direction == OrderDir.SELL: if direction == OrderDir.SELL:
pstr = self.sell_price pstr = self.sell_price
elif direction == OrderDir.BUY: elif direction == OrderDir.BUY:
pstr = self.buy_price pstr = self.buy_price
else: else:
raise NotImplementedError(f"This type of input is not supported") raise NotImplementedError(f"This type of input is not supported")
deal_price = resam_ts_data(self.quote[stock_id][pstr], start_time, end_time, method=ts_data_last) deal_price = resam_ts_data(self.quote[stock_id][pstr], start_time, end_time, method=method)
if np.isclose(deal_price, 0.0) or np.isnan(deal_price): if method is not None and (np.isclose(deal_price, 0.0) or np.isnan(deal_price)):
self.logger.warning(f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {pstr}): {deal_price}!!!") self.logger.warning(f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {pstr}): {deal_price}!!!")
self.logger.warning(f"setting deal_price to close price") self.logger.warning(f"setting deal_price to close price")
deal_price = self.get_close(stock_id, start_time, end_time) deal_price = self.get_close(stock_id, start_time, end_time, method)
return deal_price return deal_price
def get_factor(self, stock_id, start_time, end_time) -> Union[float, None]: def get_factor(self, stock_id, start_time, end_time) -> Union[float, None]:

View File

@@ -13,6 +13,7 @@ if TYPE_CHECKING:
from qlib.backtest.utils import TradeCalendarManager from qlib.backtest.utils import TradeCalendarManager
import warnings import warnings
import pandas as pd import pandas as pd
import numpy as np
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import ClassVar, Union, List, Set, Tuple from typing import ClassVar, Union, List, Set, Tuple
@@ -88,11 +89,14 @@ class Order:
return self.direction * 2 - 1 return self.direction * 2 - 1
@staticmethod @staticmethod
def parse_dir(direction: Union[str, int, OrderDir]) -> OrderDir: def parse_dir(direction: Union[str, int, float, np.integer, np.floating, OrderDir]) -> OrderDir:
if isinstance(direction, OrderDir): if isinstance(direction, OrderDir):
return direction return direction
elif isinstance(direction, int): elif isinstance(direction, (int, float, np.integer, np.floating)):
return OrderDir(direction) if direction > 0:
return Order.BUY
else:
return Order.SELL
elif isinstance(direction, str): elif isinstance(direction, str):
dl = direction.lower() dl = direction.lower()
if dl.strip() == "sell": if dl.strip() == "sell":

View File

@@ -4,9 +4,11 @@
from collections import OrderedDict from collections import OrderedDict
from logging import warning from logging import warning
from typing import List from qlib.backtest.exchange import Exchange
from qlib.backtest.order import BaseTradeDecision, Order from typing import Dict, List
from qlib.backtest.order import BaseTradeDecision, Order, OrderDir
import pandas as pd import pandas as pd
import numpy as np
import pathlib import pathlib
import warnings import warnings
from pandas.core import groupby from pandas.core import groupby
@@ -221,6 +223,33 @@ class Report:
class Indicator: class Indicator:
"""
`Indicator` is implemented in a aggregate way.
All the metrics are calculated aggregately.
All the metrics are calculated for a seperated stock and in a specific step on a specific level.
| indicator | desc. |
|--------------+--------------------------------------------------------------|
| amount | the *target* amount given by the outer strategy |
| inner_amount | the total *target* amount of inner strategy |
| trade_price | the average deal price |
| trade_value | the total trade value |
| trade_cost | the total trade cost (base price need drection) |
| trade_dir | the trading direction |
| ffr | full fill rate |
| pa | price advantage |
| pos | win rate |
| base_price | the price of baseline |
| base_volume | the volume of baseline (for weighted aggregating base_price) |
**NOTE**:
The `base_price` and `base_volume` can't be NaN when there are not trading on that step. Otherwise
aggregating get wrong results.
So `base_price` will not be calculated in a aggregate way!!
"""
def __init__(self): def __init__(self):
self.order_indicator_his = OrderedDict() self.order_indicator_his = OrderedDict()
self.order_indicator = OrderedDict() self.order_indicator = OrderedDict()
@@ -241,6 +270,7 @@ class Indicator:
trade_price = dict() trade_price = dict()
trade_value = dict() trade_value = dict()
trade_cost = dict() trade_cost = dict()
trade_dir = dict()
for order, _trade_val, _trade_cost, _trade_price in trade_info: for order, _trade_val, _trade_cost, _trade_price in trade_info:
amount[order.stock_id] = order.amount_delta amount[order.stock_id] = order.amount_delta
@@ -248,36 +278,32 @@ class Indicator:
trade_price[order.stock_id] = _trade_price trade_price[order.stock_id] = _trade_price
trade_value[order.stock_id] = _trade_val * order.sign trade_value[order.stock_id] = _trade_val * order.sign
trade_cost[order.stock_id] = _trade_cost trade_cost[order.stock_id] = _trade_cost
trade_dir[order.stock_id] = order.direction
self.order_indicator["amount"] = self.order_indicator["inner_amount"] = pd.Series(amount) self.order_indicator["amount"] = self.order_indicator["inner_amount"] = pd.Series(amount)
self.order_indicator["deal_amount"] = pd.Series(deal_amount) self.order_indicator["deal_amount"] = pd.Series(deal_amount)
# NOTE: trade_price and baseline price will be same on the lowest-level
self.order_indicator["trade_price"] = pd.Series(trade_price) self.order_indicator["trade_price"] = pd.Series(trade_price)
self.order_indicator["trade_value"] = pd.Series(trade_value) self.order_indicator["trade_value"] = pd.Series(trade_value)
self.order_indicator["trade_cost"] = pd.Series(trade_cost) self.order_indicator["trade_cost"] = pd.Series(trade_cost)
self.order_indicator["trade_dir"] = pd.Series(trade_dir)
def _update_order_fulfill_rate(self): def _update_order_fulfill_rate(self):
self.order_indicator["ffr"] = self.order_indicator["deal_amount"] / self.order_indicator["amount"] self.order_indicator["ffr"] = self.order_indicator["deal_amount"] / self.order_indicator["amount"]
def _update_order_price_advantage(self, trade_exchange, trade_start_time, trade_end_time): def _update_order_price_advantage(self):
self.order_indicator["base_price"] = self.order_indicator["trade_price"] # NOTE:
instruments = list(self.order_indicator["base_price"].index) # trade_price and baseline price will be same on the lowest-level
self.order_indicator["volume"] = pd.Series( # So Pa should be 0
[ self.order_indicator["pa"] = 0
trade_exchange.get_volume(stock_id=inst, start_time=trade_start_time, end_time=trade_end_time)
for inst in instruments
],
index=instruments,
)
self.order_indicator["pa"] = (
self.order_indicator["trade_price"] - self.order_indicator["base_price"]
) / self.order_indicator["base_price"]
def _agg_order_trade_info(self, inner_order_indicators): def _agg_order_trade_info(self, inner_order_indicators: List[Dict[str, pd.Series]]):
inner_amount = pd.Series() inner_amount = pd.Series()
deal_amount = pd.Series() deal_amount = pd.Series()
trade_price = pd.Series() trade_price = pd.Series()
trade_value = pd.Series() trade_value = pd.Series()
trade_cost = pd.Series() trade_cost = pd.Series()
trade_dir = pd.Series()
for _order_indicator in inner_order_indicators: for _order_indicator in inner_order_indicators:
inner_amount = inner_amount.add(_order_indicator["inner_amount"], fill_value=0) inner_amount = inner_amount.add(_order_indicator["inner_amount"], fill_value=0)
deal_amount = deal_amount.add(_order_indicator["deal_amount"], fill_value=0) deal_amount = deal_amount.add(_order_indicator["deal_amount"], fill_value=0)
@@ -286,6 +312,9 @@ class Indicator:
) )
trade_value = trade_value.add(_order_indicator["trade_value"], fill_value=0) trade_value = trade_value.add(_order_indicator["trade_value"], fill_value=0)
trade_cost = trade_cost.add(_order_indicator["trade_cost"], fill_value=0) trade_cost = trade_cost.add(_order_indicator["trade_cost"], fill_value=0)
trade_dir = trade_dir.add(_order_indicator["trade_dir"])
trade_dir = trade_dir.apply(Order.parse_dir)
self.order_indicator["inner_amount"] = inner_amount self.order_indicator["inner_amount"] = inner_amount
self.order_indicator["deal_amount"] = deal_amount self.order_indicator["deal_amount"] = deal_amount
@@ -293,6 +322,7 @@ class Indicator:
self.order_indicator["trade_price"] = trade_price self.order_indicator["trade_price"] = trade_price
self.order_indicator["trade_value"] = trade_value self.order_indicator["trade_value"] = trade_value
self.order_indicator["trade_cost"] = trade_cost self.order_indicator["trade_cost"] = trade_cost
self.order_indicator["trade_dir"] = trade_dir
def _update_trade_amount(self, outer_trade_decision: BaseTradeDecision): def _update_trade_amount(self, outer_trade_decision: BaseTradeDecision):
# NOTE: these indicator is designed for order execution, so the # NOTE: these indicator is designed for order execution, so the
@@ -305,34 +335,59 @@ class Indicator:
def _agg_order_fulfill_rate(self): def _agg_order_fulfill_rate(self):
self.order_indicator["ffr"] = self.order_indicator["deal_amount"] / self.order_indicator["amount"] self.order_indicator["ffr"] = self.order_indicator["deal_amount"] / self.order_indicator["amount"]
def _agg_order_price_advantage(self, inner_order_indicators, base_price="twap"): def _agg_order_price_advantage(
base_price = base_price.lower() self,
volume = pd.Series() inner_order_indicators: List[Dict[str, pd.Series]],
for _order_indicator in inner_order_indicators: trade_start_time: pd.Timestamp,
volume = volume.add(_order_indicator["volume"], fill_value=0) trade_end_time: pd.Timestamp,
self.order_indicator["volume"] = volume trade_exchange: Exchange,
pa_config: dict = {},
):
"""
if base_price == "twap": Parameters
base_price = pd.Series() ----------
price_count = pd.Series() inner_order_indicators : List[Dict[str, pd.Series]]
for _order_indicator in inner_order_indicators: the indicators of account of inner executor
base_price = base_price.add(_order_indicator["base_price"], fill_value=0) trade_start_time : pd.Timestamp
price_count = price_count.add(pd.Series(1, index=_order_indicator["base_price"].index), fill_value=0) the start_time of the trade period, for slicing
base_price /= price_count trade_end_time : pd.Timestamp
self.order_indicator["base_price"] = base_price the end_time of the trade period, for slicing (so it may include more time at the end)
trade_exchange : Exchange
for retrieving trading price
pa_config : dict
For example
{
"agg": "twap", # "vwap"
"price": "$close", # TODO: this is not supported now!!!!!
# default to use deal price of the exchange
}
"""
elif base_price == "vwap": agg = pa_config.get("agg", "twap").lower()
base_price = pd.Series() price = pa_config.get("price", "deal_price").lower()
for _order_indicator in inner_order_indicators:
base_price = base_price.add(_order_indicator["base_price"] * _order_indicator["volume"], fill_value=0)
base_price /= self.order_indicator["volume"]
self.order_indicator["base_price"] = base_price
else: base_price = {}
raise ValueError(f"base_price {base_price} is not supported!") for inst, dir in self.order_indicator["trade_dir"].items():
self.order_indicator["pa"] = self.order_indicator["trade_price"] / self.order_indicator["base_price"] - 1 if price == "deal_price":
# print("trade_price", self.order_indicator["trade_price"], "base_price", self.order_indicator["base_price"], "pa", self.order_indicator["pa"]* (2 * (self.order_indicator["amount"] < 0).astype(int) - 1)) price_s = trade_exchange.get_deal_price(inst, trade_start_time, trade_end_time, dir, method=None)
else:
raise NotImplementedError(f"This type of input is not supported")
# there are some zeros in the trading price. These cases are known meaningless
price_s = price_s.mask(np.isclose(price_s, 0))
if agg == "vwap":
volume_s = trade_exchange.get_volume(inst, trade_start_time, trade_end_time, method=None)
base_price[inst] = ((price_s * volume_s).sum() / volume_s.sum()).item()
elif agg == "twap":
base_price[inst] = price_s.mean().item()
base_price = pd.Series(base_price)
# update PA
self.order_indicator["pa"] = self.order_indicator["trade_price"] / base_price - 1
def _cal_trade_fulfill_rate(self, method="mean"): def _cal_trade_fulfill_rate(self, method="mean"):
if method == "mean": if method == "mean":
@@ -372,19 +427,27 @@ class Indicator:
def _cal_trade_order_count(self): def _cal_trade_order_count(self):
return self.order_indicator["amount"].count() return self.order_indicator["amount"].count()
def update_order_indicators(self, trade_start_time, trade_end_time, trade_info, trade_exchange): def update_order_indicators(self, trade_info: list):
self._update_order_trade_info(trade_info=trade_info) self._update_order_trade_info(trade_info=trade_info)
self._update_order_fulfill_rate() self._update_order_fulfill_rate()
self._update_order_price_advantage(trade_exchange, trade_start_time, trade_end_time) self._update_order_price_advantage()
def agg_order_indicators( def agg_order_indicators(
self, inner_order_indicators, outer_trade_decision: BaseTradeDecision, indicator_config={} self,
trade_start_time,
trade_end_time,
inner_order_indicators: List[Dict[str, pd.Series]],
outer_trade_decision: BaseTradeDecision,
trade_exchange: Exchange,
indicator_config={},
): ):
self._agg_order_trade_info(inner_order_indicators) self._agg_order_trade_info(inner_order_indicators)
self._update_trade_amount(outer_trade_decision) self._update_trade_amount(outer_trade_decision)
self._agg_order_fulfill_rate() self._agg_order_fulfill_rate()
pa_config = indicator_config.get("pa_config", {}) pa_config = indicator_config.get("pa_config", {})
self._agg_order_price_advantage(inner_order_indicators, base_price=pa_config.get("base_price", "twap")) self._agg_order_price_advantage(
inner_order_indicators, trade_start_time, trade_end_time, trade_exchange, pa_config=pa_config
)
def cal_trade_indicators(self, trade_start_time, freq, indicator_config={}): def cal_trade_indicators(self, trade_start_time, freq, indicator_config={}):
show_indicator = indicator_config.get("show_indicator", False) show_indicator = indicator_config.get("show_indicator", False)