mirror of
https://github.com/microsoft/qlib.git
synced 2026-07-11 23:06:58 +08:00
supporting seperated buy and sell price
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# Licensed under the MIT License.
|
# Licensed under the MIT License.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import copy
|
import copy
|
||||||
from typing import Union, TYPE_CHECKING
|
from typing import List, Tuple, Union, TYPE_CHECKING
|
||||||
|
|
||||||
from .account import Account
|
from .account import Account
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ def get_exchange(
|
|||||||
min_cost=5.0,
|
min_cost=5.0,
|
||||||
trade_unit=None,
|
trade_unit=None,
|
||||||
limit_threshold=None,
|
limit_threshold=None,
|
||||||
deal_price=None,
|
deal_price: Union[str, Tuple[str], List[str]] = None,
|
||||||
):
|
):
|
||||||
"""get_exchange
|
"""get_exchange
|
||||||
|
|
||||||
@@ -54,8 +54,15 @@ def get_exchange(
|
|||||||
min transaction cost.
|
min transaction cost.
|
||||||
trade_unit : int
|
trade_unit : int
|
||||||
100 for China A.
|
100 for China A.
|
||||||
deal_price: str
|
deal_price: Union[str, Tuple[str], List[str]]
|
||||||
dealing price type: 'close', 'open', 'vwap'.
|
The `deal_price` supports following two types of input
|
||||||
|
- <deal_price> : str
|
||||||
|
- (<buy_price>, <sell_price>): Tuple[str] or List[str]
|
||||||
|
|
||||||
|
<deal_price>, <buy_price> or <sell_price> := <price>
|
||||||
|
<price> := str
|
||||||
|
- for example '$close', '$open', '$vwap' ("close" is OK. `Exchange` will help to prepend
|
||||||
|
"$" to the expression)
|
||||||
limit_threshold : float
|
limit_threshold : float
|
||||||
limit move 0.1 (10%) for example, long and short with same limit.
|
limit move 0.1 (10%) for example, long and short with same limit.
|
||||||
|
|
||||||
@@ -69,13 +76,8 @@ def get_exchange(
|
|||||||
trade_unit = C.trade_unit
|
trade_unit = C.trade_unit
|
||||||
if limit_threshold is None:
|
if limit_threshold is None:
|
||||||
limit_threshold = C.limit_threshold
|
limit_threshold = C.limit_threshold
|
||||||
if deal_price is None:
|
|
||||||
deal_price = C.deal_price
|
|
||||||
if exchange is None:
|
if exchange is None:
|
||||||
logger.info("Create new exchange")
|
logger.info("Create new exchange")
|
||||||
# handle exception for deal_price
|
|
||||||
if deal_price[0] != "$":
|
|
||||||
deal_price = "$" + deal_price
|
|
||||||
|
|
||||||
exchange = Exchange(
|
exchange = Exchange(
|
||||||
freq=freq,
|
freq=freq,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
from typing import Union
|
from typing import List, Tuple, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -24,7 +24,7 @@ class Exchange:
|
|||||||
start_time=None,
|
start_time=None,
|
||||||
end_time=None,
|
end_time=None,
|
||||||
codes="all",
|
codes="all",
|
||||||
deal_price=None,
|
deal_price: Union[str, Tuple[str], List[str]] = None,
|
||||||
subscribe_fields=[],
|
subscribe_fields=[],
|
||||||
limit_threshold=None,
|
limit_threshold=None,
|
||||||
volume_threshold=None,
|
volume_threshold=None,
|
||||||
@@ -40,7 +40,17 @@ class Exchange:
|
|||||||
:param start_time: closed start time for backtest
|
:param start_time: closed start time for backtest
|
||||||
:param end_time: closed end time for backtest
|
:param end_time: closed end time for backtest
|
||||||
:param codes: list stock_id list or a string of instruments(i.e. all, csi500, sse50)
|
:param codes: list stock_id list or a string of instruments(i.e. all, csi500, sse50)
|
||||||
:param deal_price: str, 'close', 'open', 'vwap'
|
|
||||||
|
:param deal_price: Union[str, Tuple[str], List[str]]
|
||||||
|
The `deal_price` supports following two types of input
|
||||||
|
- <deal_price> : str
|
||||||
|
- (<buy_price>, <sell_price>): Tuple[str] or List[str]
|
||||||
|
|
||||||
|
<deal_price>, <buy_price> or <sell_price> := <price>
|
||||||
|
<price> := str
|
||||||
|
- for example '$close', '$open', '$vwap' ("close" is OK. `Exchange` will help to prepend
|
||||||
|
"$" to the expression)
|
||||||
|
|
||||||
:param subscribe_fields: list, subscribe fields
|
:param subscribe_fields: list, subscribe fields
|
||||||
:param limit_threshold: float, 0.1 for example, default None
|
:param limit_threshold: float, 0.1 for example, default None
|
||||||
:param volume_threshold: float, 0.1 for example, default None
|
:param volume_threshold: float, 0.1 for example, default None
|
||||||
@@ -86,10 +96,15 @@ class Exchange:
|
|||||||
if C.region == REG_CN:
|
if C.region == REG_CN:
|
||||||
self.logger.warning(f"limit_threshold may not be set to a reasonable value")
|
self.logger.warning(f"limit_threshold may not be set to a reasonable value")
|
||||||
|
|
||||||
if deal_price[0] != "$":
|
if isinstance(deal_price, str):
|
||||||
self.deal_price = "$" + deal_price
|
if deal_price[0] != "$":
|
||||||
|
deal_price = "$" + deal_price
|
||||||
|
self.buy_price = self.sell_price = deal_price
|
||||||
|
elif isinstance(deal_price, (tuple, list)):
|
||||||
|
self.buy_price, self.sell_price = deal_price
|
||||||
else:
|
else:
|
||||||
self.deal_price = deal_price
|
raise NotImplementedError(f"This type of input is not supported")
|
||||||
|
|
||||||
if isinstance(codes, str):
|
if isinstance(codes, str):
|
||||||
codes = D.instruments(codes)
|
codes = D.instruments(codes)
|
||||||
self.codes = codes
|
self.codes = codes
|
||||||
@@ -98,7 +113,7 @@ class Exchange:
|
|||||||
# $factor is for rounding to the trading unit
|
# $factor is for rounding to the trading unit
|
||||||
# $change is for calculating the limit of the stock
|
# $change is for calculating the limit of the stock
|
||||||
|
|
||||||
necessary_fields = {self.deal_price, "$close", "$change", "$factor", "$volume"}
|
necessary_fields = {self.buy_price, self.sell_price, "$close", "$change", "$factor", "$volume"}
|
||||||
subscribe_fields = list(necessary_fields | set(subscribe_fields))
|
subscribe_fields = list(necessary_fields | set(subscribe_fields))
|
||||||
all_fields = list(necessary_fields | set(subscribe_fields))
|
all_fields = list(necessary_fields | set(subscribe_fields))
|
||||||
self.all_fields = all_fields
|
self.all_fields = all_fields
|
||||||
@@ -118,8 +133,10 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
self.quote.columns = self.all_fields
|
self.quote.columns = self.all_fields
|
||||||
|
|
||||||
if self.quote[self.deal_price].isna().any():
|
for attr in "buy_price", "sell_price":
|
||||||
self.logger.warning("{} field data contains nan.".format(self.deal_price))
|
pstr = getattr(self, attr) # price string
|
||||||
|
if self.quote[pstr].isna().any():
|
||||||
|
self.logger.warning("{} field data contains nan.".format(pstr))
|
||||||
|
|
||||||
if self.quote["$factor"].isna().any():
|
if self.quote["$factor"].isna().any():
|
||||||
# The 'factor.day.bin' file not exists, and `factor` field contains `nan`
|
# The 'factor.day.bin' file not exists, and `factor` field contains `nan`
|
||||||
@@ -148,9 +165,11 @@ class Exchange:
|
|||||||
# process extra_quote
|
# process extra_quote
|
||||||
if "$close" not in self.extra_quote:
|
if "$close" not in self.extra_quote:
|
||||||
raise ValueError("$close is necessray in extra_quote")
|
raise ValueError("$close is necessray in extra_quote")
|
||||||
if self.deal_price not in self.extra_quote.columns:
|
for attr in "buy_price", "sell_price":
|
||||||
self.extra_quote[self.deal_price] = self.extra_quote["$close"]
|
pstr = getattr(self, attr) # price string
|
||||||
self.logger.warning("No deal_price set for extra_quote. Use $close as deal_price.")
|
if pstr not in self.extra_quote.columns:
|
||||||
|
self.extra_quote[pstr] = self.extra_quote["$close"]
|
||||||
|
self.logger.warning(f"No {pstr} set for extra_quote. Use $close as {pstr}.")
|
||||||
if "$factor" not in self.extra_quote.columns:
|
if "$factor" not in self.extra_quote.columns:
|
||||||
self.extra_quote["$factor"] = 1.0
|
self.extra_quote["$factor"] = 1.0
|
||||||
self.logger.warning("No $factor set for extra_quote. Use 1.0 as $factor.")
|
self.logger.warning("No $factor set for extra_quote. Use 1.0 as $factor.")
|
||||||
@@ -241,7 +260,7 @@ class Exchange:
|
|||||||
if trade_account is not None and position is not None:
|
if trade_account is not None and position is not None:
|
||||||
raise ValueError("trade_account and position can only choose one")
|
raise ValueError("trade_account and position can only choose one")
|
||||||
|
|
||||||
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time)
|
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
|
order, trade_account.current if trade_account else position
|
||||||
@@ -266,12 +285,16 @@ class Exchange:
|
|||||||
def get_volume(self, stock_id, start_time, end_time):
|
def get_volume(self, stock_id, start_time, end_time):
|
||||||
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="sum")
|
||||||
|
|
||||||
def get_deal_price(self, stock_id, start_time, end_time):
|
def get_deal_price(self, stock_id, start_time, end_time, direction: OrderDir):
|
||||||
deal_price = resam_ts_data(self.quote[stock_id][self.deal_price], start_time, end_time, method=ts_data_last)
|
if direction == OrderDir.SELL:
|
||||||
|
pstr = self.sell_price
|
||||||
|
elif direction == OrderDir.BUY:
|
||||||
|
pstr = self.buy_price
|
||||||
|
else:
|
||||||
|
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)
|
||||||
if np.isclose(deal_price, 0.0) or np.isnan(deal_price):
|
if np.isclose(deal_price, 0.0) or np.isnan(deal_price):
|
||||||
self.logger.warning(
|
self.logger.warning(f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {pstr}): {deal_price}!!!")
|
||||||
f"(stock_id:{stock_id}, trade_time:{(start_time, end_time)}, {self.deal_price}): {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)
|
||||||
return deal_price
|
return deal_price
|
||||||
@@ -288,7 +311,9 @@ class Exchange:
|
|||||||
return None
|
return None
|
||||||
return resam_ts_data(self.quote[stock_id]["$factor"], start_time, end_time, method=ts_data_last)
|
return resam_ts_data(self.quote[stock_id]["$factor"], start_time, end_time, method=ts_data_last)
|
||||||
|
|
||||||
def generate_amount_position_from_weight_position(self, weight_position, cash, start_time, end_time):
|
def generate_amount_position_from_weight_position(
|
||||||
|
self, weight_position, cash, start_time, end_time, direction=OrderDir.BUY
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
The generate the target position according to the weight and the cash.
|
The generate the target position according to the weight and the cash.
|
||||||
NOTE: All the cash will assigned to the tadable stock.
|
NOTE: All the cash will assigned to the tadable stock.
|
||||||
@@ -297,7 +322,10 @@ class Exchange:
|
|||||||
weight_position : dict {stock_id : weight}; allocate cash by weight_position
|
weight_position : dict {stock_id : weight}; allocate cash by weight_position
|
||||||
among then, weight must be in this range: 0 < weight < 1
|
among then, weight must be in this range: 0 < weight < 1
|
||||||
cash : cash
|
cash : cash
|
||||||
trade_date : trade date
|
start_time : the start time point of the step
|
||||||
|
end_time : the end time point of the step
|
||||||
|
direction : the direction of the deal price for estimating the amount
|
||||||
|
# NOTE: this function is used for calculating target position. So the default direction is buy
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# calculate the total weight of tradable value
|
# calculate the total weight of tradable value
|
||||||
@@ -324,7 +352,9 @@ class Exchange:
|
|||||||
cash
|
cash
|
||||||
* weight_position[stock_id]
|
* weight_position[stock_id]
|
||||||
/ tradable_weight
|
/ tradable_weight
|
||||||
// self.get_deal_price(stock_id=stock_id, start_time=start_time, end_time=end_time)
|
// self.get_deal_price(
|
||||||
|
stock_id=stock_id, start_time=start_time, end_time=end_time, direction=direction
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return amount_dict
|
return amount_dict
|
||||||
|
|
||||||
@@ -414,10 +444,16 @@ class Exchange:
|
|||||||
# return order_list : buy + sell
|
# return order_list : buy + sell
|
||||||
return sell_order_list + buy_order_list
|
return sell_order_list + buy_order_list
|
||||||
|
|
||||||
def calculate_amount_position_value(self, amount_dict, start_time, end_time, only_tradable=False):
|
def calculate_amount_position_value(
|
||||||
|
self, amount_dict, start_time, end_time, only_tradable=False, direction=OrderDir.SELL
|
||||||
|
):
|
||||||
"""Parameter
|
"""Parameter
|
||||||
position : Position()
|
position : Position()
|
||||||
amount_dict : {stock_id : amount}
|
amount_dict : {stock_id : amount}
|
||||||
|
direction : the direction of the deal price for estimating the amount
|
||||||
|
# NOTE:
|
||||||
|
This function is used for calculating current position value.
|
||||||
|
So the default direction is sell.
|
||||||
"""
|
"""
|
||||||
value = 0
|
value = 0
|
||||||
for stock_id in amount_dict:
|
for stock_id in amount_dict:
|
||||||
@@ -426,7 +462,9 @@ class Exchange:
|
|||||||
and self.check_stock_limit(stock_id=stock_id, start_time=start_time, end_time=end_time) is False
|
and self.check_stock_limit(stock_id=stock_id, start_time=start_time, end_time=end_time) is False
|
||||||
):
|
):
|
||||||
value += (
|
value += (
|
||||||
self.get_deal_price(stock_id=stock_id, start_time=start_time, end_time=end_time)
|
self.get_deal_price(
|
||||||
|
stock_id=stock_id, start_time=start_time, end_time=end_time, direction=direction
|
||||||
|
)
|
||||||
* amount_dict[stock_id]
|
* amount_dict[stock_id]
|
||||||
)
|
)
|
||||||
return value
|
return value
|
||||||
@@ -466,7 +504,7 @@ class Exchange:
|
|||||||
:return: trade_val, trade_cost
|
:return: trade_val, trade_cost
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time)
|
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, direction=order.direction)
|
||||||
if order.direction == Order.SELL:
|
if order.direction == Order.SELL:
|
||||||
# sell
|
# sell
|
||||||
if position is not None:
|
if position is not None:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from ...utils.resam import resam_ts_data
|
from ...utils.resam import resam_ts_data
|
||||||
from ...strategy.base import ModelStrategy
|
from ...strategy.base import ModelStrategy
|
||||||
from ...backtest.order import Order, BaseTradeDecision, TradeDecisionWO
|
from ...backtest.order import Order, BaseTradeDecision, OrderDir, TradeDecisionWO
|
||||||
|
|
||||||
from .order_generator import OrderGenWInteract
|
from .order_generator import OrderGenWInteract
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ class TopkDropoutStrategy(ModelStrategy):
|
|||||||
continue
|
continue
|
||||||
# buy order
|
# buy order
|
||||||
buy_price = self.trade_exchange.get_deal_price(
|
buy_price = self.trade_exchange.get_deal_price(
|
||||||
stock_id=code, start_time=trade_start_time, end_time=trade_end_time
|
stock_id=code, start_time=trade_start_time, end_time=trade_end_time, direction=OrderDir.BUY
|
||||||
)
|
)
|
||||||
buy_amount = value / buy_price
|
buy_amount = value / buy_price
|
||||||
factor = self.trade_exchange.get_factor(stock_id=code, start_time=trade_start_time, end_time=trade_end_time)
|
factor = self.trade_exchange.get_factor(stock_id=code, start_time=trade_start_time, end_time=trade_end_time)
|
||||||
|
|||||||
Reference in New Issue
Block a user