]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front.git/blame - api/portfolio.go
Better go import paths.
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front.git] / api / portfolio.go
CommitLineData
24e47979 1package api
2
3import (
4 "fmt"
5 "time"
6
7 "github.com/shopspring/decimal"
1d68446a 8 "git.immae.eu/Cryptoportfolio/Front.git/db"
24e47979 9)
10
11func init() {
12 decimal.MarshalJSONWithoutQuotes = true
13}
14
15const MARGIN_POSITION_SECURED_RATIO = 1.0
16
17const BTC_DIGITS = 3
18
19var MINIMAL_BTC_VALUE_TRESHOLD decimal.Decimal = decimal.NewFromFloat(10.0).Pow(decimal.NewFromFloat(-3.0))
20
21type ValuePerformance struct {
22 Value decimal.Decimal `json:"value"`
23 Variation decimal.Decimal `json:"variation"`
24 VariationP decimal.Decimal `json:"variationP"`
25}
26
27func NewValuePerformance(fromValue, toValue decimal.Decimal) ValuePerformance {
28 variation := toValue.Sub(fromValue)
29
30 return ValuePerformance{
31 Value: toValue,
32 Variation: variation,
33 VariationP: variation.Div(fromValue).Mul(decimal.NewFromFloat(100.0)),
34 }
35}
36
37type PositionType string
38
39const POSITION_SHORT PositionType = "short"
40const POSITION_LONG PositionType = "long"
41
42type PortfolioBalance struct {
43 PositionType PositionType `json:"positionType"`
44 Quantity decimal.Decimal `json:"quantity"`
45 QuantityLocked decimal.Decimal `json:"quantityLocked"`
46 BTCValue decimal.Decimal `json:"BTCValue"`
47 PositionPerformanceP decimal.Decimal `json:"positionPerformanceP"`
48 Weight decimal.Decimal `json:"weight"`
49}
50
51type Portfolio struct {
52 PeriodStart time.Time `json:"periodStart"`
53 PeriodEnd time.Time `json:"periodEnd"`
54 Balances map[string]PortfolioBalance `json:"balances"`
55 Value decimal.Decimal `json:"value"`
56 Performance ValuePerformance `json:"performance"`
57}
58
59func (p Portfolio) Round() Portfolio {
60 p.Value = p.Value.Round(BTC_DIGITS)
61 for currency := range p.Balances {
62 balance := p.Balances[currency]
63 balance.Quantity = balance.Quantity.Round(2)
64 balance.BTCValue = balance.BTCValue.Round(BTC_DIGITS)
65 balance.Weight = balance.Weight.Round(1)
66 balance.PositionPerformanceP = balance.PositionPerformanceP.Round(1)
67 p.Balances[currency] = balance
68 }
69
70 p.Performance.VariationP = p.Performance.VariationP.Round(1)
71 p.Performance.Variation = p.Performance.Variation.Round(BTC_DIGITS)
72 p.Performance.Value = p.Performance.Value.Round(BTC_DIGITS)
73 return p
74}
75
76func GetCurrenciesPerformance(from, to db.ReportTickers) map[string]ValuePerformance {
77 performances := make(map[string]ValuePerformance)
78 currencies := make(map[string]struct{})
79 for currency := range to.Balances {
80 if to.Balances[currency].Abs().LessThan(MINIMAL_BTC_VALUE_TRESHOLD) {
81 continue
82 }
83 currencies[currency] = struct{}{}
84 }
85
86 for currency := range currencies {
87 performances[currency] = NewValuePerformance(from.GetBTCRate(currency), to.GetBTCRate(currency))
88 }
89
90 return performances
91}
92
93func UserMovements(from db.Report, to db.Report) (decimal.Decimal, error) {
94 if from.Tag != db.BUY_END || (to.Tag != db.SELL_BEGIN && to.Tag != db.INTERMADIATE_STATE) {
95 return decimal.Zero, fmt.Errorf("cannot compare reports: '%s' -> '%s'", from.Tag, to.Tag)
96 }
97
98 var deltaBTC decimal.Decimal
99
100 currencies := make(map[string]struct{})
101 for currency := range to.Balances {
102 currencies[currency] = struct{}{}
103 }
104 for currency := range from.Balances {
105 currencies[currency] = struct{}{}
106 }
107
108 for currency := range currencies {
109 balanceFrom := from.Balances[currency]
110 balanceTo := to.Balances[currency]
111
112 delta := balanceTo.Total.Sub(balanceFrom.Total)
113 if !delta.Equals(decimal.Zero) {
114 deltaBTC = deltaBTC.Add(delta.Mul(to.Tickers.GetBTCRate(currency)).Neg())
115 }
116
117 }
118
119 return deltaBTC, nil
120}
121
122// Computes plus-value, ignoring positions took by users.
123func ComputePlusValue(from db.Report, to db.Report) (decimal.Decimal, error) {
124 if from.Tag != db.BUY_END || (to.Tag != db.SELL_BEGIN && to.Tag != db.INTERMADIATE_STATE) {
125 return decimal.Zero, fmt.Errorf("cannot compare reports: '%s' -> '%s'", from.Tag, to.Tag)
126 }
127
128 diff, err := UserMovements(from, to)
129 if err != nil {
130 return decimal.Zero, err
131 }
132
133 return to.Tickers.Total.Sub(from.Tickers.Total).Add(diff), nil
134}
135
136func ComputeWeights(report db.Report) map[string]decimal.Decimal {
137 weights := make(map[string]decimal.Decimal)
138
139 for currency := range report.Balances {
140
141 if report.Tickers.Balances[currency].Abs().LessThan(MINIMAL_BTC_VALUE_TRESHOLD) {
142 continue
143 }
144
145 quantityBlocked := report.Balances[currency].MarginInPosition.Mul(decimal.NewFromFloat(1.0 + MARGIN_POSITION_SECURED_RATIO))
146 weights[currency] = report.Tickers.Balances[currency].
147 Sub(quantityBlocked.Mul(report.Tickers.GetBTCRate(currency))).
148 Abs().
149 Div(report.Tickers.Total).
150 Mul(decimal.NewFromFloat(100))
151 }
152
153 return weights
154}
155
156func GetWeekPortfolio(marketConfig db.MarketConfig) (Portfolio, error) {
157 portfolio := Portfolio{
158 Balances: make(map[string]PortfolioBalance),
159 }
160
161 report, err := db.GetLastPortfolioBegin(marketConfig)
162 if err != nil {
163 return portfolio, err
164 }
165
166 if report == nil {
167 return portfolio, &Error{NotFound, "no report", fmt.Errorf("no reports for marketConfigId '%v'", marketConfig.Id)}
168 }
169
170 liveReport, err := db.GetLatestReport(marketConfig)
171 if err != nil {
172 return portfolio, err
173 }
174
175 weights := ComputeWeights(*report)
176 currenciesPerformances := GetCurrenciesPerformance(report.Tickers, liveReport.Tickers)
177
178 portfolio.PeriodStart = report.Date.Truncate(time.Second).UTC()
179 portfolio.PeriodEnd = liveReport.Date.Truncate(time.Second).UTC()
180
181 for currency := range liveReport.Balances {
182 balance := liveReport.Balances[currency]
183 btcTicker := liveReport.Tickers.Balances[currency]
184
185 if btcTicker.Abs().LessThan(MINIMAL_BTC_VALUE_TRESHOLD) {
186 continue
187 }
188
189 var positionType PositionType
190 var perfMul decimal.Decimal
191
192 if balance.Total.LessThan(decimal.Zero) {
193 positionType = POSITION_SHORT
194 perfMul = decimal.NewFromFloat(-1.0)
195 } else {
196 positionType = POSITION_LONG
197 perfMul = decimal.NewFromFloat(1.0)
198 }
199
200 portfolio.Balances[currency] = PortfolioBalance{
201 PositionType: positionType,
202 Quantity: balance.Total,
203 BTCValue: btcTicker,
204 QuantityLocked: balance.MarginInPosition.Mul(decimal.NewFromFloat(1.0 + MARGIN_POSITION_SECURED_RATIO)),
205 Weight: weights[currency],
206 PositionPerformanceP: currenciesPerformances[currency].VariationP.Mul(perfMul),
207 }
208 }
209
210 portfolio.Value = liveReport.Tickers.Total
211 plusValue, err := ComputePlusValue(*report, liveReport)
212 if err != nil {
213 return portfolio, err
214 }
215
216 portfolio.Performance = NewValuePerformance(liveReport.Tickers.Total.Sub(plusValue), liveReport.Tickers.Total)
217
218 return portfolio, nil
219}