Last active
September 14, 2020 01:33
-
-
Save jitenspin/4cb60aa068f5397e8a52e18ecf9e86d3 to your computer and use it in GitHub Desktop.
定期レバ調整比較
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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