如何构建回测系统(八)

如何构建回测系统(八)

代码升级问题

回测系统必然涉及策略的反复测试及重新部署

重写ClassLoader

为了将代码存放在不同的位置,我们需要重写ClassLoader用以加载二进制代码,我们选用数据库作为代码的存放处,方便查询

重写Classloader中findClass函数

需要注意关键点是,defineClass对同一个class只能解析一次(JVM限制),所以我们需要解析后将Class用Map缓存起来

如果需要更新同名class,需要将Classloader丢弃掉,并新建一个ClassLoader实例(JVM在认为一个相同的Class是指相同的Classloader+全限名则是相同)

实现JavaCompilerInMemory

为了让前端页面能动态的修改class类,需要将.java的内容通过Java代码进行编译
参考代码:

    public byte[] compile() {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        var diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
        var fileManager = new MyJavaFileObjectManager(compiler.getStandardFileManager(diagnosticCollector, null, StandardCharsets.UTF_8));
        var javaFileObjects = Collections.singletonList(new MyJavaFileObject(getName(), codes, byteArrayOutputStream));
        fileManager.outputStream = byteArrayOutputStream;

        var compilerTask = compiler.getTask(null, fileManager, diagnosticCollector, null, null, javaFileObjects);
        if (compilerTask.call()) {
            return byteArrayOutputStream.toByteArray();
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            diagnosticCollector.getDiagnostics().forEach(diagnostic -> stringBuilder.append(diagnostic.getMessage(Locale.CHINESE)));
            throw new RunException(stringBuilder.toString());
        }
    }

    static class MyJavaFileObjectManager extends ForwardingJavaFileManager<JavaFileManager> {

        Map<String, JavaFileObject> fileObjects = new HashMap<>();
        OutputStream outputStream;

        public MyJavaFileObjectManager(JavaFileManager fileManager) {
            super(fileManager);
        }

        @Override
        public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {
            JavaFileObject javaFileObject = fileObjects.get(className);
            if (javaFileObject == null) {
                return super.getJavaFileForInput(location, className, kind);
            }
            return javaFileObject;
        }

        @Override
        public JavaFileObject getJavaFileForOutput(Location location, String qualifiedClassName, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
            JavaFileObject javaFileObject = new MyJavaFileObject(qualifiedClassName, kind, outputStream);
            fileObjects.put(qualifiedClassName, javaFileObject);
            return javaFileObject;
        }
    }

    static class MyJavaFileObject extends SimpleJavaFileObject {
        String source;
        OutputStream outputStream;
        String name;

        public MyJavaFileObject(String name, Kind kind, OutputStream stream) {
            super(URI.create("String:///" + name + kind.extension), kind);
            this.outputStream = stream;
        }

        public MyJavaFileObject(String name, String source) {
            super(URI.create("String:///" + name + Kind.SOURCE.extension), Kind.SOURCE);
            this.source = source;
            this.name = name;
        }

        public MyJavaFileObject(String name, String source, OutputStream outputStream) {
            this(name, source);
            this.outputStream = outputStream;
        }

        public MyJavaFileObject(URI uri, Kind kind) {
            super(uri, kind);
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return source;
        }

        @Override
        public OutputStream openOutputStream() throws IOException {
            return outputStream;
        }
    }

最终

通过CURD将上述的ClassLoader和JavaCompilerInMemory通过自己的业务穿起来,就可以实现在网页编写java代码,并动态修改和运行

socket编程日记

socket编程日记

自定义分割包协议

注意到,socket发送出去的包最大只能为tcp的mtu长度减去包头长度,超过长度的包会分批到达

TCP保证了包到达的顺序及正确性

UDP需要自己实现包顺序及异常校验

对端如果没有分包协议会导致一次性读过头或者没读完就进行数据操作

分包协议的两种方式

包头放置数据

  • 特征值
  • 数据长度
  • 数组字段
接收端伪代码形式
header{
    len
}
while(1){
    buf[64]
    socket.recv(buf)
    read(&header,buf)
    data = new data[header.len]
    readLen = readRemainBuf(buf,data)
    socket.recv(data,(header.len-readLen))
}

包尾放置分隔符

  • 数据字段
  • 分隔符
while(1){
    buf
    socket.recvAndExpend(buf)
    while not found delimiter:
        socket.recvAndExpend(buf)
        if found delimiter:
            set delimiterIndex
    return buf[0:delimiterIndex]
}

如何构建回测系统(七)

如何构建回测系统(七)

清洗数据

内存不足问题

  1. 单个对象粗略估算(刨除所有的对象辅助数据):438byte
  2. 约有5*60*60*3000条数据
  3. 为了使得在8GB左右的系统中也能顺利运行需要做改进

数据不连续问题

  1. ETF中成分股出现没有交易无法从逐笔还原

改进

  1. 切割导入时间,实践是每三十分钟一个段落导入
  2. 所有需要前置数据可以预留在内存
  3. 做到每个段落指执行读1次数据库,写一次数据库
  4. 将导入任务抽离,并且每进行一次导入保存当前进度以方便过程恢复
  5. 往前搜索直到股票第一次开市价格
  6. 需要保存股票的天数据
  7. 使用现金替代来代替无交易数据

结果

  1. 在8GB系统中,成功完成4GB数据导入
  2. iopv均有数据,但无法验证,下一步想办法验证

如何构建回测系统(六)

如何构建回测系统(六)

数据量问题

  1. 粗略估算,从2000年到目前,若完全采用逐笔构建高精度交易数据,至少需要31TB的存储空间
  2. 内存不可能满足
  3. 采用文件存储搜索不方便,时间复杂度过高

解决方向

  1. 抛弃原有的完全将数据放置于内存中的想法
    1. 引入数据库
    2. 分表存储和读取
    3. 应用层做聚合
  2. 将原来由回测系统每次构建高精度数据,变更为ETL系统做数据的计算和存储,回测系统仅读取,分离其功能

难点

  1. 为提升开发效率使用的是jpa/hibernate,但是jpa的层级过高,不方便做分表分库聚合,需要使用其底层的接口针对需要分表的对象重新构建应用层数据库接口
  2. 剥离了项目之后的依赖关系,及接口关系梳理

如何建立一个回测系统(五)

如何建立一个回测系统(五)

账户系统设计

设计需求

  1. 模拟交易
  2. 模拟费率
  3. 能够记录每次交易时候的状态
  4. 自动计算胜率
  5. 无缝对接到正常交易系统

字段设计

  1. taskUid(记录对应搜索任务)
  2. Datasource(价格记录源)
  3. SimulateMarket(交易场,传入正式则用于正式交易系统,传入模拟则用于回测系统)
  4. actualMoney(已经到账的钱)
  5. bookMoney(账本计算的钱)
  6. beginDate(开户日期)
  7. holdComponents(交易标的物持有状态及价格)
  8. book(每日交易的Order)

行为设计

  1. 平账
  2. 申购
  3. 赎回
  4. 查询持有均价

以上为账户的完备详细设计

效率优先

效率优先

开场白真难,好,开场结束

什么是低效实践

我是以java语言为主力开发语言的代码编写人员,同时还负责我们所有的服务器的部署及维护, 实践中遇到最痛苦的事情莫过于, 精细化管理一切代码行为

为什么MyBatis在我的项目中很糟糕

我们团队一共就五个人,两个做后端,一个前端,一个原型设计,外加一个对外沟通的部门负责人.在一开始的选型中, 选中的框架是使用mybatis+springboot+vue来完成功能,我们主要是采集工厂生产过程中关键点温度的变化,以及保温性能的数据。

对于工业化设备监控来说,开发过程中最大的问题是,我们的模型的字段会在开发过程中不断的更新,于是我们实践中就会将mybatis当成一个Orm框架来使用,基本来说,就是只是用CRUD,每个对象是一张表,操作顺序都是查出对象-》修改对象-》回写数据库,这种半自动的工具在这样的实践中就会带来需要管理的代码实在太多,而且基本为样板代码,更严重的是,对象直接的关系维护必须手工写代码(这种代码基本就是样板代码,其实Hibernate这种框架都已经解决好了),对于一个小团队来说这是一个不可取的行为。没那么多精力,也不应该花那么多精力来维护这些样板代码。

举个栗子

我们对于采集点管理进行建模,首先有区域,然后有管道,接着有测温点信息,最后测温点有测温点数据

然后我们需要维护区域与管道之间的关系,管道与测温点之间的关系,区域与区域之间的关系,测温点数据与测温点之间的关系

一顿操作,需要有四个Dao,对应四个Entity,然后关系(就是Repository的职责)有四个Repository。

以上全是样板代码

提升效率

问题的关键在于如何将我们的开发人员从无趣且繁琐的样板代码实施中解放出来呢,答案在EJB中已经被实践,就是全自动的ORM框架

全自动的ORM带来的是开发人员不用再去做大量重复性工作以维护数据之间的关系和正确性

对于一个我们进行中的项目,采用MyBatis plus方案需要实现四个Dao,四个Repository,还有实现四个Entity,而改用Jpa/Hibernate之后,代码数量减少到四个Repository,无需实现Entity,Dao,而且SpringDataJpa已经可以通过动态代理方式生成代码,Repository是无需实现,只需要声明接口

原先使用了接近5(人/日)的工作量才完成的方案,改用Jpa后只需要0.5(人/日)

需要回避的点

全自动ORM是否有缺陷?

有,一是对象与数据库之间的关系需要靠一套不可变的规则维持,但是代码总是有人维护的,而且Hibernate的实现人并不是我们,若规则变动,我们的数据全部报废

二是全自动的ORM会有解析对象的性能开销,具体到我们项目是,启动时间增加两秒到五秒

三是需要实现一层控制事务的Service

解决方法也很简单,版本别动,启动时间其实无所谓,然后多采用懒加载,事务无非就是多一个Transactional的注解

结语

  1. mybatis不适用于人少的项目,开发人员少的情况下,要优先考虑开发效率
  2. 绝大部分开发人员真的不需要考虑从持久化方案中榨取更多性能,而榨取的通道多数会变成你的维护成本
  3. 性能有绝对要求下应该考虑其他方案而不采用数据库
  4. 真心建议国内的风气变一下,我在调研中发现mybatis最后演化出了mybatisplus的方案,一个四不像的方案,既没有减轻开发人员维护数据库的负担,也没有减少样板代码(mybatisplus依旧要手工维护关系)

JPA+Springboot小限制

JPA+Springboot小限制

限制一

  • 通过JPA(Hibernate)在MySQL环境下自动建表要注意,JPA的默认字段长度为255,而如果数据库中存在索引不能超过768bytes的限制,则需要考虑自己手工维护表

  • 限制来源于JPA或默认新建一个记录序号的表,此表的主键为Varchar(255),且程序员无法修改,只要编码长度是超过三字节编码,则建表失败

  • 可以通过SpringData/Hibernate自动调用classpath下特定的sql文件特性绕过

  • 但如果抛弃了由hibernate做表维护而转为程序员手工维护表关系则不如采用mybatis方案更为直接

限制二

  • hibernate自动建表时候,是没法设置在哪个schame下或者哪个catalog下建表,如果DB的其他schame有同名表,则建表会失败

  • 此限制来源于Hibernate做数据库元数据查询时候只使用了table字段,且我无法找到可以让查询元数据时候指定schame+table联合确定table是否存在的办法

  • 此限制的场景会在Junit测试时候如果想做schame隔离是无法做到的

  • 一并发现此问题的还有activiti的自动建表也是

从业务到代码

从业务到代码

目的

所以我写这篇文章的目的很简单,为了给各位写代码的新手带来一点不同的视角

前言

代码不过是业务的表示,当你把代码看作一门语言(这里语言不是指java/C这些编码语言的意思,而是普适意义上的language)的时候,我们,作为编码人员,更多的是像是陈述事实一样,将业务陈述,然后通过代码落地。

问题

在新手中,经常有这么几个疑问
1. 这么写有什么用
2. 明明我一个函数就可以把所有工作做完,为什么要拆开来
3. 这个工具屌不屌
4. 这个语言如何如何

看法

结论

  1. 如果不能跳出只是代码堆砌工的眼界,那么永远不知道有什么用
  2. 拆分是为了将函数目的单纯化
  3. 工具只是生产力工具的一部分,不能说锤子一定比机床好
  4. 同样,都是锤子,不能说A类型的锤子一定好与B类型锤子

理由

不知道各位呆过几个公司,每家公司给人的感觉都是不一样的。提几个点让大家想一想
1. 有问题能不能找到确定的一个人能解决确定的一类问题
2. 出了事情,背锅是平摊锅还是说加权锅,还是看老板心情分
3. 简单点,如果电脑坏了要换,上报后多久能搞定
4. 工作的成果是否能够量化
让我们来换到代码的视角来看这个问题
1. 出了bug能否容易精确定位在什么类
2. 出错了,整个系统的其他部分是否依旧稳定
3. 某个部件如果因为市场因素全部重做,重做的成本是低是高
可能换到编码视角,各位一定能够简单的提出一些熟悉的概念,比如鲁棒性,比如重构,etc。但是不知道各位有没有做过思考,亦即这些概念套用到非编码视角的时候也是通用的,更或者说,这概念都是从其他行业或者领域移植到软件开发领域,毕竟软件开发到今天真正普及也就四十年?

所以

  1. 向业务学习,好的代码应该是和业务一致的
  2. 因为你的代码就是业务时候,业务的快速变动就能变成了代码变动
  3. 而一个公司的业务变动不可能连根本逻辑都改变了
  4. 加班就会少了
  5. 与业务人员沟通也会顺畅了

论目标

论目标

信息安全中经常需要验证一条信息,其做法是,定义一个函数f,f作用于信息得到一条简短的信息,即f(g(x),c) == f(h(x),c) == F 其中,g(x)是发送端的信息,h(x)是接收端信息,c是双方约定的一个信息,F是计算的结果,如果经过f作用后,得到的结果是相同,那么就可以认为,这条信息是真实的。

原理上这东西很简单,也非常实用,那么对应到不是信息安全领域中呢。

举个例子:

  1. 领导说,我们的当前目标是,在合法的范围内挣得一百万
  2. 所有人开始提方案
  3. 每个人根据目标,代入自己的方案,是否满足条件
  4. 如果所有方案都不能满足,和领导讨论是否tradeoff一部分要求

现在我经常性面对的问题是:

  1. 负责的人,提不出一个目标
  2. 执行的人,总能找到各种似是而非的理由,在一定条件下这又是成立的

举个例子:

  1. 提出做一个系统
  2. a提出A方案,方案A优点是1,2,3,4,5
  3. 但是大家都不同意,觉得这些操作相当冗余

那么这时候谁对谁错,作为一个负责人,这时候就要想到这么一个事情,这个事情的目标是什么:

  • A1,A2,A3,A4

那么上面的对错就容易判断了,如果a提出的1,2,3,4,5不满足A1,A2,A3,A4,一律是要砍掉,只保留能够满足A1,A2,A3,A4的设计

对应出来我们就得到了类似的和信息安全领域的结果即
f(1,2,3,4,5) == A1,A2,A3,A4
如此我们总能够简单的验证我们的工作成果,而不是陷入无止境的扯皮中

软件开发的三个层级

软件开发的三个层级

  1. 核心领域模型:该层级与实际业务应该一致,举个例子:我们在财务系统中有依据收款信息开票的选项,那么我们的领域模型就应该有个类叫做收款信息,它有一个动作叫做开票
  2. 领域模型对外的交互:到了这个层级,会有很多开源代码库帮助我们,例如我们需要发票对象需要对外暴露,那么通过mvc的形式,可以对外暴露发票的信息,或者是发票需要做持久化,有各种各样的Persistence的工具帮助我们
  3. 底层:例如数据库字段长度,报文的payload等等,到了这一层,应该是一个很具体的问题,具体来说,在这一层级不应该有任何业务相关的知识,而完全是计算机领域的知识

通过对软件开发进行划分层级,可以帮助我们开发过程中渐进式开发
比如我要关注业务代码的时候,就纯粹的业务思维,如果我是底层的开发,我只需要关注计算机领域的知识

为什么要这么划分

我在开发中观察到,大部分新手或者开发了很久的人,在写代码的时候,过早的关注到细节问题,比如写开发票的时候就开始关注到发票数据库字段长度,就直接陷进了细节中,具体对外表现出来就是业务实现统统一个函数打包,甚至还有全局变量等等
渐进式的分层开发有助于我们专注于一件事件,只有专注了,那么开发中错误才能减少