仓位管理类的实现与遇到的坑

2022年8月29日 622点热度 5人点赞 0条评论

在实盘策略上,我目前所有持仓标的应用的都是网格策略。在市场处于震荡期间,网格无疑是最好的交易策略之一。而对于网格交易来讲,最需要关注的是仓位,确保在市场出现调整时仓位按照预期进行控制,并且有资金可以加仓。除去仓位管理这个工作,我不太关心明天市场是涨是跌,因为只要有波动性在就可以稳定赚取差价收益,积少成多。

过去在仓位控制上,没有通过代码进行自动化的管理,基本都是靠盘前主观预判后,手动开启或关闭策略类实例的买入或者卖出权限实现。周末写了一个类,完善了之前在仓位管理上存在的不足。

之所以过去没有做仓位管理的自动化处理,一定程度上是因为滑点,废单,手工下单等因素的存在,主观上感觉无法通过程序完全准确的计算拟合出账户的真实资金状态,所以一直没有主动用代码进行实现。在之前出现过几次仓位接近满仓的情况后,我感觉还是需要解决这个问题。而解决这个问题,一个重要的前提,就是要实时准确的获取账户的资金状态以判断可用资金和仓位,既然因为前面提到的各种因素无法准确计算,那可以换个思路,改为直接读取交易软件界面控件值,按这个方向进行尝试后,发现可行,但有坑,这里有必要记录备忘一下。

作为拿不到券商交易接口的普通散户,下单上的处理,一般思路是通过模拟键鼠操作PC端交易软件解决。以同某顺为例,在其交易界面上,我们可以看到账户资金信息显示在下图红框内的Label中,这是我们需要读取的目标。

祭出Spy++,看看是不是能正常读取这些窗体和控件的句柄,可以的话,通过win32api或者现成的工具包对其进行操作就是很容易处理的事情。如下图所示,通过Spy++能看到交易软件的PC端使用的是MFC框架,并且能够正常获得这些控件的句柄。

OK,接下来要处理另一个问题是刷新问题,这些我们需要读取的数值在同某顺中并不是实时刷新的,而我们需要的是在交易信号触发后即时判断仓位,资金等信息,要实现这个需求,很自然的做法是获取刷新按钮的句柄并调用它的click事件,刷新后再读取控件值。

走到这里,第一个坑出来了:

尽管交易软件基于MFC进行开发,但在界面工具条最左侧的按钮组(下图红框),并不是标准的windows组件,无论用Spy++如何读取,整个窗体组件的树状结构里依然看不到任何关于这些button的影子。

然而问题并不是完全无解,软件为这个刷新按钮设置了一个快捷键F5,所以,一个替代方法就是手动设置整个窗体的焦点至前台,然后模拟键盘发送F5,刷新后读取即可。相比于获取按钮句柄调用click,这样的做法美中不足是需要将窗体置于前台设置焦点,不过影响并不大,因为在交易时段,交易窗体本身即时置顶的。

接下来,试着用代码读取这些控件,使用pywinauto这个自动化包,创建app对象后调用print_control_identifiers(),打印Spy++中读取的窗体树状结构并保存到ctrl_list.txt这个文件中。代码如下:

import pywinauto
from config import Sys_config

if __name__ == '__main__':
    app = pywinauto.application.Application()
    app.connect(title=Sys_config.THS_FORM_TITLE)
    app[Sys_config.THS_FORM_TITLE].print_control_identifiers(filename='ctrl_list.txt')

OK,接下来第二个坑登场:

在导出的树状结构中,我们可以看到每个MFC的Static组件都有各自的编号,但是这个树状结构中的编号,均不是该Static的真实编号。

通过我人肉手动循环测试,发现真实的Static序号是树状结构中的序号 -2,这是重点,必须敲黑板。

最终梳理了一下,在计算仓位,资金时,主要需要用到的数据和Static编号对应的关系如下:

总资产 :Static12 持仓市值:Static11 持仓盈亏:Static14 可用金额:Static6

至于仓位,用 持仓市值/总资产 进行计算。

好了,至此相关的技术问题已经解决,接下来是写我们在仓位管理上需要用到类,我给这个类命名为Broker,在后续的开发中这个类将承担更多的计算和调度任务。目前实现三个接口:1、获取并计算仓位,可用资金,这是供其他接口调用的一个基础方法。2、下单前的资金和仓位判断接口pay,当可用资金大于头寸金额,且交易后仓位不大于系统设定阈值,返回True,否则False。3、一个记录资金和仓位明细的方法,每日盘后调用,用作账户资金仓位的每日记录。一个初版的Broker类代码实现如下:

# 账户资金信息读取与资金管理

import os,sys
sys.path.append(os.getcwd())

import pywinauto
import pandas as pd
from pywinauto.keyboard import send_keys
from config import Sys_config,Trading_config
from commons import Sys_path
from analyzer import profit_cost
from utils import log_console
from datetime import date

class Broker:
    """
    读取交易软件界面控件,计算账户资金信息进行资金管理

    Methods:
        get_assets_info: 获取账户资金信息,返回字典:
            {
                'total_assets':总资产,
                'positions_money':持仓市值,
                'positions_profit':持仓盈亏,
                'available_money':可用金额,
                'positions':仓位
            }
        pay: 资金调用申请接口,当交易后持仓未达最大仓位,且可用资金大于本次买入所需资金时,返回True
    """
    def __init__(self) -> None:
        # 初始化资金变量
        self.max_positions = float(Trading_config.MAX_POSITIONS)
        self.initial_funding = float(Trading_config.INITIAL_FUNDING)
        self.position_size = float(Trading_config.POSITION_SIZE)
    
    def get_assets_info(self) -> dict:
        """
        读取交易软件界面控件,获取总资产、持仓市值、持仓盈亏、可用金额、仓位比例等

        Returns:
            dict: {
                'total_assets':总资产,
                'positions_money':持仓市值,
                'positions_profit':持仓盈亏,
                'available_money':可用金额,
                'positions':仓位
            }
        """
        # 确保只有在调用函数时进行app连接,不在构造函数中调用,防止程序启动初始化类时因为没有预先启动
        # 交易软件而引发异常
        try:
            app = pywinauto.application.Application()
            app.connect(title=Sys_config.THS_FORM_TITLE)
            top_hwnd = pywinauto.findwindows.find_window(title=Sys_config.THS_FORM_TITLE)
            top_w  = app.window(handle=top_hwnd)
            top_w.set_focus()
            send_keys('{F5}')
        except:
            return None
        # 总资产
        total_assets = float(app[Sys_config.THS_FORM_TITLE].Static12.texts()[0])
        # 持仓市值
        positions_money = float(app[Sys_config.THS_FORM_TITLE].Static11.texts()[0])
        # 持仓盈亏
        positions_profit = float(app[Sys_config.THS_FORM_TITLE].Static14.texts()[0])
        # 可用金额
        available_money = float(app[Sys_config.THS_FORM_TITLE].Static6.texts()[0])
        # 仓位
        positions = round(positions_money/total_assets,4)
        
        result = {
            'total_assets':total_assets,
            'positions_money':positions_money,
            'positions_profit':positions_profit,
            'available_money':available_money,
            'positions':positions
        }
        return result
    
    def pay(self,buy_price:float,buy_quantity:int,cost_type:str,code:str) -> bool:
        """
        当买入信号触发,策略向Broker类的pay方法申请资金
        在可用资金大于买入所需资金及买入后仓位不大于最大仓位时,返回True,否则返回False

        Args:
            buy_price (float): 买入价格
            buy_quantity (int): 买入数量
            cost_type (str): 成本类型 (STOCKS_RATE|ETF_RATE|CONVERTIBLE_BOND_RATE_SH|CONVERTIBLE_BOND_RATE_SZ)
            code (str): 调用该方法的标的代码,用于出错时的log记录

        Returns:
            bool: 是否可以买入
        """
        assets_info = self.get_assets_info()
        if assets_info:
            total_assets = assets_info['total_assets']
            positions_money = assets_info['positions_money']
            available_money = assets_info['available_money']
            buy_money = buy_price*buy_quantity + profit_cost(cost_type=cost_type,buy_price=buy_price,quantity=buy_quantity)['buy_cost']
            positions_money_after_buy = positions_money + buy_money
            positions_after_buy = round(positions_money_after_buy/total_assets,4)
            
            if available_money > buy_money and positions_after_buy < self.max_positions:
                return True
            else:
                return False
        else:
            msg = f'标的 {code} 调用broker申请资金时出错,money_info 变量为空'
            log_console(msg)
            return False
    
    def assets_record(self) -> None:
        """
        资产记录,每日盘后调用,记录资产明细
        """
        df = pd.read_csv(Sys_path.ASSETS_RECORD_PATH,index_col=0)
        if not df.empty:
            last_date = pd.Timestamp(df.tail(1)['date'].values[0]).to_pydatetime().date()
            if last_date != date.today():
                assets_info = self.get_assets_info()
                if assets_info:
                    df = df.append({
                        'date': date.today(),
                        'total_assets': assets_info['total_assets'],
                        'positions_money': assets_info['positions_money'],
                        'positions_profit': assets_info['positions_profit'],
                        'available_money': assets_info['available_money'],
                        'positions': assets_info['positions']
                    },ignore_index=True)
                    df.to_csv(Sys_path.ASSETS_RECORD_PATH)
        else:
            assets_info = self.get_assets_info()
            if assets_info:
                df = df.append({
                    'date': date.today(),
                    'total_assets': assets_info['total_assets'],
                    'positions_money': assets_info['positions_money'],
                    'positions_profit': assets_info['positions_profit'],
                    'available_money': assets_info['available_money'],
                    'positions': assets_info['positions']
                },ignore_index=True)
                df.to_csv(Sys_path.ASSETS_RECORD_PATH)

QThinker

前地产从业者,假装是个程序员,热爱编程与交易 自研QThinker量化交易框架

文章评论