1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-16 01:06:56 +08:00

Cash Update (#559)

* fix negative cash

* update order test

* fix bug

* update file_order_test
This commit is contained in:
wangwenxi-handsome
2021-08-12 23:44:22 +08:00
committed by GitHub
parent 05b9fb5a47
commit 735153a50d
4 changed files with 87 additions and 209 deletions

View File

@@ -165,24 +165,25 @@ class Account:
def get_cash(self): def get_cash(self):
return self.current.get_cash() return self.current.get_cash()
def _update_state_from_order(self, order, trade_val, cost, trade_price): def _update_accum_info_from_order(self, order, trade_val, cost, trade_price):
# update turnover if self.is_port_metr_enabled():
self.accum_info.add_turnover(trade_val) # update turnover
# update cost self.accum_info.add_turnover(trade_val)
self.accum_info.add_cost(cost) # update cost
self.accum_info.add_cost(cost)
# update return from order # update return from order
trade_amount = trade_val / trade_price trade_amount = trade_val / trade_price
if order.direction == Order.SELL: # 0 for sell if order.direction == Order.SELL: # 0 for sell
# when sell stock, get profit from price change # when sell stock, get profit from price change
profit = trade_val - self.current.get_stock_price(order.stock_id) * trade_amount profit = trade_val - self.current.get_stock_price(order.stock_id) * trade_amount
self.accum_info.add_return_value(profit) # note here do not consider cost self.accum_info.add_return_value(profit) # note here do not consider cost
elif order.direction == Order.BUY: # 1 for buy elif order.direction == Order.BUY: # 1 for buy
# when buy stock, we get return for the rtn computing method # when buy stock, we get return for the rtn computing method
# profit in buy order is to make rtn is consistent with earning at the end of bar # profit in buy order is to make rtn is consistent with earning at the end of bar
profit = self.current.get_stock_price(order.stock_id) * trade_amount - trade_val profit = self.current.get_stock_price(order.stock_id) * trade_amount - trade_val
self.accum_info.add_return_value(profit) # note here do not consider cost self.accum_info.add_return_value(profit) # note here do not consider cost
def update_order(self, order, trade_val, cost, trade_price): def update_order(self, order, trade_val, cost, trade_price):
if self.current.skip_update(): if self.current.skip_update():
@@ -195,8 +196,7 @@ class Account:
# The cost will be substracted from the cash at last. So the trading logic can ignore the cost calculation # The cost will be substracted from the cash at last. So the trading logic can ignore the cost calculation
if order.direction == Order.SELL: if order.direction == Order.SELL:
# sell stock # sell stock
if getattr(self, "accum_info") is not None: self._update_accum_info_from_order(order, trade_val, cost, trade_price)
self._update_state_from_order(order, trade_val, cost, trade_price)
# update current position # update current position
# for may sell all of stock_id # for may sell all of stock_id
self.current.update_order(order, trade_val, cost, trade_price) self.current.update_order(order, trade_val, cost, trade_price)
@@ -204,8 +204,7 @@ class Account:
# buy stock # buy stock
# deal order, then update state # deal order, then update state
self.current.update_order(order, trade_val, cost, trade_price) self.current.update_order(order, trade_val, cost, trade_price)
if getattr(self, "accum_info") is not None: self._update_accum_info_from_order(order, trade_val, cost, trade_price)
self._update_state_from_order(order, trade_val, cost, trade_price)
def update_bar_count(self): def update_bar_count(self):
"""at the end of the trading bar, update holding bar, count of stock""" """at the end of the trading bar, update holding bar, count of stock"""

View File

@@ -407,9 +407,6 @@ class Exchange:
trade_account.update_order(order=order, trade_val=trade_val, cost=trade_cost, trade_price=trade_price) trade_account.update_order(order=order, trade_val=trade_val, cost=trade_cost, trade_price=trade_price)
elif position: elif position:
position.update_order(order=order, trade_val=trade_val, cost=trade_cost, trade_price=trade_price) position.update_order(order=order, trade_val=trade_val, cost=trade_cost, trade_price=trade_price)
else:
# if dealing is not successful, the trade_cost should be zero
trade_cost = 0
return trade_val, trade_cost, trade_price return trade_val, trade_cost, trade_price
@@ -713,53 +710,31 @@ class Exchange:
f"Order clipped due to volume limitation: {order}, {[(vol, rule) for vol, rule in zip(vol_limit_num, vol_limit)]}" f"Order clipped due to volume limitation: {order}, {[(vol, rule) for vol, rule in zip(vol_limit_num, vol_limit)]}"
) )
def _get_max_amount_by_cash_limit(self, trade_price, order, position): def _get_buy_amount_by_cash_limit(self, trade_price, cash):
"""return the real order amount after cash limit. """return the real order amount after cash limit for buying.
Parameters Parameters
---------- ----------
trade_price : float trade_price : float
order : Order position : cash
position : Position
Return Return
---------- ----------
float float
the real order amount after cash limit. the real order amount after cash limit for buying.
""" """
cash = position.get_cash()
max_trade_amount = 0 max_trade_amount = 0
if cash >= self.min_cost: if cash >= self.min_cost:
if order.direction == Order.SELL: # critical_amount means the stock transaction amount when the service fee is equal to min_cost.
max_trade_amount = cash / self.close_cost / trade_price critical_amount = self.min_cost / self.open_cost + self.min_cost
elif order.direction == Order.BUY: if cash >= critical_amount:
critical_amount = self.min_cost / (self.open_cost * trade_price) # the service fee is equal to open_cost * trade_amount
critical_price = critical_amount * trade_price + self.min_cost max_trade_amount = cash / (1 + self.open_cost) / trade_price
if cash >= critical_price: else:
max_trade_amount = cash / (1 + self.open_cost) / trade_price # the service fee is equal to min_cost
else: max_trade_amount = (cash - self.min_cost) / trade_price
max_trade_amount = (cash - self.min_cost) / trade_price
return max_trade_amount return max_trade_amount
def _get_max_amount_by_stock_limit(self, order, position):
"""return the real order amount after stock amount limit.
Parameters
----------
order : Order
position : Position
Return
----------
float
the real order amount after stock amount limit.
"""
if order.direction == Order.SELL:
current_amount = position.get_stock_amount(order.stock_id) if position.check_stock(order.stock_id) else 0
return current_amount
elif order.direction == Order.BUY:
return np.inf
def _calc_trade_info_by_order(self, order, position: Position, dealt_order_amount): def _calc_trade_info_by_order(self, order, position: Position, dealt_order_amount):
""" """
Calculation of trade info Calculation of trade info
@@ -771,55 +746,60 @@ class Exchange:
:param dealt_order_amount: the dealt 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_price, trade_val, trade_cost :return: trade_price, trade_val, trade_cost
""" """
trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, direction=order.direction) trade_price = self.get_deal_price(order.stock_id, order.start_time, order.end_time, direction=order.direction)
order.factor = self.get_factor(order.stock_id, order.start_time, order.end_time) order.factor = self.get_factor(order.stock_id, order.start_time, order.end_time)
# get all limits amount
# cash limit
cash_max_amount = self._get_max_amount_by_cash_limit(trade_price, order, position)
# held stock limit
stock_max_amount = self._get_max_amount_by_stock_limit(order, position)
if order.direction == Order.SELL: if order.direction == Order.SELL:
cost_ratio = self.close_cost
# sell # sell
if position is not None: if position is not None:
if np.isclose(order.amount, stock_max_amount): current_amount = (
position.get_stock_amount(order.stock_id) if position.check_stock(order.stock_id) else 0
)
if np.isclose(order.amount, current_amount):
# when selling last stock. The amount don't need rounding # when selling last stock. The amount don't need rounding
if stock_max_amount <= cash_max_amount: order.deal_amount = order.amount
order.deal_amount = stock_max_amount
else:
order.deal_amount = self.round_amount_by_trade_unit(cash_max_amount, order.factor)
else: else:
now_trade_amount = min(order.amount, stock_max_amount) order.deal_amount = self.round_amount_by_trade_unit(min(current_amount, order.amount), order.factor)
if now_trade_amount > cash_max_amount:
self.logger.debug(f"Order clipped due to cash limitation: {order}") # in case of negative value of cash
order.deal_amount = self.round_amount_by_trade_unit( if position.get_cash() + order.deal_amount * trade_price < max(
min(now_trade_amount, cash_max_amount), order.factor order.deal_amount * trade_price * cost_ratio,
) self.min_cost,
):
order.deal_amount = 0
self.logger.debug(f"Order clipped due to cash limitation: {order}")
else: else:
# TODO: We don't know current position. # TODO: We don't know current position.
# We choose to sell all # We choose to sell all
order.deal_amount = order.amount order.deal_amount = order.amount
self._clip_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: elif order.direction == Order.BUY:
cost_ratio = self.open_cost
# buy # buy
if position is not None: if position is not None:
if order.amount > cash_max_amount: cash = position.get_cash()
trade_val = order.amount * trade_price
if cash < trade_val + max(trade_val * cost_ratio, self.min_cost):
# The money is not enough
max_buy_amount = self._get_buy_amount_by_cash_limit(trade_price, cash)
order.deal_amount = self.round_amount_by_trade_unit(max_buy_amount, order.factor)
self.logger.debug(f"Order clipped due to cash limitation: {order}") self.logger.debug(f"Order clipped due to cash limitation: {order}")
order.deal_amount = self.round_amount_by_trade_unit(min(order.amount, cash_max_amount), order.factor) else:
# The money is enough
order.deal_amount = self.round_amount_by_trade_unit(order.amount, order.factor)
else: else:
# 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)
self._clip_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: else:
raise NotImplementedError("order type {} error".format(order.type)) raise NotImplementedError("order type {} error".format(order.type))
self._clip_amount_by_volume(order, dealt_order_amount)
trade_val = order.deal_amount * trade_price
trade_cost = max(trade_val * cost_ratio, self.min_cost)
if trade_val <= 1e-5:
# if dealing is not successful, the trade_cost should be zero.
trade_cost = 0
return trade_price, trade_val, trade_cost return trade_price, trade_val, trade_cost
def get_order_helper(self) -> OrderHelper: def get_order_helper(self) -> OrderHelper:

View File

@@ -16,6 +16,8 @@ class FileStrTest(TestAutoData):
EXAMPLE_FILE = DIRNAME / "order_example.csv" EXAMPLE_FILE = DIRNAME / "order_example.csv"
DEAL_NUM_FOR_1000 = 123.47105436976445
def _gen_orders(self) -> pd.DataFrame: def _gen_orders(self) -> pd.DataFrame:
headers = [ headers = [
"datetime", "datetime",
@@ -24,11 +26,18 @@ class FileStrTest(TestAutoData):
"direction", "direction",
] ]
orders = [ orders = [
["20200102", self.TEST_INST, "1000", "sell"], # test cash limit for buying
["20200103", self.TEST_INST, "1000", "buy"], ["20200103", self.TEST_INST, "1000", "buy"],
# test min_cost for buying
["20200103", self.TEST_INST, "1", "buy"],
# test held stock limit for selling
["20200106", self.TEST_INST, "1000", "sell"], ["20200106", self.TEST_INST, "1000", "sell"],
["20200106", self.TEST_INST, "1000", "buy"], # test cash limit for buying
["20200106", self.TEST_INST, "949.7773413058803", "sell"], ["20200107", self.TEST_INST, "1000", "buy"],
# test min_cost for selling
["20200108", self.TEST_INST, "1", "sell"],
# test selling all stocks
["20200110", self.TEST_INST, str(self.DEAL_NUM_FOR_1000), "sell"],
] ]
return pd.DataFrame(orders, columns=headers).set_index(["datetime", "instrument"]) return pd.DataFrame(orders, columns=headers).set_index(["datetime", "instrument"])
@@ -54,7 +63,7 @@ class FileStrTest(TestAutoData):
backtest_config = { backtest_config = {
"start_time": start_time, "start_time": start_time,
"end_time": end_time, "end_time": end_time,
"account": 100000000, "account": 30000,
"benchmark": None, # benchmark is not required here for trading "benchmark": None, # benchmark is not required here for trading
"exchange_kwargs": { "exchange_kwargs": {
"freq": freq, "freq": freq,
@@ -62,9 +71,9 @@ class FileStrTest(TestAutoData):
"deal_price": "close", "deal_price": "close",
"open_cost": 0.0005, "open_cost": 0.0005,
"close_cost": 0.0015, "close_cost": 0.0015,
"min_cost": 5, "min_cost": 500,
"codes": codes, "codes": codes,
"trade_unit": 100, "trade_unit": 2,
}, },
# "pos_type": "InfPosition" # Position with infinitive position # "pos_type": "InfPosition" # Position with infinitive position
} }
@@ -80,7 +89,16 @@ class FileStrTest(TestAutoData):
}, },
}, },
} }
backtest(executor=executor_config, strategy=strategy_config, **backtest_config) report_dict, indicator_dict = backtest(executor=executor_config, strategy=strategy_config, **backtest_config)
# ffr valid
ffr_dict = indicator_dict["1day"]["ffr"].to_dict()
ffr_dict = {str(date).split()[0]: ffr_dict[date] for date in ffr_dict}
assert ffr_dict["2020-01-03"] == 0
assert ffr_dict["2020-01-06"] == self.DEAL_NUM_FOR_1000 / 1000
assert ffr_dict["2020-01-07"] == self.DEAL_NUM_FOR_1000 / 1000
assert ffr_dict["2020-01-08"] == 0
assert ffr_dict["2020-01-10"] == 1
self.EXAMPLE_FILE.unlink() self.EXAMPLE_FILE.unlink()

View File

@@ -1,119 +0,0 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
import unittest
import qlib
from qlib.backtest import backtest, order
from qlib.tests import TestAutoData
from qlib.backtest.order import TradeDecisionWO, TradeRangeByTime
import pandas as pd
from pathlib import Path
class FileStrTest(TestAutoData):
TEST_INST = "SH600519"
def init_qlib(self):
provider_uri_day = "/nfs_data1/stock_data/huaxia_1d_qlib"
provider_uri_1min = "/nfs_data1/stock_data/huaxia_1min_qlib"
provider_uri_map = {"1min": provider_uri_1min, "day": provider_uri_day}
client_config = {
"calendar_provider": {
"class": "LocalCalendarProvider",
"module_path": "qlib.data.data",
"kwargs": {
"backend": {
"class": "FileCalendarStorage",
"module_path": "qlib.data.storage.file_storage",
"kwargs": {"provider_uri_map": provider_uri_map},
}
},
},
"feature_provider": {
"class": "LocalFeatureProvider",
"module_path": "qlib.data.data",
"kwargs": {
"backend": {
"class": "FileFeatureStorage",
"module_path": "qlib.data.storage.file_storage",
"kwargs": {"provider_uri_map": provider_uri_map},
}
},
},
}
qlib.init(provider_uri=provider_uri_day, **client_config, expression_cache=None, dataset_cache=None)
def test_file_str(self):
freq = "1min"
inst = ["SH600000", "SH600011"]
start_time = "2020-01-01"
end_time = "2020-01-15 15:00"
strategy_config = {
"class": "RandomOrderStrategy",
"module_path": "qlib.contrib.strategy.rule_strategy",
"kwargs": {
"trade_range": TradeRangeByTime("9:30", "15:00"),
"sample_ratio": 1.0,
"volume_ratio": 0.01,
"market": inst,
},
}
position_dict = {
"cash": 100000000,
"SH600000": {"amount": 100},
"SH600011": {"amount": 101},
}
backtest_config = {
"start_time": start_time,
"end_time": end_time,
"account": position_dict,
"benchmark": None, # benchmark is not required here for trading
"exchange_kwargs": {
"freq": freq,
"limit_threshold": 0.095,
"deal_price": "close",
"open_cost": 0.0005,
"close_cost": 0.0015,
"min_cost": 5,
"codes": inst,
},
"pos_type": "Position", # Position with infinitive position
}
executor_config = {
"class": "NestedExecutor",
"module_path": "qlib.backtest.executor",
"kwargs": {
"time_per_step": "day",
"inner_executor": {
"class": "SimulatorExecutor",
"module_path": "qlib.backtest.executor",
"kwargs": {
"time_per_step": freq,
"generate_report": False,
"verbose": False,
# "verbose": True,
"indicator_config": {
"show_indicator": False,
},
},
},
"inner_strategy": {
"class": "TWAPStrategy",
"module_path": "qlib.contrib.strategy.rule_strategy",
},
"track_data": True,
"generate_report": True,
"indicator_config": {
"show_indicator": True,
},
},
}
self.init_qlib()
backtest(executor=executor_config, strategy=strategy_config, **backtest_config)
if __name__ == "__main__":
unittest.main()