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