1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-03 02:50:58 +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
register it in qlib_init.
- "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,
so it can be directly used as a capacity limit.
e.g. ("cum", "0.2 * DayCumsum($volume, '9:45', '14:45')"), ("current", "$bidV1")
@@ -304,10 +304,10 @@ class Exchange:
if isinstance(volume_threshold, tuple):
volume_threshold = {"all": volume_threshold}
assert type(volume_threshold) == dict
assert isinstance(volume_threshold, dict)
for key in volume_threshold:
vol_limit = volume_threshold[key]
assert type(vol_limit) == tuple
assert isinstance(vol_limit, tuple)
fields.add(vol_limit[1])
if key in ("buy", "all"):
@@ -370,7 +370,7 @@ class Exchange:
order,
trade_account: Account = None,
position: BasePosition = None,
dealed_order_amount: defaultdict = defaultdict(float),
dealt_order_amount: defaultdict = defaultdict(float),
):
"""
Deal order when the actual transaction
@@ -380,7 +380,7 @@ class Exchange:
:param order: Deal 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 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
"""
# 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)
# NOTE: order will be changed in this function
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 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
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.
Parameters
----------
order : Order
the order to be executed.
dealed_order_amount : dict
:param dealed_order_amount: the dealed order amount dict with the format of {stock_id: float}
dealt_order_amount : dict
:param dealt_order_amount: the dealt order amount dict with the format of {stock_id: float}
Returns
-------
@@ -685,33 +685,23 @@ class Exchange:
vol_limit_num = []
for limit in vol_limit:
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":
vol_limit_num.append(
self.quote.get_data(
order.stock_id,
order.start_time,
order.end_time,
fields=limit[1],
method=ts_data_last,
)
)
vol_limit_num.append(limit_value)
elif limit[0] == "cum":
vol_limit_num.append(
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]
)
vol_limit_num.append(limit_value - dealt_order_amount[order.stock_id])
else:
raise ValueError(f"{limit[0]} is not supported")
vol_limit_num = min(vol_limit_num)
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
@@ -719,7 +709,7 @@ class Exchange:
:param order:
: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
"""
@@ -743,7 +733,7 @@ class Exchange:
# We choose to sell all
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_cost = max(trade_val * self.close_cost, self.min_cost)
elif order.direction == Order.BUY:
@@ -763,7 +753,7 @@ class Exchange:
# 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._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_cost = max(trade_val * self.open_cost, self.min_cost)
else:

View File

@@ -115,7 +115,7 @@ class BaseExecutor:
get_module_logger("BaseExecutor").warning(f"`common_infra` is not set for {self}")
# record deal order amount in one day
self.dealed_order_amount = defaultdict(float)
self.dealt_order_amount = defaultdict(float)
self.deal_day = None
def reset_common_infra(self, common_infra):
@@ -500,14 +500,14 @@ class SimulatorExecutor(BaseExecutor):
raise NotImplementedError(f"This type of input is not supported")
return order_it
def _update_dealed_order_amount(self, order):
"""update date and dealed order amount in the day."""
def _update_dealt_order_amount(self, order):
"""update date and dealt order amount in the day."""
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:
self.dealed_order_amount = defaultdict(float)
self.dealt_order_amount = defaultdict(float)
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):
@@ -520,10 +520,10 @@ class SimulatorExecutor(BaseExecutor):
trade_val, trade_cost, trade_price = self.trade_exchange.deal_order(
order,
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))
self._update_dealed_order_amount(order)
self._update_dealt_order_amount(order)
if self.verbose:
print(
"[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.data import Cal
from qlib.data.ops import ElemOperator
from qlib.utils.time import time_to_day_index
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_close = datetime.strptime("15:00", "%H:%M")
self.start_id = self.time_to_index(self.start)
self.end_id = self.time_to_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")
self.start_id = time_to_day_index(self.start)
self.end_id = time_to_day_index(self.end)
def period_cusum(self, df):
df = df.copy()
assert len(df) == 240
df.iloc[0 : self.start_id] = 0
df = df.cumsum()

View File

@@ -11,6 +11,7 @@ from numpy import append
import pandas as pd
from qlib.config import C
import functools
from typing import Union
@functools.lru_cache(maxsize=240)
@@ -96,6 +97,29 @@ class 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]:
"""
get the min-bar index in a day for a time range (both left and right is closed) given a fixed frequency