1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-13 15:56:57 +08:00

fix vol limit bug

This commit is contained in:
wangwenxi.handsome
2021-08-02 03:49:03 +00:00
parent 0f2d85d098
commit f5db0e1b05
4 changed files with 57 additions and 49 deletions

View File

@@ -84,7 +84,7 @@ class Exchange:
such as DayCumsum. !!!NOTE: if you want you use the custom operator, you need to such as DayCumsum. !!!NOTE: if you want you use the custom operator, you need to
register it in qlib_init. register it in qlib_init.
- "cum" means that this is a cumulative value over time, such as cumulative market volume. - "cum" means that this is a cumulative value over time, such as cumulative market volume.
So when it is used as a volume limit, it is necessary to subtract the dealed amount. So when it is used as a volume limit, it is necessary to subtract the dealt amount.
- "current" means that this is a real-time value and will not accumulate over time, - "current" means that this is a real-time value and will not accumulate over time,
so it can be directly used as a capacity limit. so it can be directly used as a capacity limit.
e.g. ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"), ("current", "$bidV1") e.g. ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"), ("current", "$bidV1")
@@ -304,10 +304,10 @@ class Exchange:
if isinstance(volume_threshold, tuple): if isinstance(volume_threshold, tuple):
volume_threshold = {"all": volume_threshold} volume_threshold = {"all": volume_threshold}
assert type(volume_threshold) == dict assert isinstance(volume_threshold, dict)
for key in volume_threshold: for key in volume_threshold:
vol_limit = volume_threshold[key] vol_limit = volume_threshold[key]
assert type(vol_limit) == tuple assert isinstance(vol_limit, tuple)
fields.add(vol_limit[1]) fields.add(vol_limit[1])
if key in ("buy", "all"): if key in ("buy", "all"):
@@ -370,7 +370,7 @@ class Exchange:
order, order,
trade_account: Account = None, trade_account: Account = None,
position: BasePosition = None, position: BasePosition = None,
dealed_order_amount: defaultdict = defaultdict(float), dealt_order_amount: defaultdict = defaultdict(float),
): ):
""" """
Deal order when the actual transaction Deal order when the actual transaction
@@ -380,7 +380,7 @@ class Exchange:
:param order: Deal the order. :param order: Deal the order.
:param trade_account: Trade account to be updated after dealing the order. :param trade_account: Trade account to be updated after dealing the order.
:param position: position to be updated after dealing the order. :param position: position to be updated after dealing the order.
:param dealed_order_amount: the dealed order amount dict with the format of {stock_id: float} :param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float}
:return: trade_val, trade_cost, trade_price :return: trade_val, trade_cost, trade_price
""" """
# check order first. # check order first.
@@ -395,7 +395,7 @@ class Exchange:
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, order.direction) trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, order.direction)
# NOTE: order will be changed in this function # NOTE: order will be changed in this function
trade_val, trade_cost = self._calc_trade_info_by_order( trade_val, trade_cost = self._calc_trade_info_by_order(
order, trade_account.current if trade_account else position, dealed_order_amount order, trade_account.current if trade_account else position, dealt_order_amount
) )
if order.deal_amount > 1e-5: if order.deal_amount > 1e-5:
# If the order can only be deal 0 amount. Nothing to be updated # If the order can only be deal 0 amount. Nothing to be updated
@@ -659,15 +659,15 @@ class Exchange:
return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor return (deal_amount * factor + 0.1) // self.trade_unit * self.trade_unit / factor
return deal_amount return deal_amount
def _get_amount_by_volume(self, order: Order, dealed_order_amount: dict) -> int: def _get_amount_by_volume(self, order: Order, dealt_order_amount: dict) -> int:
"""parse the capacity limit string and return the actual amount of orders that can be executed. """parse the capacity limit string and return the actual amount of orders that can be executed.
Parameters Parameters
---------- ----------
order : Order order : Order
the order to be executed. the order to be executed.
dealed_order_amount : dict dealt_order_amount : dict
:param dealed_order_amount: the dealed order amount dict with the format of {stock_id: float} :param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float}
Returns Returns
------- -------
@@ -685,33 +685,23 @@ class Exchange:
vol_limit_num = [] vol_limit_num = []
for limit in vol_limit: for limit in vol_limit:
assert isinstance(limit, tuple) assert isinstance(limit, tuple)
limit_value = self.quote.get_data(
order.stock_id,
order.start_time,
order.end_time,
fields=limit[1],
method=ts_data_last,
)
if limit[0] == "current": if limit[0] == "current":
vol_limit_num.append( vol_limit_num.append(limit_value)
self.quote.get_data(
order.stock_id,
order.start_time,
order.end_time,
fields=limit[1],
method=ts_data_last,
)
)
elif limit[0] == "cum": elif limit[0] == "cum":
vol_limit_num.append( vol_limit_num.append(limit_value - dealt_order_amount[order.stock_id])
self.quote.get_data(
order.stock_id,
order.start_time,
order.end_time,
fields=limit[1],
method=ts_data_last,
)
- dealed_order_amount[order.stock_id]
)
else: else:
raise ValueError(f"{limit[0]} is not supported") raise ValueError(f"{limit[0]} is not supported")
vol_limit_num = min(vol_limit_num) vol_limit_num = min(vol_limit_num)
return max(min(vol_limit_num, order.deal_amount), 0) return max(min(vol_limit_num, order.deal_amount), 0)
def _calc_trade_info_by_order(self, order, position: Position, dealed_order_amount): def _calc_trade_info_by_order(self, order, position: Position, dealt_order_amount):
""" """
Calculation of trade info Calculation of trade info
@@ -719,7 +709,7 @@ class Exchange:
:param order: :param order:
:param position: Position :param position: Position
:param dealed_order_amount: the dealed order amount dict with the format of {stock_id: float} :param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float}
:return: trade_val, trade_cost :return: trade_val, trade_cost
""" """
@@ -743,7 +733,7 @@ class Exchange:
# We choose to sell all # We choose to sell all
order.deal_amount = order.amount order.deal_amount = order.amount
order.deal_amount = self._get_amount_by_volume(order, dealed_order_amount) order.deal_amount = self._get_amount_by_volume(order, dealt_order_amount)
trade_val = order.deal_amount * trade_price trade_val = order.deal_amount * trade_price
trade_cost = max(trade_val * self.close_cost, self.min_cost) trade_cost = max(trade_val * self.close_cost, self.min_cost)
elif order.direction == Order.BUY: elif order.direction == Order.BUY:
@@ -763,7 +753,7 @@ class Exchange:
# Unknown amount of money. Just round the amount # Unknown amount of money. Just round the amount
order.deal_amount = self.round_amount_by_trade_unit(order.amount, order.factor) order.deal_amount = self.round_amount_by_trade_unit(order.amount, order.factor)
order.deal_amount = self._get_amount_by_volume(order, dealed_order_amount) order.deal_amount = self._get_amount_by_volume(order, dealt_order_amount)
trade_val = order.deal_amount * trade_price trade_val = order.deal_amount * trade_price
trade_cost = max(trade_val * self.open_cost, self.min_cost) trade_cost = max(trade_val * self.open_cost, self.min_cost)
else: else:

View File

@@ -115,7 +115,7 @@ class BaseExecutor:
get_module_logger("BaseExecutor").warning(f"`common_infra` is not set for {self}") get_module_logger("BaseExecutor").warning(f"`common_infra` is not set for {self}")
# record deal order amount in one day # record deal order amount in one day
self.dealed_order_amount = defaultdict(float) self.dealt_order_amount = defaultdict(float)
self.deal_day = None self.deal_day = None
def reset_common_infra(self, common_infra): def reset_common_infra(self, common_infra):
@@ -500,14 +500,14 @@ class SimulatorExecutor(BaseExecutor):
raise NotImplementedError(f"This type of input is not supported") raise NotImplementedError(f"This type of input is not supported")
return order_it return order_it
def _update_dealed_order_amount(self, order): def _update_dealt_order_amount(self, order):
"""update date and dealed order amount in the day.""" """update date and dealt order amount in the day."""
now_deal_day = self.trade_calendar.get_step_time()[0].floor(freq="D") now_deal_day = self.trade_calendar.get_step_time()[0].floor(freq="D")
if self.deal_day is None or now_deal_day > self.deal_day: if self.deal_day is None or now_deal_day > self.deal_day:
self.dealed_order_amount = defaultdict(float) self.dealt_order_amount = defaultdict(float)
self.deal_day = now_deal_day self.deal_day = now_deal_day
self.dealed_order_amount[order.stock_id] += order.deal_amount self.dealt_order_amount[order.stock_id] += order.deal_amount
def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0): def _collect_data(self, trade_decision: BaseTradeDecision, level: int = 0):
@@ -520,10 +520,10 @@ class SimulatorExecutor(BaseExecutor):
trade_val, trade_cost, trade_price = self.trade_exchange.deal_order( trade_val, trade_cost, trade_price = self.trade_exchange.deal_order(
order, order,
trade_account=self.trade_account, trade_account=self.trade_account,
dealed_order_amount=self.dealed_order_amount, dealt_order_amount=self.dealt_order_amount,
) )
execute_result.append((order, trade_val, trade_cost, trade_price)) execute_result.append((order, trade_val, trade_cost, trade_price))
self._update_dealed_order_amount(order) self._update_dealt_order_amount(order)
if self.verbose: if self.verbose:
print( print(
"[I {:%Y-%m-%d %H:%M:%S}]: {} {}, price {:.2f}, amount {}, deal_amount {}, factor {}, value {:.2f}, cash {:.2f}.".format( "[I {:%Y-%m-%d %H:%M:%S}]: {} {}, price {:.2f}, amount {}, deal_amount {}, factor {}, value {:.2f}, cash {:.2f}.".format(

View File

@@ -10,6 +10,7 @@ from qlib.data import D
from qlib.data.cache import H from qlib.data.cache import H
from qlib.data.data import Cal from qlib.data.data import Cal
from qlib.data.ops import ElemOperator from qlib.data.ops import ElemOperator
from qlib.utils.time import time_to_day_index
def get_calendar_day(freq="1min", future=False): def get_calendar_day(freq="1min", future=False):
@@ -71,18 +72,11 @@ class DayCumsum(ElemOperator):
self.noon_open = datetime.strptime("13:00", "%H:%M") self.noon_open = datetime.strptime("13:00", "%H:%M")
self.noon_close = datetime.strptime("15:00", "%H:%M") self.noon_close = datetime.strptime("15:00", "%H:%M")
self.start_id = self.time_to_index(self.start) self.start_id = time_to_day_index(self.start)
self.end_id = self.time_to_index(self.end) self.end_id = time_to_day_index(self.end)
def time_to_index(self, t):
if t >= self.morning_open and t < self.morning_close:
return int((t - self.morning_open).total_seconds() / 60)
elif t >= self.noon_open and t < self.noon_close:
return int((t - self.noon_open).total_seconds() / 60) + 120
else:
raise ValueError(f"{t} is not the opening time of the stock market")
def period_cusum(self, df): def period_cusum(self, df):
df = df.copy()
assert len(df) == 240 assert len(df) == 240
df.iloc[0 : self.start_id] = 0 df.iloc[0 : self.start_id] = 0
df = df.cumsum() df = df.cumsum()

View File

@@ -11,6 +11,7 @@ from numpy import append
import pandas as pd import pandas as pd
from qlib.config import C from qlib.config import C
import functools import functools
from typing import Union
@functools.lru_cache(maxsize=240) @functools.lru_cache(maxsize=240)
@@ -96,6 +97,29 @@ class Freq:
return _count, _freq_format_dict[_freq] return _count, _freq_format_dict[_freq]
cn_time = [datetime.strptime("9:30", "%H:%M"), datetime.strptime("11:30", "%H:%M"),
datetime.strptime("13:00", "%H:%M"), datetime.strptime("15:00", "%H:%M")]
us_time = [datetime.strptime("9:30", "%H:%M"), datetime.strptime("16:00", "%H:%M")]
def time_to_day_index(time_obj: Union[str, datetime], region: str="cn"):
if isinstance(time_obj, str):
time_obj = datetime.strptime(time_obj, "%H:%M")
if region == "cn":
if time_obj >= cn_time[0] and time_obj < cn_time[1]:
return int((time_obj - cn_time[0]).total_seconds() / 60)
elif time_obj >= cn_time[2] and time_obj < cn_time[3]:
return int((time_obj - cn_time[2]).total_seconds() / 60) + 120
else:
raise ValueError(f"{time_obj} is not the opening time of the {region} stock market")
elif region == "us":
if time_obj >= us_time[0] and time_obj < us_time[1]:
return int((time_obj - us_time[0]).total_seconds() / 60)
else:
raise ValueError(f"{time_obj} is not the opening time of the {region} stock market")
else:
raise ValueError(f"{region} is not supported")
def get_day_min_idx_range(start: str, end: str, freq: str) -> Tuple[int, int]: def get_day_min_idx_range(start: str, end: str, freq: str) -> Tuple[int, int]:
""" """
get the min-bar index in a day for a time range (both left and right is closed) given a fixed frequency get the min-bar index in a day for a time range (both left and right is closed) given a fixed frequency