Skip to content

Instantly share code, notes, and snippets.

@jitenspin
Last active September 14, 2020 01:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jitenspin/4cb60aa068f5397e8a52e18ecf9e86d3 to your computer and use it in GitHub Desktop.
Save jitenspin/4cb60aa068f5397e8a52e18ecf9e86d3 to your computer and use it in GitHub Desktop.
定期レバ調整比較
package main
import (
"encoding/csv"
"os"
"strconv"
)
type HistoricalData struct {
date string
open float64
close float64
high float64
low float64
}
func ReadYahooData(path string) ([]*HistoricalData, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
r := csv.NewReader(f)
var line []string
acc := []*HistoricalData{}
_, err = r.Read()
if err != nil {
return nil, err
}
for {
line, err = r.Read()
if err != nil {
break
}
open, err := strconv.ParseFloat(line[1], 64)
if err != nil {
return nil, err
}
high, err := strconv.ParseFloat(line[2], 64)
if err != nil {
return nil, err
}
low, err := strconv.ParseFloat(line[3], 64)
if err != nil {
return nil, err
}
close, err := strconv.ParseFloat(line[4], 64)
if err != nil {
return nil, err
}
d := &HistoricalData{
date: line[0],
open: open,
close: close,
high: high,
low: low,
}
acc = append(acc, d)
}
return acc, nil
}
package main
import (
"fmt"
"io"
"log"
"os"
"time"
)
func main() {
historicalData, err := ReadYahooData("./spx_daily.csv")
if err != nil {
log.Fatalf("Failed to read daily data")
}
ratios := []float64{1.0, 1.5, 2.0, 2.5, 3.0}
periods := []RebalancePeriod{dailyRebalance, weeklyRebalance, monthlyRebalance, yearlyRebalance}
for _, ratio := range ratios {
for _, period := range periods {
f, err := os.Create(fmt.Sprintf("out/%s_%.1f.tsv", periodToString(period), ratio))
if err != nil {
log.Fatalf("Failed to create tsv")
}
run(f, historicalData, ratio, period, 100.0)
}
}
}
type RebalancePeriod int
const (
dailyRebalance RebalancePeriod = iota
weeklyRebalance
monthlyRebalance
yearlyRebalance
)
// 毎回レバ調整する
func run(w io.Writer, data []*HistoricalData, ratio float64, period RebalancePeriod, initialMargin float64) {
if len(data) == 0 {
return
}
// 最初に実効レバレッジを設定
//
// 実効レバレッジ = 現在価格 * 枚数 / 時価評価総額
// 時価評価総額 = 証拠金 + 評価損益
//
// 枚数は面倒なので float (1未満もあり) とする
price := data[0].open
valuation := initialMargin
size := positionSize(price, valuation, ratio)
requiredMargin := price * size / 10 // 株価指数は10倍までというルールがあるはず
prev := data[0]
marginCallExecuted := 0
for _, d := range data {
// 事前準備
// 月初、年初などの場合は、前日の close でリバランスしたとする
// 時価評価総額は変わらない
if isRebalanceRequired(period, prev.date, d.date) {
size = positionSize(prev.close, valuation, ratio)
requiredMargin = prev.close * size / 10
}
// 実行中
// 追証: 必要証拠金が、時価評価総額を割り込む
// だいたいどこも追証の定義はこれ、のはず
if (valuation+(d.low-prev.close)*size)/requiredMargin < 1 {
log.Printf("margin call is executed: date=%s, period=%s, ratio=%.1f", d.date, periodToString(period), ratio)
valuation = valuation + (d.low-prev.close)*size
size = positionSize(d.low, valuation, ratio)
requiredMargin = d.low * size / 10
valuation = valuation + (d.close-d.low)*size
marginCallExecuted = 1
} else {
valuation = valuation + (d.close-prev.close)*size
marginCallExecuted = 0
}
// TODO: 強制ロスカット
fmt.Fprintf(w, "%s\t%f\t%d\n", d.date, valuation, marginCallExecuted)
// 事後
// for next
prev = d
}
}
func positionSize(price float64, valuation float64, ratio float64) float64 {
// ratio = price * size / margin
// size = ratio * margin / price
return ratio * valuation / price
}
func isRebalanceRequired(period RebalancePeriod, prevDate string, currentDate string) bool {
switch period {
case dailyRebalance:
return prevDate != currentDate
case weeklyRebalance:
layout := "2006-01-02"
prev, err := time.Parse(layout, prevDate)
if err != nil {
log.Fatalf("unreachable")
}
current, err := time.Parse(layout, currentDate)
if err != nil {
log.Fatalf("unreachable")
}
_, prevWeek := prev.ISOWeek()
_, currentWeek := current.ISOWeek()
return prevWeek != currentWeek
case monthlyRebalance:
return prevDate[5:6] != currentDate[5:6]
case yearlyRebalance:
return prevDate[0:3] != currentDate[0:3]
default:
log.Fatalf("unreachable")
return false
}
}
func periodToString(period RebalancePeriod) string {
switch period {
case dailyRebalance:
return "daily"
case weeklyRebalance:
return "weekly"
case monthlyRebalance:
return "monthly"
case yearlyRebalance:
return "yearly"
default:
return "unreachable"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment