読者です 読者をやめる 読者になる 読者になる

先日ちらっと書いていた日本個別株をDBに放り込んだ話の続きで、簡単な売買シュミレーションをしてみようかと。

ロジックの記述方法はS式でも使えば面白いのではないかと思い、Sexpモジュールを使ってみることにしました。

それにしてもこのモジュール名は少し恥ずかしい気がするのです。
「あんれまぁ、ケン坊!! なんだいこのセ…、な、なんとかちゅうのは?」「あ、お、おばあちゃん、こ、これは、あれだよ、全然変な意味じゃなくって…ね、え、えーっと、S式のことだよ」「あぃやぁ、Lispで使っているあの丸っこいめんこいやつかねぇ、ばあちゃんてっきり…」とかありそうな気がします。


で、とりあえずそれらしいロジック記述方法っぽいものを考えないと先に進まないので、適当にひねり出してみます。

(((cond (TimeOpen)) (action Buy Open 0))
 ((cond (Price Ge Open +1.2)) (action Sell Open +1.2))
 ((cond (Price Lt Open -1.5)) (action Sell Open -1.5))
 ((cond (TimeClose)) (action Sell Close 0)))

condの引数に条件(TimeXXXは寄りと引けのイベント、Priceイベントは指値)のリストを書いておいて、論理積で成立した時にactionの売買を実行するみたいなものでしょうか(自分で書いておいて自信無し)。数値は株価のパーセンテージ。そして、これがOCamlのデータ構造に変換されれば良いと。なんだか冗長な感じがしてきましたが、まぁ練習がてらということで。

で、そもそもパーサーの書き方とか良く分からないという衝撃の事実と戦いながら書いてみたのがこちら。

#use "topfind"
#require "sexplib"

open Sexplib
open Sexplib.Sexp

type price = Open of float   | Top of float
           | Bottom of float | Close of float

type price_sig = Ge of price | Gt of price
               | Le of price | Lt of price

type cond = TimeOpen | TimeClose | Price of price_sig

type action = Buy of price | Sell of price

type elm = Conds of cond list | Action of action
         | Record of cond list * action
         | Records of (cond list * action) list

let to_price s v =
  let v = float_of_string v in
  match s with
  | "Open"   -> Open v   | "Top"    -> Top v
  | "Bottom" -> Bottom v | "Close"  -> Close v
  | _ -> failwith "invalid price"

let to_price_sig s pr =
  match s with
  | "Ge" -> Ge pr | "Gt" -> Gt pr | "Le" -> Le pr | "Lt" -> Lt pr
  | _ -> failwith "invalid oprator"

let to_action s pr =
  match s with
  | "Buy"  -> Buy pr | "Sell" -> Sell pr
  | _ -> failwith "invalid action"

let get_conds = function
  | Conds xs -> xs | _ -> failwith "invalid conditions"

let get_action = function
  | Action x -> x | _ -> failwith "invalid action"

let get_record = function
  | Record (c, a) -> (c, a) | _ -> failwith "invalid record"

let cond_parse = function
  | List ([Atom "TimeOpen"]) -> TimeOpen
  | List ([Atom "TimeClose"]) -> TimeClose
  | List (Atom "Price"::Atom op::Atom pr::[Atom v]) ->
    let price = to_price pr v in
    let price_sig = to_price_sig op price in
    Price price_sig
  | _ -> failwith "invalid condition"

let parse s =
  let rec parse s =
    match s with
    | List (Atom "cond"::conds) ->
        Conds (List.rev_map (fun x -> cond_parse x) conds)
    | List (Atom "action"::Atom act::Atom pr::[Atom v]) ->
        let price = to_price pr v in
        let act = to_action act price in
        Action act
    | List (conds::[action]) ->
        let conds = get_conds (parse conds) in
        let action = get_action (parse action) in
        Record (conds, action)
    | List (rs) ->
        Records (List.map (fun r -> get_record (parse r)) rs)
    | _ -> failwith "invalid syntax"
  in
  let lex = Lexing.from_string s in
  parse (scan_sexp lex)

let sample = "
(((cond (TimeOpen)) (action Buy Open 0))
 ((cond (Price Ge Open +1.2) (Price Le Top -0.5)) (action Sell Open +1.2))
 ((cond (Price Lt Open -1.5)) (action Sell Open -1.5))
 ((cond (TimeClose)) (action Sell Close 0)))
"
let parsed = parse sample

う〜ん、型の使い方が変な気がしてきました。これをトップレベルで読み込んでみると

   :
   :
val parsed : elm =
  Records
   [([TimeOpen], Buy (Open 0.));
    ([Price (Le (Top (-0.5))); Price (Ge (Open 1.2))], Sell (Open 1.2));
    ([Price (Lt (Open (-1.5)))], Sell (Open (-1.5)));
    ([TimeClose], Sell (Close 0.))]

お、何だか変換できたみたい。だけど、このフォーマットが使えるかは別問題ですねぇ。まぁいいか。