定期レバ調整比較
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