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