# coding:utf-8
# The MIT License (MIT)
#
# Copyright (c) 2021-2029 XuHaiJiang/QFF
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import pandas as pd
from qff.price.cache import get_current_data
from qff.frame.context import context
from qff.frame.position import Position
from qff.frame.const import RUN_TYPE, ORDER_TYPE, ORDER_STATUS
from qff.price.query import get_stock_name
from qff.tools.utils import util_gen_id
from qff.tools.logs import log
from typing import Dict, Callable, Optional
# 订单未完成, 无任何成交
# 订单完成, 已撤销,
# 订单完成, 全部成交
# 订单状态
[文档]class Order:
"""
订单对象
============== ======================== =====================================================
属性 类型 说明
============== ======================== =====================================================
id str 订单编号
security str 股票代码
is_buy boolean 买入还是卖出, True-买入, False-卖出
amount int 委托数量
status :class:`.ORDER_STATUS` 订单状态
add_time str 订单委托时间
deal_time str 订单成交时间
cancel_time str 订单取消时间
style :class:`.ORDER_TYPE` 订单类型,市价单还是限价单
order_price float 委托价格,当订单为限价单时
trade_amount int 成交数量
trade_money float 成交金额(含交易费用)
trade_price float 成交均价,等于成交金额除以成交数量,包含了交易费用分摊
commission float 交易费用(佣金、税费等)
gain float 订单收益 股票卖出时计算该值
============== ======================== =====================================================
"""
def __init__(self, security, amount, price=None, style=ORDER_TYPE.MARKET, callback=None):
self.id = util_gen_id() # 订单ID
self.security = security # 股票代码
self.security_name = get_stock_name(security, context.current_dt[:10])[security] # 股票名称
self.is_buy = amount > 0 # 买入 or 卖出
self.amount = abs(amount) # 委托数量
self.status = ORDER_STATUS.OPEN # 订单状态
self.add_time = context.current_dt # 订单添加时间
self.deal_time = None # 订单成交时间
self.cancel_time = None # 订单取消时间
self.style = style # 市价单 or 限价单
self.order_price = price # 委托价格,当订单为限价单
self.trade_amount = 0 # 成交数量
# self.cancel_amount = 0 # 撤销数量
self.trade_money = 0 # 成交金额(含交易费用)
self.trade_price = 0 # 成交均价,等于成交金额除以成交数量,包含了交易费用分摊
self.commission = 0 # 交易费用(佣金、税费等)
self.gain = 0 # 订单收益 股票卖出时计算该值
self._callback = callback
# 对账户资金或股票进行锁定
if self.is_buy: # 买入
money = round(price * amount, 2)
commission = round(max(money * context.trade_cost.open_commission, context.trade_cost.min_commission)
+ money * context.trade_cost.open_tax, 2)
self.lock_money = money + commission
context.portfolio.locked_cash += self.lock_money
context.portfolio.available_cash -= self.lock_money
else: # 卖出
context.portfolio.positions[security].locked_amount += self.amount
context.portfolio.positions[security].closeable_amount -= self.amount
@property
def message(self):
return {
'订单编号': self.id,
'股票代码': self.security,
'股票名称': self.security_name,
'交易方向': '买入' if self.is_buy else '卖出',
'交易日期': self.add_time[:10],
'委托时间': self.add_time[11:16],
'成交状态': self.status,
'委托数量': self.amount,
'委托价格': self.order_price,
'订单类型': self.style,
'成交时间': self.deal_time[11:16],
'成交单价': self.trade_price,
'成交数量': self.trade_amount,
'成交金额': self.trade_money,
'交易费用': self.commission,
}
def __repr__(self):
return self.message
def deal(self, deal_time=None):
"""
订单成交
:return:
"""
money = round(self.order_price * self.amount, 2)
self.trade_amount = self.amount
if self.is_buy:
self.commission = round(max(money * context.trade_cost.open_commission,
context.trade_cost.min_commission) + money * context.trade_cost.open_tax, 2)
self.trade_money = money + self.commission
else:
self.commission = round(max(money * context.trade_cost.close_commission,
context.trade_cost.min_commission) + money * context.trade_cost.close_tax, 2)
self.trade_money = money - self.commission
# 股票卖出时,计算该订单的收益
self.gain = round(self.trade_money -
context.portfolio.positions[self.security].avg_cost * self.trade_amount, 2)
self.trade_price = round(self.trade_money / self.trade_amount, 2)
self.deal_time = deal_time if deal_time is not None else context.current_dt
# 对账户资金或股票数据进行更新
if self.is_buy: # 买入
context.portfolio.locked_cash -= self.lock_money
if self.security in context.portfolio.positions.keys():
# 加仓
position: Position = context.portfolio.positions[self.security]
position.today_open_amount += self.trade_amount # 当日加仓数量
position.today_open_price = self.trade_price # 当日买入单价
position.transact_time = self.deal_time # 最后交易时间
position.avg_cost = round((position.avg_cost * position.total_amount + self.trade_money)
/ (position.total_amount + self.trade_amount), 2) # 当前持仓成本
position.acc_avg_cost = round((position.acc_avg_cost * position.total_amount + self.trade_money)
/ (position.total_amount + self.trade_amount), 2) # 累计持仓成本
position.total_amount += self.trade_amount # 总仓位
else:
# 生成一个position对象
position = Position(self.security, self.security_name, self.deal_time, self.trade_amount,
self.trade_price)
context.portfolio.positions[self.security] = position
log.info("订单成交:买入,订单编号:{},股票代码:{},下单数量:{}, 成交时间:{}.".
format(self.id, self.security, self.trade_amount, self.deal_time))
else: # 卖出
context.portfolio.available_cash += self.trade_money
position: Position = context.portfolio.positions[self.security]
position.locked_amount -= self.amount # 挂单冻结仓位
position.transact_time = self.deal_time # 最后交易时间
if position.total_amount > self.trade_amount:
position.acc_avg_cost = round((position.acc_avg_cost * position.total_amount - self.trade_money)
/ (position.total_amount - self.amount), 2) # 累计持仓成本
position.total_amount -= self.trade_amount # 总仓位
if position.total_amount == 0:
context.portfolio.positions.pop(position.security)
log.info("订单成交:卖出,订单编号:{},股票代码:{},下单数量:{}, 成交时间:{}.".
format(self.id, self.security, self.trade_amount, self.deal_time))
self.status = ORDER_STATUS.DEAL
if self._callback is not None:
self._callback(ORDER_STATUS.DEAL)
def cancel(self):
"""
订单取消
:return: 成功返回True
"""
if self.status != ORDER_STATUS.OPEN:
return False
self.status = ORDER_STATUS.CANCELLED
self.cancel_time = context.current_dt
# 对账户资金或股票进行解锁
if self.is_buy: # 买入
context.portfolio.locked_cash -= self.lock_money
context.portfolio.available_cash += self.lock_money
else: # 卖出
context.portfolio.positions[self.security].locked_amount -= self.amount
context.portfolio.positions[self.security].closeable_amount += self.amount
log.info("订单取消:订单编号:{},股票代码:{},下单数量:{}, {}."
.format(self.id, self.security, self.amount, ('买入' if self.is_buy else '卖出')))
if self._callback is not None:
self._callback(ORDER_STATUS.CANCELLED)
def rejected(self):
"""
订单拒绝
:return:
"""
pass
[文档]def order(security, amount=100, price=None, callback=None):
# type: (str, int, float, Callable) -> Optional[str]
"""
按股票数量下单
调用成功后, 您将可以调用 :func:`.get_open_orders` 取得所有未完成的交易, 也可以调用 :func:`.order_cancel` 取消交易
:param security: 标的代码
:param amount: 交易数量, 正数表示买入, 负数表示卖出
:param price: 下单价格,下单价格为空,则认为是市价单,按当前最新价格挂单,否则认为是限价单
:param callback: 回调函数,订单成交/取消后调用执行, callback(status)
:return: 成功返回order对象id,失败返回None
订单撮合规则:
1. 为简化操作,撮合时不考虑成交量,一个订单一次成交记录
2. 市价单买入时按当前价格+滑点价格,转成限价单。如果当前价格为涨停价格,则订单取消。
3. 市价单卖出时按当前价格-滑点价格,转成限价单。如果当前价格为跌停价格,则订单取消。
4. 如果运行频率为天,则下单后立即撮合,读取剩余的分钟数据曲线,判断最高价是否大于委托价,是则成交。
5. 如果运行频率为分钟,则下单后,每分钟都判断该订单是否符合成交条件。
6. 对未成交的订单,在本交易日结束后撤销。
"""
"""
1、根据股票数量判断是买入还是卖出
2、取股票的最新价和涨跌停价
3、如果是市价单,则买入价为最新价+滑点、卖出价为最新价-滑点
4、判断买入卖出的价格是否超过涨跌停价,如果是则返回false
5、判断买入的金额是否小于账户可用金额,卖出的股票数量是否小于当前持股数量,否则返回false
6、生成订单对象,并加入到order_list中,锁定账户中对应的资金或股票
7、如果运行频率为天,则立即撮合,取该股票下单时间后的分钟曲线数据,判断哪个bar能够成交,记录成交价格及成交时间
"""
# log.info('调用order_amount' + str(locals()).replace('{', '(').replace('}', ')'))
log.info(f'调用order(security={security}, amount={amount}, price={price})')
slippage = context.slippage if amount > 0 else -context.slippage
cur_data = get_current_data(security)
if cur_data.paused:
log.warning(f"下单失败:{security}当日停牌!")
return None
if price is None:
style = ORDER_TYPE.MARKET
order_price = round(cur_data.last_price * (1 + slippage), 2)
else:
style = ORDER_TYPE.LIMIT
order_price = price
if order_price > cur_data.high_limit:
order_price = cur_data.high_limit
log.warning('注意:下单价格为涨停价{}'.format(order_price))
elif order_price < cur_data.low_limit:
order_price = cur_data.low_limit
log.warning('注意:下单价格为跌停价{}'.format(order_price))
if amount > 0:
new_amount = int(amount / 100) * 100 # 一手为100股
if new_amount == 0:
log.warning("下单失败:下单股票数量{}不足一手!".format(amount))
return None
if amount != new_amount:
amount = new_amount
log.info('注意:开仓数量必须是100的整数倍,调整为{}。'.format(amount))
money = round(order_price * amount, 2)
commission = round(max(money * context.trade_cost.open_commission, context.trade_cost.min_commission)
+ money * context.trade_cost.open_tax, 2)
money = money + commission
if context.portfolio.available_cash <= money:
available_cash = context.portfolio.available_cash - round(
max(context.portfolio.available_cash * context.trade_cost.open_commission,
context.trade_cost.min_commission) + context.portfolio.available_cash * context.trade_cost.open_tax,
2)
new_amount = int((available_cash / order_price) / 100) * 100
if new_amount == 0:
log.warning("下单失败:账户可用资金可购买股票数量不足一手!")
return None
else:
amount = new_amount
log.warning("注意:账户可用资金不足! 调整开仓数量为{}".format(amount))
elif amount < 0:
if security not in context.portfolio.positions.keys() \
or context.portfolio.positions[security].closeable_amount < abs(amount):
log.warning("下单失败:账户无该数量的股票!")
return None
else:
log.warning("下单失败:订单股票数量为0!")
return None
_order = Order(security, amount, order_price, style, callback)
if _order is not None:
context.order_list[_order.id] = _order
log.info("下单成功:订单编号:{},股票代码:{},下单数量:{}, {}.".
format(_order.id, _order.security, amount, ('买入' if _order.is_buy else '卖出')))
if context.run_freq == 'day' and context.run_type == RUN_TYPE.BACK_TEST:
order_broker_day(_order.id)
return _order.id
else:
return None
[文档]def order_value(security, value, price=None, callback=None):
# type: (str, float, float, Callable) -> Optional[str]
"""
按股票价值下单
:param security: 股票代码
:param value: 股票价值,value = 最新价 * 手数 * 乘数(股票为100)
:param price: 下单价格,市价单可不填价格,按当前最新价格挂单
:param callback: 回调函数,订单成交/取消后调用执行, callback(status)
:return: Order对象id或者None, 如果创建委托成功, 则返回Order对象id, 失败则返回None
:Example:
.. code-block:: python
# 卖出价值为10000元的平安银行股票
order_value('000001', -10000)
"""
log.debug('调用order_value' + str(locals()).replace('{', '(').replace('}', ')'))
if value == 0:
log.warning("下单失败:下单股票价值为0!")
return None
cur_data = get_current_data(security)
slippage = context.slippage if value > 0 else -context.slippage
order_price = price if price is not None else cur_data.last_price * (1 + slippage)
# order_price = price if price is not None else cur_data.last_price
amount = int(value / order_price)
return order(security, amount, price, callback)
[文档]def order_target(security, amount, price=None, callback=None):
# type: (str, int, float, Callable) -> Optional[str]
"""
按股票目标数量下单
使最终标的的数量达到指定的amount。
**注意使用此接口下单时若指定的标的有未完成的订单,则先前未完成的订单将会被取消**
:param security: 股票代码
:param amount: 期望的标的最终持有的股票数量
:param price: 下单价格,市价单可不填价格,按当前最新价格挂单
:param callback: 回调函数,订单成交/取消后调用执行, callback(status)
:return: Order对象id或者None, 如果创建委托成功, 则返回Order对象id, 失败则返回None
"""
log.debug('调用order_target' + str(locals()).replace('{', '(').replace('}', ')'))
if amount < 0:
log.warning("下单失败:目标数量不能小于0!")
return None
pre_hold = 0 # 之前建仓的股票数量
if security in context.portfolio.positions.keys():
pst: Position = context.portfolio.positions[security]
_order: Order
for _order in context.order_list.values():
if _order.security == security and _order.status == ORDER_STATUS.OPEN:
_order.cancel()
if pst.today_open_amount > amount:
log.warning("今日建仓的股票数量大于目标数量!,目标数量修改为今日建仓数量!")
amount = pst.today_open_amount
pre_hold = pst.total_amount
return order(security, amount - pre_hold, price, callback)
[文档]def order_target_value(security, value, price=None, callback=None):
# type: (str, float, float, Callable) -> Optional[str]
"""
按股票目标价值下单
调整标的仓位到value价值,
**注意使用此接口下单时若指定的标的有未完成的订单,则先前未完成的订单将会被取消**
:param security: 股票代码
:param value: 期望的标的最终价值,value = 最新价 * 手数 * 乘数(股票为100)
:param price: 下单价格,市价单可不填价格,按当前最新价格挂单
:param callback: 回调函数,订单成交/取消后调用执行, callback(status)
:return: Order对象id或者None, 如果创建委托成功, 则返回Order对象id, 失败则返回None
:Example:
.. code-block:: python
# 卖出价值为10000元的平安银行股票
order_value('000001', -10000)
#卖出平安银行所有股票
order_target_value('000001', 0)
#调整平安银行股票仓位到10000元价值
order_target_value('000001', 10000)
"""
log.debug('调用order_target_value' + str(locals()).replace('{', '(').replace('}', ')'))
order_price = get_current_data(security).last_price
amount = int(value / order_price)
return order_target(security, amount, price, callback)
[文档]def order_cancel(order_id):
# type: (str) -> bool
"""
撤回已下的订单
:param order_id: 订单编号
:return: 成功返回True,失败返回False
"""
log.debug('调用order_cancel' + str(locals()).replace('{', '(').replace('}', ')'))
if order_id in context.order_list.keys():
order_obj: Order = context.order_list[order_id]
return order_obj.cancel()
else:
return False
[文档]def get_orders(order_id=None, security=None, status=None):
# type: (str, str, ORDER_TYPE) -> Dict[str, Order]
"""
获取当天的所有订单
:param order_id: 订单 id
:param security: 标的代码,可以用来查询指定标的的所有订单
:param status: 查询特定订单状态的所有订单
:return: 返回一个dict, key是order_id, value是 :class:`.Order` 对象
"""
log.debug('调用get_orders' + str(locals()).replace('{', '(').replace('}', ')'))
rtn = {}
if order_id is not None:
if order_id in context.order_list.keys():
rtn[order_id] = context.order_list[order_id]
elif security is not None:
for _id, order_obj in context.order_list.items():
if order_obj.security == security:
rtn[_id] = context.order_list[_id]
elif status is not None:
for _id, order_obj in context.order_list.items():
if order_obj.status == status:
rtn[_id] = context.order_list[_id]
else:
rtn = context.order_list
return rtn
[文档]def get_open_orders():
"""
获得当天的所有未完成的订单
:return: 返回一个dict, key是order_id, value是[Order]对象
"""
rtn = {}
for _id, order_obj in context.order_list.items():
if order_obj.status == ORDER_STATUS.OPEN:
rtn[_id] = order_obj
return rtn
def order_broker_day(order_id):
"""
如果回测运行频率为 'day',则立即进行订单撮合
:param order_id:
:return: None
"""
log.debug('调用order_broker_day' + str(locals()).replace('{', '(').replace('}', ')'))
_order = context.order_list[order_id]
code = _order.security
data: pd.DataFrame = get_current_data(code).min_data_after
if data is not None and len(data) > 1:
for i in range(0, len(data)):
if _order.is_buy:
if data.iloc[i].low <= _order.order_price:
_order.deal(data.index[i])
break
elif data.iloc[i].high >= _order.order_price:
_order.deal(data.index[i])
break
return
def order_broker():
"""
分钟撮合函数,根据回测频率运行
:return:
"""
log.debug('调用order_broker' + str(locals()).replace('{', '(').replace('}', ')'))
for _order in context.order_list.values():
if _order.add_time[0:10] != context.current_dt[0:10]: # 防止框架恢复运行导入其他日期的context
context.order_list.remove(_order)
if _order.status == ORDER_STATUS.OPEN and _order.add_time < context.current_dt: # 防止下单后马上撮合
code = _order.security
data = get_current_data(code)
if _order.is_buy:
if data.last_low < _order.order_price:
_order.deal()
# log.info("订单成交:订单编号{},股票代码{},成交数量{},成交时间{}"
# .format(_order.id, code, _order.trade_amount, context.current_dt[11:]))
elif data.last_high > _order.order_price:
_order.deal()
# log.info("订单成交:订单编号{},股票代码{},成交数量{},成交时间{}"
# .format(_order.id, code, _order.trade_amount, context.current_dt[11:]))