Skip to content

Web

本module提供服务端点的响应处理

accounts

简易账户管理

提供了创建账户、查询账户、删除账户和状态持久化实现。

Accounts

Source code in backtest/web/accounts.py
class Accounts:
    _brokers = {}

    def on_startup(self):
        token = cfg.auth.admin
        self._brokers[token] = Broker("admin", 0, 0.0)

        state_file = os.path.join(home_dir(), "state.pkl")
        try:
            with open(state_file, "rb") as f:
                self._brokers = pickle.load(f)
        except FileNotFoundError:
            pass
        except Exception as e:
            logger.exception(e)

    def on_exit(self):
        state_file = os.path.join(home_dir(), "state.pkl")
        with open(state_file, "wb") as f:
            pickle.dump(self._brokers, f)

    def get_broker(self, token):
        return self._brokers.get(token)

    def is_valid(self, token: str):
        return token in self._brokers

    def is_admin(self, token: str):
        cfg = cfg4py.get_instance()
        return token == cfg.auth.admin

    def create_account(
        self,
        name: str,
        token: str,
        principal: float,
        commission: float,
        start: datetime.date = None,
        end: datetime.date = None,
    ):
        """创建新账户

        一个账户由`name`和`token`的组合惟一确定。如果前述组合已经存在,则创建失败。

        Args:
            name (str): 账户/策略名称
            token (str): 账户token
            principal (float): 账户起始资金
            commission (float): 账户手续费
            start (datetime.date, optional): 回测开始日期,如果是模拟盘,则可为空
            end (datetime.date, optional): 回测结束日期,如果是模拟盘,则可为空
        """
        if token in self._brokers:
            msg = f"账户{name}:{token}已经存在,不能重复创建。"
            raise AccountError(msg)

        for broker in self._brokers.values():
            if broker.account_name == name:
                msg = f"账户{name}:{token}已经存在,不能重复创建。"
                raise AccountError(msg)

        broker = Broker(name, principal, commission, start, end)
        self._brokers[token] = broker

        logger.info("新建账户:%s, %s", name, token)
        return {
            "account_name": name,
            "token": token,
            "account_start_date": broker.account_start_date,
            "principal": broker.principal,
        }

    def list_accounts(self, mode: str):
        if mode != "all":
            filtered = {
                token: broker
                for token, broker in self._brokers.items()
                if broker.mode == mode and broker.account_name != "admin"
            }
        else:
            filtered = {
                token: broker
                for token, broker in self._brokers.items()
                if broker.account_name != "admin"
            }

        return [
            {
                "account_name": broker.account_name,
                "token": token,
                "account_start_date": broker.account_start_date,
                "principal": broker.principal,
            }
            for token, broker in filtered.items()
        ]

    def delete_accounts(self, account_to_delete: str = None):
        if account_to_delete is None:
            self._brokers = {}
            self._brokers[cfg.auth.admin] = Broker("admin", 0, 0.0)
            return 0
        else:
            for token, broker in self._brokers.items():
                if broker.account_name == account_to_delete:
                    del self._brokers[token]
                    logger.info("账户:%s已删除", account_to_delete)

                    return len(self._brokers) - 1
            else:
                logger.warning("账户%s不存在", account_to_delete)
                return len(self._brokers)

create_account(self, name, token, principal, commission, start=None, end=None)

创建新账户

一个账户由nametoken的组合惟一确定。如果前述组合已经存在,则创建失败。

Parameters:

Name Type Description Default
name str

账户/策略名称

required
token str

账户token

required
principal float

账户起始资金

required
commission float

账户手续费

required
start datetime.date

回测开始日期,如果是模拟盘,则可为空

None
end datetime.date

回测结束日期,如果是模拟盘,则可为空

None
Source code in backtest/web/accounts.py
def create_account(
    self,
    name: str,
    token: str,
    principal: float,
    commission: float,
    start: datetime.date = None,
    end: datetime.date = None,
):
    """创建新账户

    一个账户由`name`和`token`的组合惟一确定。如果前述组合已经存在,则创建失败。

    Args:
        name (str): 账户/策略名称
        token (str): 账户token
        principal (float): 账户起始资金
        commission (float): 账户手续费
        start (datetime.date, optional): 回测开始日期,如果是模拟盘,则可为空
        end (datetime.date, optional): 回测结束日期,如果是模拟盘,则可为空
    """
    if token in self._brokers:
        msg = f"账户{name}:{token}已经存在,不能重复创建。"
        raise AccountError(msg)

    for broker in self._brokers.values():
        if broker.account_name == name:
            msg = f"账户{name}:{token}已经存在,不能重复创建。"
            raise AccountError(msg)

    broker = Broker(name, principal, commission, start, end)
    self._brokers[token] = broker

    logger.info("新建账户:%s, %s", name, token)
    return {
        "account_name": name,
        "token": token,
        "account_start_date": broker.account_start_date,
        "principal": broker.principal,
    }

interfaces

bills(request) async

获取交易记录

Returns:

Type Description
Response

以binary方式返回。结果为一个字典,包括以下字段:

  • tx: 配对的交易记录
  • trades: 成交记录
  • positions: 持仓记录
  • assets: 每日市值
Source code in backtest/web/interfaces.py
@bp.route("bills", methods=["GET"])
@protected
async def bills(request):
    """获取交易记录

    Returns:
        Response: 以binary方式返回。结果为一个字典,包括以下字段:

        - tx: 配对的交易记录
        - trades: 成交记录
        - positions: 持仓记录
        - assets: 每日市值

    """
    results = {}

    broker: Broker = request.ctx.broker

    results["tx"] = broker.transactions
    results["trades"] = broker.trades
    results["positions"] = broker._positions

    if not (broker.mode == "bt" and broker._bt_stopped):
        await broker.recalc_assets()

    results["assets"] = broker._assets
    return response.json(jsonify(results))

buy(request) async

买入

Parameters:

Name Type Description Default
request Request

参数以json方式传入, 包含:

  • security : 证券代码
  • price: 买入价格,如果为None,则意味着以市价买入
  • volume: 买入数量
  • order_time: 下单时间
required

Returns:

Type Description
Response

买入结果, 字典,包含以下字段:

  • tid: str, 交易id
  • eid: str, 委托id
  • security: str, 证券代码
  • order_side: str, 买入/卖出
  • price: float, 成交均价
  • filled: float, 成交数量
  • time: str, 下单时间
  • trade_fees: float, 手续费
Source code in backtest/web/interfaces.py
@bp.route("buy", methods=["POST"])
@protected
async def buy(request):
    """买入

    Args:
        request Request: 参数以json方式传入, 包含:

            - security : 证券代码
            - price: 买入价格,如果为None,则意味着以市价买入
            - volume: 买入数量
            - order_time: 下单时间

    Returns:
        Response: 买入结果, 字典,包含以下字段:

        - tid: str, 交易id
        - eid: str, 委托id
        - security: str, 证券代码
        - order_side: str, 买入/卖出
        - price: float, 成交均价
        - filled: float, 成交数量
        - time: str, 下单时间
        - trade_fees: float, 手续费

    """
    params = request.json or {}

    security = params["security"]
    price = params["price"]
    volume = params["volume"]
    order_time = arrow.get(params["order_time"]).naive

    result = await request.ctx.broker.buy(security, price, volume, order_time)
    return response.json(jsonify(result))

delete_accounts(request) async

删除账户

当提供了账户名name和token(通过headers传递)时,如果name与token能够匹配,则删除name账户。

Parameters:

Name Type Description Default
request Request

通过params传递以下字段

  • name, 待删除的账户名。如果为空,且提供了admin token,则删除全部账户。
required
Source code in backtest/web/interfaces.py
@bp.route("accounts", methods=["DELETE"])
@protected
async def delete_accounts(request):
    """删除账户

    当提供了账户名`name`和token(通过headers传递)时,如果name与token能够匹配,则删除`name`账户。
    Args:
        request Request: 通过params传递以下字段

            - name, 待删除的账户名。如果为空,且提供了admin token,则删除全部账户。

    """
    account_to_delete = request.args.get("name", None)
    accounts = request.app.ctx.accounts

    if account_to_delete is None:
        if request.ctx.broker.account_name == "admin":
            accounts.delete_accounts()
        else:
            return response.text("admin account required", status=403)

    if account_to_delete == request.ctx.broker.account_name:
        accounts.delete_accounts(account_to_delete)

get_assets(request) async

获取账户资产信息

本方法主要为绘制资产收益曲线提供数据。

Parameters:

Name Type Description Default
request Request

以args方式传入,包含以下字段

  • start: 日期,格式为YYYY-MM-DD,待获取账户信息的日期,如果为空,则取账户起始日
  • end: 日期,格式为YYYY-MM-DD,待获取账户信息的日期,如果为空,则取最后交易日
required

Returns:

Type Description
Response

startend期间的账户资产信息,结果以binary方式返回,参考backtest.trade.datatypes.rich_assets_dtype

Source code in backtest/web/interfaces.py
@bp.route("assets", methods=["GET"])
@protected
async def get_assets(request):
    """获取账户资产信息

    本方法主要为绘制资产收益曲线提供数据。

    Args:
        request Request: 以args方式传入,包含以下字段

            - start: 日期,格式为YYYY-MM-DD,待获取账户信息的日期,如果为空,则取账户起始日
            - end: 日期,格式为YYYY-MM-DD,待获取账户信息的日期,如果为空,则取最后交易日

    Returns:

        Response: 从`start`到`end`期间的账户资产信息,结果以binary方式返回,参考[backtest.trade.datatypes.rich_assets_dtype][]

    """
    broker: Broker = request.ctx.broker

    start = request.args.get("start")
    if start:
        start = arrow.get(start).date()
    else:
        start = broker.account_start_date

    end = request.args.get("end")
    if end:
        end = arrow.get(end).date()
    else:
        end = broker.account_end_date

    if not (broker.mode == "bt" and broker._bt_stopped):
        await broker.recalc_assets(end)

    if broker._assets.size == 0:
        return response.raw(pickle.dump(np.empty(0, dtype=rich_assets_dtype)))

    # cash may be shorter than assets
    if broker._cash.size == 0:
        cash = broker._assets.astype(cash_dtype)
    elif broker._cash.size < broker._assets.size:
        n = broker._assets.size - broker._cash.size
        cash = np.pad(broker._cash, (0, n), "edge")
        cash["date"] = broker._assets["date"]
    else:
        cash = broker._cash

    cash = cash[(cash["date"] <= end) & (cash["date"] >= start)]

    assets = broker._assets
    assets = assets[(assets["date"] <= end) & (assets["date"] >= start)]

    mv = assets["assets"] - cash["cash"]

    # both _cash and _assets has been moved backward one day
    result = numpy_append_fields(
        assets, ["cash", "mv"], [cash["cash"], mv], [("cash", "f8"), ("mv", "f8")]
    ).astype(rich_assets_dtype)

    return response.raw(pickle.dumps(result))

info(request) async

获取账户信息

Parameters:

Name Type Description Default
request Request

以args方式传入,包含以下字段

  • date: 日期,格式为YYYY-MM-DD,待获取账户信息的日期,如果为空,则意味着取当前日期的账户信息
required

Returns:

Type Description
Response

结果以binary方式返回。结果为一个dict,其中包含以下字段:

  • name: str, 账户名
  • principal: float, 初始资金
  • assets: float, 当前资产
  • start: datetime.date, 账户创建时间
  • last_trade: datetime.date, 最后一笔交易日期
  • end: 账户结束时间,仅对回测模式有效
  • available: float, 可用资金
  • market_value: 股票市值
  • pnl: 盈亏(绝对值)
  • ppnl: 盈亏(百分比),即pnl/principal
  • positions: 当前持仓,dtype为backtest.trade.datatypes.position_dtype的numpy structured array
Source code in backtest/web/interfaces.py
@bp.route("info", methods=["GET"])
@protected
async def info(request):
    """获取账户信息

    Args:
        request Request: 以args方式传入,包含以下字段

            - date: 日期,格式为YYYY-MM-DD,待获取账户信息的日期,如果为空,则意味着取当前日期的账户信息

    Returns:

        Response: 结果以binary方式返回。结果为一个dict,其中包含以下字段:

        - name: str, 账户名
        - principal: float, 初始资金
        - assets: float, 当前资产
        - start: datetime.date, 账户创建时间
        - last_trade: datetime.date, 最后一笔交易日期
        - end: 账户结束时间,仅对回测模式有效
        - available: float, 可用资金
        - market_value: 股票市值
        - pnl: 盈亏(绝对值)
        - ppnl: 盈亏(百分比),即pnl/principal
        - positions: 当前持仓,dtype为[backtest.trade.datatypes.position_dtype][]的numpy structured array

    """
    date = request.args.get("date")
    result = await request.ctx.broker.info(date)
    return response.raw(pickle.dumps(result))

market_buy(request) async

市价买入

Parameters:

Name Type Description Default
request Request

参数以json方式传入, 包含

  • security: 证券代码
  • volume: 买入数量
  • order_time: 下单时间
required

Returns:

Type Description
Response

买入结果, 请参考backtest.web.interfaces.buy

Source code in backtest/web/interfaces.py
@bp.route("market_buy", methods=["POST"])
@protected
async def market_buy(request):
    """市价买入

    Args:
        request Request: 参数以json方式传入, 包含

            - security: 证券代码
            - volume: 买入数量
            - order_time: 下单时间
    Returns:
        Response: 买入结果, 请参考[backtest.web.interfaces.buy][]

    """
    params = request.json or {}

    security = params["security"]
    volume = params["volume"]
    order_time = arrow.get(params["order_time"]).naive

    result = await request.ctx.broker.buy(security, None, volume, order_time)
    return response.json(jsonify(result))

market_sell(request) async

以市价卖出证券

Parameters:

Name Type Description Default
request

以json方式传入,包含以下字段

  • security : 证券代码
  • volume: 卖出数量
  • order_time: 下单时间
required

Returns:

Type Description
Response

参考backtest.web.interfaces.buy

Source code in backtest/web/interfaces.py
@bp.route("market_sell", methods=["POST"])
@protected
async def market_sell(request):
    """以市价卖出证券

    Args:
        request : 以json方式传入,包含以下字段

            - security : 证券代码
            - volume: 卖出数量
            - order_time: 下单时间

    Returns:
        Response: 参考[backtest.web.interfaces.buy][]
    """
    params = request.json or {}

    security = params["security"]
    volume = params["volume"]
    order_time = arrow.get(params["order_time"]).naive

    result = await request.ctx.broker.sell(security, None, volume, order_time)
    return response.json(jsonify(result))

metrics(request) async

获取回测的评估指标信息

Parameters:

Name Type Description Default
request

以args方式传入,包含以下字段

  • start: 开始时间,格式为YYYY-MM-DD
  • end: 结束时间,格式为YYYY-MM-DD
  • baseline: str, 用来做对比的证券代码,默认为空,即不做对比
required

Returns:

Type Description
Response

结果以binary方式返回,参考backtest.trade.broker.Broker.metrics

Source code in backtest/web/interfaces.py
@bp.route("metrics", methods=["GET"])
@protected
async def metrics(request):
    """获取回测的评估指标信息

    Args:
        request : 以args方式传入,包含以下字段

            - start: 开始时间,格式为YYYY-MM-DD
            - end: 结束时间,格式为YYYY-MM-DD
            - baseline: str, 用来做对比的证券代码,默认为空,即不做对比

    Returns:

        Response: 结果以binary方式返回,参考[backtest.trade.broker.Broker.metrics][]

    """
    start = request.args.get("start")
    end = request.args.get("end")
    baseline = request.args.get("baseline")

    if start:
        start = arrow.get(start).date()

    if end:
        end = arrow.get(end).date()

    metrics = await request.ctx.broker.metrics(start, end, baseline)
    return response.raw(pickle.dumps(metrics))

positions(request) async

获取持仓信息

Parameters:

Name Type Description Default
request Request

以args方式传入,包含以下字段:

  • date: 日期,格式为YYYY-MM-DD,待获取持仓信息的日期
required

Returns:

Type Description
Response

结果以binary方式返回。结果为一个numpy structured array数组,其dtype为backtest.trade.datatypes.daily_position_dtype

Source code in backtest/web/interfaces.py
@bp.route("positions", methods=["GET"])
@protected
async def positions(request) -> NDArray[daily_position_dtype]:
    """获取持仓信息

    Args:
        request Request:以args方式传入,包含以下字段:

            - date: 日期,格式为YYYY-MM-DD,待获取持仓信息的日期

    Returns:
        Response: 结果以binary方式返回。结果为一个numpy structured array数组,其dtype为[backtest.trade.datatypes.daily_position_dtype][]

    """
    date = request.args.get("date")

    if date is None:
        position = request.ctx.broker.position
    else:
        date = arrow.get(date).date()
        position = request.ctx.broker.get_position(date)

    position = position[position["shares"] != 0]
    return response.raw(pickle.dumps(position))

sell(request) async

卖出证券

Parameters:

Name Type Description Default
request

参数以json方式传入, 包含:

  • security : 证券代码
  • price: 卖出价格,如果为None,则意味着以市价卖出
  • volume: 卖出数量
  • order_time: 下单时间
required

Returns:

Type Description
Response

参考backtest.web.interfaces.buy

Source code in backtest/web/interfaces.py
@bp.route("sell", methods=["POST"])
@protected
async def sell(request):
    """卖出证券

    Args:
        request: 参数以json方式传入, 包含:

            - security : 证券代码
            - price: 卖出价格,如果为None,则意味着以市价卖出
            - volume: 卖出数量
            - order_time: 下单时间

    Returns:
        Response: 参考[backtest.web.interfaces.buy][]
    """
    params = request.json or {}

    security = params["security"]
    price = params["price"]
    volume = params["volume"]
    order_time = arrow.get(params["order_time"]).naive

    result = await request.ctx.broker.sell(security, price, volume, order_time)
    return response.json(jsonify(result))

sell_percent(request) async

卖出证券

Parameters:

Name Type Description Default
request Request

参数以json方式传入, 包含

  • security: 证券代码
  • percent: 卖出比例
  • order_time: 下单时间
  • price: 卖出价格,如果为None,则意味着以市价卖出
required

Returns:

Type Description
Response

参考backtest.web.interfaces.buy

Source code in backtest/web/interfaces.py
@bp.route("sell_percent", methods=["POST"])
@protected
async def sell_percent(request):
    """卖出证券

    Args:
        request Request: 参数以json方式传入, 包含

            - security: 证券代码
            - percent: 卖出比例
            - order_time: 下单时间
            - price: 卖出价格,如果为None,则意味着以市价卖出

    Returns:
        Response: 参考[backtest.web.interfaces.buy][]
    """
    params = request.json or {}

    security = params["security"]
    price = params["price"]
    percent = params["percent"]
    order_time = arrow.get(params["order_time"]).naive

    assert 0 < percent <= 1.0, "percent must be between 0 and 1.0"
    broker: Broker = request.ctx.broker
    position = broker.get_position(order_time.date())
    sellable = position[position["security"] == security]
    if sellable.size == 0:
        raise EntrustError(EntrustError.NO_POSITION, security=security, time=order_time)

    sellable = sellable[0]["sellable"] * percent

    result = await request.ctx.broker.sell(security, price, sellable, order_time)
    return response.json(jsonify(result))

start_backtest(request) async

启动回测

启动回测时,将为接下来的回测创建一个新的账户。

Parameters:

Name Type Description Default
request Request

包含以下字段的请求对象

  • name, 账户名称
  • token,账户token
  • principal,账户初始资金
  • commission,账户手续费率
  • start,回测开始日期,格式为YYYY-MM-DD
  • end,回测结束日期,格式为YYYY-MM-DD
required

Returns:

Type Description
json

包含以下字段的json对象

  • account_name, str
  • token, str
  • account_start_date, str
  • principal, float
Source code in backtest/web/interfaces.py
@bp.route("start_backtest", methods=["POST"])
async def start_backtest(request):
    """启动回测

    启动回测时,将为接下来的回测创建一个新的账户。

    Args:
        request Request: 包含以下字段的请求对象

            - name, 账户名称
            - token,账户token
            - principal,账户初始资金
            - commission,账户手续费率
            - start,回测开始日期,格式为YYYY-MM-DD
            - end,回测结束日期,格式为YYYY-MM-DD

    Returns:

        json: 包含以下字段的json对象

        - account_name, str
        - token, str
        - account_start_date, str
        - principal, float

    """
    params = request.json or {}

    try:
        name = params["name"]
        token = params["token"]
        start = arrow.get(params["start"]).date()
        end = arrow.get(params["end"]).date()
        principal = params["principal"]
        commission = params["commission"]
    except KeyError as e:
        logger.warning(f"parameter {e} is required")
        return response.text(f"parameter {e} is required", status=499)
    except Exception as e:
        logger.exception(e)
        return response.text(
            "parameter error: name, token, start, end, principal, commission",
            status=499,
        )

    accounts = request.app.ctx.accounts
    try:
        result = accounts.create_account(
            name, token, principal, commission, start=start, end=end
        )
        logger.info("backtest account created:", result)
        return response.json(jsonify(result))
    except AccountError as e:
        return response.text(e.message, status=499)

stop_backtest(request) async

结束回测

结束回测后,账户将被冻结,此后将不允许进行任何操作

todo: 增加持久化操作
Source code in backtest/web/interfaces.py
@bp.route("stop_backtest", methods=["POST"])
@protected
async def stop_backtest(request):
    """结束回测

    结束回测后,账户将被冻结,此后将不允许进行任何操作

    # todo: 增加持久化操作

    """
    broker = request.ctx.broker
    if broker.mode != "bt":
        raise AccountError("在非回测账户上试图执行不允许的操作")

    if not broker._bt_stopped:
        broker._bt_stopped = True
        await broker.recalc_assets()

    return response.text("ok")