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:
@@ -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)
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user