aboutsummaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
authorjloup <jeanloup.jamet@gmail.com>2018-05-09 19:44:17 +0200
committerjloup <jeanloup.jamet@gmail.com>2018-05-09 19:44:17 +0200
commit24e4797900b3d2edf642fdb547bc22357a5b39ad (patch)
treea2e62dd7382eb4c9ba817c25ba2fa15a659638d3 /api
parent78e3e81ddf01f41102f3f4e32c5a3955cf5fb04f (diff)
downloadFront-24e4797900b3d2edf642fdb547bc22357a5b39ad.tar.gz
Front-24e4797900b3d2edf642fdb547bc22357a5b39ad.tar.zst
Front-24e4797900b3d2edf642fdb547bc22357a5b39ad.zip
Refactor Portfolio balance.
Diffstat (limited to 'api')
-rw-r--r--api/api.go12
-rw-r--r--api/const.go2
-rw-r--r--api/market_config.go75
-rw-r--r--api/portfolio.go219
-rw-r--r--api/routes.go17
5 files changed, 287 insertions, 38 deletions
diff --git a/api/api.go b/api/api.go
index 42b9923..ece2a26 100644
--- a/api/api.go
+++ b/api/api.go
@@ -45,6 +45,18 @@ func (e Error) Error() string {
45 return "" 45 return ""
46} 46}
47 47
48func ErrorIs(err error, code ErrorCode) bool {
49 if err == nil {
50 return false
51 }
52
53 if apiError, ok := err.(*Error); !ok {
54 return false
55 } else {
56 return apiError.Code == code
57 }
58}
59
48func NewInternalError(err error) *Error { 60func NewInternalError(err error) *Error {
49 return &Error{InternalError, "internal error", err} 61 return &Error{InternalError, "internal error", err}
50} 62}
diff --git a/api/const.go b/api/const.go
index d3e5f42..1f15c6e 100644
--- a/api/const.go
+++ b/api/const.go
@@ -6,7 +6,7 @@ import "net/http"
6type Status uint32 6type Status uint32
7type ErrorCode uint32 7type ErrorCode uint32
8 8
9const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 10 9const EXTERNAL_SERVICE_TIMEOUT_SECONDS = 20
10 10
11const ( 11const (
12 OK Status = iota 12 OK Status = iota
diff --git a/api/market_config.go b/api/market_config.go
index c2248b3..81a92d1 100644
--- a/api/market_config.go
+++ b/api/market_config.go
@@ -5,11 +5,9 @@ import (
5 "strings" 5 "strings"
6 "time" 6 "time"
7 7
8 "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets"
9
10 "github.com/jloup/utils" 8 "github.com/jloup/utils"
11 "github.com/shopspring/decimal"
12 "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" 9 "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db"
10 "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets"
13) 11)
14 12
15type MarketConfigQuery struct { 13type MarketConfigQuery struct {
@@ -56,45 +54,33 @@ func (q MarketConfigQuery) Run() (interface{}, *Error) {
56 return config.Config, nil 54 return config.Config, nil
57} 55}
58 56
59type MarketBalanceQuery struct { 57type TestMarketCredentialsQuery struct {
60 In struct { 58 In struct {
61 User db.User 59 User db.User
62 Market string 60 Market string
63 Currency string
64 } 61 }
65} 62}
66 63
67func (q MarketBalanceQuery) ValidateParams() *Error { 64func (q TestMarketCredentialsQuery) ValidateParams() *Error {
68 if q.In.Market != "poloniex" { 65 if q.In.Market != "poloniex" {
69 return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)} 66 return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)}
70 } 67 }
71 68
72 // TODO: we should request market for available currencies.
73 if q.In.Currency != "BTC" && q.In.Currency != "USDT" && q.In.Currency != "ETH" {
74 return &Error{BadRequest, "invalid currency, accept [BTC, USDT, ETH]", fmt.Errorf("'%v' is not a valid currency", q.In.Currency)}
75 }
76
77 return nil 69 return nil
78} 70}
79 71
80func (q MarketBalanceQuery) Run() (interface{}, *Error) { 72func (q TestMarketCredentialsQuery) Run() (interface{}, *Error) {
81 config, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market) 73 config, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market)
82 if err != nil { 74 if err != nil {
83 return nil, NewInternalError(err) 75 return nil, NewInternalError(err)
84 } 76 }
85 77
86 if config.Config["key"] == "" || config.Config["secret"] == "" { 78 if config.Config["key"] == "" || config.Config["secret"] == "" {
87 return nil, &Error{InvalidMarketCredentials, "your credentials for this market are not setup", fmt.Errorf("'%v' credentials are not setup", q.In.Market)} 79 return nil, &Error{InvalidMarketCredentials, "no market credentials", fmt.Errorf("market credentials are empty for marketId '%v'", q.In.Market)}
88 } 80 }
89 81
90 result := struct {
91 Value decimal.Decimal `json:"value"`
92 ValueCurrency string `json:"valueCurrency"`
93 Balance map[string]markets.Balance `json:"balance"`
94 }{}
95
96 resultErr := CallExternalService(fmt.Sprintf("'%s' GetBalanceValue", q.In.Market), EXTERNAL_SERVICE_TIMEOUT_SECONDS*time.Second, func() *Error { 82 resultErr := CallExternalService(fmt.Sprintf("'%s' GetBalanceValue", q.In.Market), EXTERNAL_SERVICE_TIMEOUT_SECONDS*time.Second, func() *Error {
97 balance, err := Poloniex.GetBalance(config.Config["key"], config.Config["secret"]) 83 err := Poloniex.TestCredentials(config.Config["key"], config.Config["secret"])
98 84
99 if utils.ErrIs(err, markets.InvalidCredentials) { 85 if utils.ErrIs(err, markets.InvalidCredentials) {
100 return &Error{InvalidMarketCredentials, "wrong market credentials", fmt.Errorf("wrong '%v' market credentials", q.In.Market)} 86 return &Error{InvalidMarketCredentials, "wrong market credentials", fmt.Errorf("wrong '%v' market credentials", q.In.Market)}
@@ -108,16 +94,6 @@ func (q MarketBalanceQuery) Run() (interface{}, *Error) {
108 return NewInternalError(err) 94 return NewInternalError(err)
109 } 95 }
110 96
111 for currency, value := range balance.Balances {
112 if value.BTCValue.Abs().LessThan(decimal.NewFromFloat(0.0001)) {
113 delete(balance.Balances, currency)
114 }
115 }
116
117 result.Balance = balance.Balances
118 result.ValueCurrency = "BTC"
119 result.Value = balance.BTCValue.Round(8)
120
121 return nil 97 return nil
122 }) 98 })
123 99
@@ -125,7 +101,7 @@ func (q MarketBalanceQuery) Run() (interface{}, *Error) {
125 return nil, resultErr 101 return nil, resultErr
126 } 102 }
127 103
128 return &result, nil 104 return nil, nil
129} 105}
130 106
131type UpdateMarketConfigQuery struct { 107type UpdateMarketConfigQuery struct {
@@ -164,3 +140,36 @@ func (q UpdateMarketConfigQuery) Run() (interface{}, *Error) {
164 140
165 return nil, nil 141 return nil, nil
166} 142}
143
144type GetPortfolioQuery struct {
145 In struct {
146 User db.User
147 Market string
148 }
149}
150
151func (q GetPortfolioQuery) ValidateParams() *Error {
152 if q.In.Market != "poloniex" {
153 return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)}
154 }
155
156 return nil
157}
158
159func (q GetPortfolioQuery) Run() (interface{}, *Error) {
160 marketConfig, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market)
161 if err != nil {
162 return nil, NewInternalError(err)
163 }
164
165 report, err := GetWeekPortfolio(*marketConfig)
166 if ErrorIs(err, NotFound) {
167 return nil, err.(*Error)
168 }
169
170 if err != nil {
171 return nil, NewInternalError(err)
172 }
173
174 return report.Round(), nil
175}
diff --git a/api/portfolio.go b/api/portfolio.go
new file mode 100644
index 0000000..0fef94c
--- /dev/null
+++ b/api/portfolio.go
@@ -0,0 +1,219 @@
1package api
2
3import (
4 "fmt"
5 "time"
6
7 "github.com/shopspring/decimal"
8 "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db"
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}
diff --git a/api/routes.go b/api/routes.go
index 22af0e7..d7b316d 100644
--- a/api/routes.go
+++ b/api/routes.go
@@ -43,7 +43,8 @@ var Groups = []Group{
43 []Route{ 43 []Route{
44 {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"}, 44 {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"},
45 {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"}, 45 {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"},
46 {"GET", []gin.HandlerFunc{GetMarketBalance}, "/:name/balance"}, 46 {"GET", []gin.HandlerFunc{TestMarketCredentials}, "/:name/test-credentials"},
47 {"GET", []gin.HandlerFunc{GetPortfolio}, "/:name/portfolio"},
47 }, 48 },
48 }, 49 },
49} 50}
@@ -114,12 +115,20 @@ func GetMarketConfig(c *gin.Context) {
114 RunQuery(query, c) 115 RunQuery(query, c)
115} 116}
116 117
117func GetMarketBalance(c *gin.Context) { 118func TestMarketCredentials(c *gin.Context) {
118 query := &MarketBalanceQuery{} 119 query := &TestMarketCredentialsQuery{}
120
121 query.In.User = GetUser(c)
122 query.In.Market = c.Param("name")
123
124 RunQuery(query, c)
125}
126
127func GetPortfolio(c *gin.Context) {
128 query := &GetPortfolioQuery{}
119 129
120 query.In.User = GetUser(c) 130 query.In.User = GetUser(c)
121 query.In.Market = c.Param("name") 131 query.In.Market = c.Param("name")
122 query.In.Currency = c.Query("currency")
123 132
124 RunQuery(query, c) 133 RunQuery(query, c)
125} 134}