作为一根韭菜,很重要的当然是盯盘,这时候你是要在上班的时候掏出手机看还是打开网页看?
作为一根合格的韭菜,答案当然是用命令行了!够低调,同时内容又高度定制化。
而作为一根在 .NET 的生态里讨饭吃的韭菜,我首选的工具是 F#。
F# 是 Functional base 的编程语言,相较于 C#,语法更灵活也更简洁。同时 F# 并不如 Haskell 那般 strict,可以使用多范式去构建程序,所以也可以很方便的使用 .NET 上其他的绝大多数 DLL 库,无缝和 C# 进行交互。换言之,.NET 拥有的能力,F# 都可以有。
相较于 C#,F# 拥有更强大的类型系统。
虽然相对于其他 OOP 风格的语言来说,C# 已经有了 LINQ 这样的大杀器,但是 F# 借助 computation expression 的扩展,不但支持类 LINQ 的语法 Query Expression,而且更强大,也更易于扩展。
同样得益于 computation expression,F# 有更好的异步支持 Async Workflow
简单讲就是 C# 有的 F# 都有,同时 F# 可以更简洁。
进入正题,首先,需要去创建一个新的 Project。
dotnet new console -lang F# -o TStock
这里的 -lang
指定使用的编程语言是 F#, -o
指定了项目目录名称。
然后在生成的 Program.fs
中可以看到如下代码
// Learn more about F# at http://fsharp.org
open System
[<EntryPoint>]
let main argv =
printfn "Hello World from F#!"
0 // return an integer exit code
使用 dotnet run
执行可以得到输出 Hello World from F#!
。
paket 是 F# 社区实现的一款包管理工具,相较于 nuget
来说更灵活一些,可以同时用来管理 nuget
和 github
上的依赖。
choco install paket
如果没有安装过 paket
,可以用 chocolatey 方便的安装 paket
, 安装好之后,需要在项目中初始化 paket
paket init
这是时候项目中多出了 .paket
, .paket-files
, paket.dependencies
。
在 paket.dependencies
中写入依赖如下
source https://www.nuget.org/api/v2
nuget FSharp.Core
nuget FSharp.Data
nuget FSharp.Json
使用 .\paket\paket.exe install
安装依赖。
这时候声明的依赖都安装到了 packages
目录下,那么怎么使用呢?
在和 *.fsproj
同目录的地方创建一个 paket.references
文件,里面写入这个项目的依赖
FSharp.Core
FSharp.Data
FSharp.Json
这主要是因为考虑到通常一个 solution 下面需要很多个 project 的情况。
再跑一次 .\paket\paket.exe install
, .paket
目录下多了一个 Paket.Restore.targets
文件,fsproj 文件中也多出了一行
<Import Project=".paket\Paket.Restore.targets" />
通过这个文件,就可以在项目中引用到安装的依赖了。
先保留 main 函数里面的部分不动,开始代码逻辑部分的处理。
type Code = Code of string
type StockCode =
| SH of Code
| SZ of Code
| HK of Code
| NSDQ of Code
type Stock = {
Name: string
Code: StockCode
}
Code
是股票编码,但是 A 股、港股、美股等都有不同情况要处理,如何进行区分?所以我引入了 StockCode
的 union type,这样就有办法描述更多的股票了。
如果用 C# 会是什么情况?
class BaseCode
{
private string _code { get; set; }
public string Code
{
get() {
return _code;
}
}
Sz(string code)
{
_code = code
}
public string ToString()
{
return Code;
}
}
class SHCode: BaseCode {
SHCode(code: string): base(code) {}
public override string ToString()
{
return "sh" + Code;
}
}
class SZCode: BaseCode
{
/// blabla
}
class HKCode: BaseCode
{
/// blabla
}
/// blabla
接下来就是数据部分了,这部分不是我的重点,很多网站都有股票实时数据,比如新浪股票,都是可以的选择。值得注意的是,这些网站都加了反爬虫的技术,另一方面,股票数据要实时刷新,为了逻辑上统一,股票数据一般都是由异步接口提供,jsonp 或者 json,具体的接口不同的网站不同,需要自己去分析。
找到接口之后,就可以愉快的拿数据来玩耍了。
type StockData = {
Price: float
Open: float
High: float
Low: float
UpDown: float
UpDownRate: float
ExchangeRatio: float
}
type StockResult = {
Stock: Stock
Data: StockData option
}
这里 StockData
定义了拿到的数据格式,StockResult
定义了完整的返回结果,将数据信息和股票信息关联起来,方便后面的处理。
let getStockData (stock: Stock) =
async {
let code = stock.Code
let baseURL = sprintf "http://%s.%s/%s/quotelist" (getHostPrefix code) baseHost (getRegion code)
let url = sprintf "%s?code=%s&column=%s&callback=%s" baseURL (getCodeString code) column callback
let! resp = Http.AsyncRequest url
if resp.StatusCode > 300 then
return { Stock = stock; Data = None }
else
let stockData = resp.Body.ToString() |> parseData code
return { Stock = stock; Data = stockData }
}
这里的逻辑很简单,就是拿到传入一只股票的基本信息,根据股票的 code 信息拼出完整 url,再通过一个异步请求去获取数据。这里的异步请求本身可能因为各种原因失败,所以这里的 Data 是一个 Option
类型的结果。
我这里取的是一个 jsonp 的请求,不能直接 deserialize 成一个对象,所以用了一个额外的 parseData 方法来处理数据
let parseData code (text: string) =
try
let valuePart = text.Split(":").[1]
let unit = match code with
| HK _ -> 1000.0
| _ -> 100.0
valuePart.Substring(3, valuePart.Length - 10).Split(",")
|> Seq.map (float >> (fun x -> x / unit))
|> Seq.toArray
|> fun s ->
Some {
Price = s.[0]
Open = s.[1]
High = s.[2]
Low = s.[3]
UpDown = s.[4]
UpDownRate = s.[5]
ExchangeRatio = s.[6]
}
with
| _ -> None
这里 parse 本身是一个极容易发生错误的过程,返回结果不对,或者返回结果不规范都有可能导致失败,由于我不关心错误原因,所以只要错误返回 None 就可以了,try 里面只关心正常的逻辑,这样可以让代码更好读一点。
这里面有两个奇怪的符号 |>
、 >>
,它们本质上都是函数,|>
的定义是 ( |> ): 'T1 -> ('T1 -> 'U) -> 'U
,即将左侧的参数 apply 到右侧的函数中,返回其结果,类似于一个管道,将数据流处理通过一个个函数串联起来,基本上是 fsharp 中最常见的符号了。>>
的定义是 ( >> ) : ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3
,即 compose 函数,将两个函数组装成一个函数。
还有一个比较 tricky 的事实是,fsharp 去处理这样的中缀符号的结合性的时候,是通过符号的形状来决定的,而不是像 Haskell 那样去显式的声明其结合性和优先级。
有了这些方法之后,主要功能就基本上完成了。最后来更改 main
函数。
[<EntryPoint>]
let main argv =
let stocks = [
{
Name = "MSFT";
Code = NSDQ (Code "MSFT")
}
]
stocks
|> Seq.map getStockData
|> Async.Parallel
|> Async.RunSynchronously
|> Seq.iter
(fun result ->
match result with
| result when result.Data = None -> printfn "|%-20s|%10s|%10s|%10s|%10s|%10s|%10s|" result.Stock.Name "_" "_" "_" "_" "_" "_"
| { StockResult.Stock = stock; StockResult.Data = Some data } ->
printfn "|%-20s|%10.2f|%10.2f|%10.2f|%10.2f|%10.2f|%10.2f%%|" stock.Name data.Price data.Open data.Low data.High data.UpDown data.UpDownRate
| _ -> ()
)
0
这里有使用 printfn
将最后拿到的结果打印成表格呈现到终端。
为了让代码更灵活,这里把 stocks
放到一个外部 json 文件中,通过命令行参数进行捕获。这里我用到的是 FSharp.Json
这个库。
首先需要给最初定义的 StockCode
和 Stock
类型加一些辅助的 attribute。这里涉及到一个 union type 的映射问题,当然,这里的代码是非常直白的。
open FSharp.Json
[<JsonUnion(Mode=UnionMode.CaseKeyAsFieldValue, CaseKeyField="type", CaseValueField="code")>]
type StockCode =
| SH of Code
| SZ of Code
| HK of Code
| NSDQ of Code
type Stock = {
[<JsonField("name")>]
Name: string
[<JsonField("code")>]
Code: StockCode
}
然后是更改我们的 main 函数。通过 argv
传入 filename,再通过 File
把内容读取到程序中,最后通过 FSharp.Json
deserialize 成我们需要的结构,其他统统保持不变。
[<EntryPoint>]
let main argv =
let filename = argv.[0]
let content = File.ReadAllText(filename)
let stocks = Json.deserialize<Stock[]>(content)
// ...
下面是 stocks.json
文件的一个示例。
[
{
"name": "MSFT",
"code": {
"type": "NSDQ",
"code": "MSFT"
}
}
]
以上,就是整个应用的主要实现了,当然为了节省篇幅,中间省去了一些细枝末节。
完整的代码可以去访问我的 Github 查看。