【Grasshopper基础5】在GH里看基金? —— 简单电池项目实战

经过前面【Grasshopper基础1~4】的介绍,相信读者已经了解了如何在Visual Studio里创建电池、如何获取数据、如何传出数据。

那么在了解这些原理之后,就让我们来一起实现一个小的项目,来看看一个电池从头到尾到底如何制作吧。

正如标题中所述的,我们今天就来做一个 “在GH里看基金净值” 的小电池,大致就是实现一个“给定基金代码,通过网络API调用获取基金净值信息,并输出该基金的所有净值信息”。要实现这个功能,首先我们需要首先进行需求分析,然后确定实现的逻辑,最后进行代码编写和简单的测试。


需求分析

“给定基金代码”
一般而言,基金代码都是整型数值,但是考虑到日常使用的时候,基金代码前可能会包含数字0,故认为它会是整型或者是字符串。
不过,我们在前文(【Grasshopper基础3】)中已经了解到了,由于GH里字符串(GH_String)和整型(GH_Integer)会自动进行隐式转换,其实不需要担心到底会是整形还是字符串,只需确定我们代码最终接受的类型,GH会在输入参数中自动帮我们进行类型转换。

“输出该基金的所有净值信息”
基金净值是浮点型,但是由于需要获取所有净值信息,包含历史上基金存在时所有的值,所以对应每一个基金代码应该会附带一个列表的信息。净值是浮点型。

经过上述分析,

  1. 电池的输入端需要一个参数,可以是整型或字符串,这个项目我们就直接采用整型;
  2. 输出端需要一个参数,是浮点型;
  3. 输入和输出直接的对应关系是一个整型输入对应一个浮点型列表,确定输入端的参数的Access属性为item,输出端的参数的Access属性为list

此时可以开一个Visual Studio,新建一个Grasshopper电池项目,然后将我们的输入、输出端的参数相关代码放入了。

protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
{
    pManager.AddIntegerParameter("Code", "C", "", GH_ParamAccess.item);
}
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
    pManager.AddNumber("DayValues", "v", "", GH_ParamAccess.list);
}

电池内部实现逻辑

这部分是整个电池项目中最耗时的部分,也就是在电池类的 SolveInstance 中处理输入的基金代码信息,并给出最终的净值浮点数列表。

首先获取基金代码:

protected override void SolveInstance(IGH_DataAccess DA)
{
    // 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
    int code = int.MinValue;
    bool Success = DA.GetData(0, ref code);
    if(!Success) return;
}

然后就是对基金净值API的调用,这里调用的是 “小熊同学”网站的基金API。这个API的返回值是一个json字符串,里面包含基金的名称、代码、净值等信息,详细的API文档可在网站中找到。

现在的问题就聚焦到两个部分了,一个是如何发送网络请求,另一个是如何在返回的json字符串中提取我们需要的信息。

网络请求的发送

由于我们是在Grasshopper中实现该逻辑,所以运行环境是 .NET Framework 4.5。在网络上找资料时也需要时刻注意运行环境。目前.NET生态已经布局到全平台,很多相关网络请求的资料找到的都是 .NET Core 或者 .NET 的,而 .NET Framework 与它们略有不同,依赖项可能会不一样。

(具体 .NET / .NET Core / .NET Framework 三者有什么区别和联系读者们也可以自行探索一下,相关历史介绍还是挺多的)

在 .NET Framework 框架下,网络请求的发送可以使用 System.Net 命名空间里的 WebRequest 类实现。

protected override void SolveInstance(IGH_DataAccess DA)
{
    // 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
    // .... ... .. . 略
    // 建立请求
    var req = WebRequest.Create(@"https://api.doctorxiong.club/v1/fund/detail?code=" + code.ToString());
    req.Method = "GET";
    req.Headers.Add(HttpRequestHeader.AcceptCharset, "utf-8");
    // 发起请求,将请求返回保存至res中
    var res = req.GetResponse();
}

返回的Json字符串解析

接下来,就需要对返回的json字符串进行解析了。考虑到现在网络编程的便利性,json字符串几乎已经成为网络编程中数据往来的标准格式,所以json字符串的解析就不需要手写了,.NET 框架已经自带了json字符串解析的功能(System.Text.Json),可以自动将json字符串封装成一个 C# 的object提供原生访问,十分方便。

但是,这个 System.Text.Json.NET Framework 中是没有的!所以我们需要用到它的前生 Newtonsoft.Json。不过这个dll需要手动添加,具体添加方法为

  1. 在Visual Studio的项目管理器中找到引用(References)并右键单击,选择添加引用
  2. 弹出窗口中,左上角选择 Assemblies -> Extensions
  3. 找到并选择 Json.NET,单击确定添加,此时引用中出现 Newtonsoft.Json

在这里插入图片描述

引用添加成功后我们就可以在代码中加入命名空间

using Newtonsoft.Json;

此时我们离解析成功仅差一步之遥了。

前面提到用使用Newtonsoft.Json可以自动把json字符串转换为一个可以操作的对象实例,其前提是,这个对象实例我们需要有这个对象的数据模型

想象json是一堆货物,Newtonsoft.Json可以帮忙装货,但是它需要知道每种货物需要被装在什么位置,我们需要指明数据会被装在什么对象的什么属性中。

数据模型其实就是一个类,我们需要依据json的格式去构建这个类的属性,下面就直接给出笔者建立的数据模型。笔者在这里使用的是struct结构体,读者也可以自行换成class关键字。

// 数据模型中每一个属性都对应着json字符串中的标签
// 数据模型可以不包含所有的json字符串标签,不包含的数据会被忽略掉
public struct ResponseData
{
    public HttpStatusCode Code { get; set; }
    public string Message { get; set; }
    public Fund Data { get; set; }
    public string Meta { get; set; }
    public struct Fund
    {
        public string Code { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }
        public float NetWorth { get; set; }
        public float ExpectWorth { get; set; }
        public float TotalWorth { get; set; }
        public string ExpectGrowth { get; set; }
        public string DayGrowth { get; set; }
        public string LastWeekGrowth { get; set; }
        public string LastMonthGrowth { get; set; }
        public string LastThreeMonthsGrowth { get; set; }
        public string LastSixMonthsGrowth { get; set; }
        public string LastYearGrowth { get; set; }
        public string BuyMin { get; set; }
        public string BuySourceRate { get; set; }
        public string BuyRate { get; set; }
        public string Manager { get; set; }
        public string FundScale { get; set; }
        public string NetWorthDate { get; set; }
        public string ExpectWorthDate { get; set; }
        public string[][] NetWorthData { get; set; }
        public string[][] TotalNetWorthData { get; set; }
    }
}

有了这个数据模型,我们就可以直接对返回的json字符串进行解析,代码如下。MemoryStream类需要用到命名空间System.IOEncoding类需要用命名空间System.Text

protected override void SolveInstance(IGH_DataAccess DA)
{
    // 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
    // 建立请求
    // 发起请求,将请求返回保存至res中

    // 准备解析
    string jsonString;
    using (var responseStream = res.GetResponseStream()) // 获取返回流
    {
        // 在内存中创建临时存储用来存储返回流数据
        using (var ms = new MemoryStream()) 
        {
            // 返回流数据复制至内存中
            responseStream.CopyTo(ms);  
            // 用UTF8解码返回流数据
            jsonString = Encoding.UTF8.GetString(ms.ToArray()); 
            // 解析json字符串
            var resData = JsonConvert.DeserializeObject<ResponseData>(jsonString);
        }
    }
}

最终我们获得了变量 resData 就是最终我们解析得到的数据,它是一个 ResponseData 结构体的实例,我们可以在后续传出数据时方便地通过该结构体获得数据。

电池数据传出

下面的事情就又变得简单了起来,由于我们的输出是对应一个数据列表,我们仅需从 resData 中提取到我们想要的基金净值数据,存储在一个 List<double> 中,然后借由 DA.SetDataList()方法传出即可。

通过基金API可知,基金净值信息包含在 NetWorthData 中,每个NetWorthData包含4个数据,分别是 “日期、净值、日涨跌、额外信息”。我们所需要的净值在该列表的第二项。

又因为所有的数据在数据模型中是以字符串形式给出,我们需要使用double.Parse来进行格式转 …… 等等……GH可以自动隐式转换,我们不如直接输出字符串试试看?

protected override void SolveInstance(IGH_DataAccess DA)
{
    // 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
    // 建立请求
    // 发起请求,将请求返回保存至res中

    // 准备解析
    string jsonString;
    using (var responseStream = res.GetResponseStream()) // 获取返回流
    {
        // 在内存中创建临时存储用来存储返回流数据
        using (var ms = new MemoryStream()) 
        {
            // 返回流数据复制至内存中
            // 用UTF8解码返回流数据
            // 解析json字符串

            // 提取每天的净值信息,放入列表中
            List<string> dayValues = new List<string>();
            foreach (var item in resData.Data.NetWorthData)
                dayValues.Add(item[1]);
            // 传出数据
            DA.SetDataList(0, dayValues);
        }
    }
}

这样我们的电池就完工了!下面赶紧让我们来试试看把。(文章最后有完整cs文件的代码可供参考)


代码测试

首先要做的是在Rhino中使用 GrasshopperDevelopSetting 命令加入我们电池的输出目录。具体细节可见【Grasshopper基础1】,本文在此不再复述。设置完成后关闭Rhino。

点击运行,让我们测试一下自己的电池吧!

Record_2021_01_24_16_29_43_247

但是作为测试内容,我们不能总是期待会给出正确的基金代码,换一个思路,当我们瞎输入一个实际不存在的基金代码时……

Record_2021_01_24_16_34_57_619

报错了!

Error converting value {null} to type ******

解读一下发现,原来是我们在做解析的时候并没有做空值判断。因为如果基金代码不存在的话,API返回的值肯定是带有错误信息的,其中的Data值肯定是空,我们忽略了这一点,所以自然就报错啦。

简单处理一下,把这段放在try-catch中,并添加一个错误信息,直接返回

protected override void SolveInstance(IGH_DataAccess DA)
{
    // 获取基金代码,检测获取是否成功,若不成功直接退出电池执行
    // 建立请求
    // 发起请求,将请求返回保存至res中

    // 准备解析
    string jsonString;
    using (var responseStream = res.GetResponseStream()) // 获取返回流
    {
        // 在内存中创建临时存储用来存储返回流数据
        using (var ms = new MemoryStream()) 
        {
            // 返回流数据复制至内存中
            // 用UTF8解码返回流数据
            // 解析json字符串
            ResponseData resData = default(ResponseData);
            try
            {
                resData = JsonConvert.DeserializeObject<ResponseData>(jsonString);
            }
            catch (Exception e)
            {
                AddRuntimeMessage(GH_RuntimeMessageLevel.Error, e.Message);
                return;
            }
            // 提取每天的净值信息,放入列表中
            // 传出数据
        }
    }
}

Record_2021_01_24_16_45_12_196

这回就对错误有了一些简单的错误处理逻辑了。当然,通过查阅API,还可以在返回的数据结构中针对不同种类的错误给出不同的提示,这里限于篇幅就不展开了。


总结

通过这个小电池,我们将前面1~4的内容进行了一个串联,从如何收集数据、实现自己的逻辑,到最后传出数据,进行简单的测试和Debug。涉及到的点大致有下面几个内容

  1. 输入/输出参数的确定,以及它们的类型,相关的GH特有的隐式类型转换
  2. WebAPI 发起一个网络请求
  3. 对返回的json字符串进行解析
  4. 简单的测试

其中,json字符串解析部分本文并没有详细展开,因为它本身就能做一个超级大的内容了,这里只给出了一个具体的应用。想要详细了解的读者可以在CSDN上直接搜索(点击右侧) Newtonsoft.Json 就可以出来更多的资料了。

另外,网络请求相关的内容,读者在搜索额外资料时请注意适用环境,.NET / .NET Core 是一类环境,.NET Framework 与它们是有一些差别的。

最后,部分代码涉及到了 流(Stream) 的概念,这是有关于I/O相关的内容。初步理解的话就是可以认为“流”是一系列函数,最后的数据会在所有的中间的流中过一遍,也就是实现了对整个数据流的依次处理。读者感兴趣的话可以自行继续深入学习。

电池cs文件的全部代码

using Grasshopper.Kernel;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;

namespace DigitalCrab.Grasshopper
{
    public struct ResponseData
    {
        public HttpStatusCode Code { get; set; }
        public string Message { get; set; }
        public Fund Data { get; set; }
        public string Meta { get; set; }
        public struct Fund
        {
            public string Code { get; set; }
            public string Name { get; set; }
            public string Type { get; set; }
            public float NetWorth { get; set; }
            public float ExpectWorth { get; set; }
            public float TotalWorth { get; set; }
            public string ExpectGrowth { get; set; }
            public string DayGrowth { get; set; }
            public string LastWeekGrowth { get; set; }
            public string LastMonthGrowth { get; set; }
            public string LastThreeMonthsGrowth { get; set; }
            public string LastSixMonthsGrowth { get; set; }
            public string LastYearGrowth { get; set; }
            public string BuyMin { get; set; }
            public string BuySourceRate { get; set; }
            public string BuyRate { get; set; }
            public string Manager { get; set; }
            public string FundScale { get; set; }
            public string NetWorthDate { get; set; }
            public string ExpectWorthDate { get; set; }
            public string[][] NetWorthData { get; set; }
            public string[][] TotalNetWorthData { get; set; }
        }
    }

    public class GetStock : GH_Component
    {
        public GetStock()
          : base("GetStock", "GS",
              "Description",
              "Params", "DigitalCrab")
        {}
        protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
        {
            pManager.AddIntegerParameter("Code", "C", "", GH_ParamAccess.item);
        }
        protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
        {
            pManager.AddNumberParameter("DayValues", "v", "", GH_ParamAccess.list);
        }
        protected override void SolveInstance(IGH_DataAccess DA)
        {
            int code = int.MinValue;
		    bool Success = DA.GetData(0, ref code);
		    if(!Success) return;
            var req = WebRequest.Create(@"https://api.doctorxiong.club/v1/fund/detail?code=" + code.ToString());
            req.Method = "GET";
            req.Headers.Add(HttpRequestHeader.AcceptCharset, "utf-8");
            var res = req.GetResponse();

            string jsonString;
            using (var responseStream = res.GetResponseStream())
            {
                using (var ms = new MemoryStream())
                {
                    responseStream.CopyTo(ms);
                    jsonString = Encoding.UTF8.GetString(ms.ToArray());
                    ResponseData resData = default(ResponseData);
                    try {
                        resData = JsonConvert.DeserializeObject<ResponseData>(jsonString);
                    }
                    catch (Exception e) {
                        AddRuntimeMessage(GH_RuntimeMessageLevel.Error, e.Message);
                        return;
                    }
                    List<string> dayValues = new List<string>();
                    foreach (var item in resData.Data.NetWorthData) dayValues.Add((item[1]));
                    DA.SetDataList(0, dayValues);
                }
            }
        }
        protected override System.Drawing.Bitmap Icon => null;
        public override Guid ComponentGuid => throw new NotImplementedException("请自行申请GUID并替换");
    }
}
  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值