diff options
-rw-r--r-- | Gopkg.lock | 22 | ||||
-rw-r--r-- | Gopkg.toml | 8 | ||||
-rw-r--r-- | api/market_config.go | 52 | ||||
-rw-r--r-- | api/routes.go | 11 | ||||
-rw-r--r-- | cmd/web/js/api.js | 11 | ||||
-rw-r--r-- | markets/poloniex.go | 149 |
6 files changed, 251 insertions, 2 deletions
@@ -78,6 +78,12 @@ | |||
78 | 78 | ||
79 | [[projects]] | 79 | [[projects]] |
80 | branch = "master" | 80 | branch = "master" |
81 | name = "github.com/jloup/poloniex" | ||
82 | packages = ["."] | ||
83 | revision = "e75e6fd7991c1d71576ad97de73fc922f24a5fd2" | ||
84 | |||
85 | [[projects]] | ||
86 | branch = "master" | ||
81 | name = "github.com/jloup/utils" | 87 | name = "github.com/jloup/utils" |
82 | packages = ["."] | 88 | packages = ["."] |
83 | revision = "6055a8f761d5892502228aa62249e122f8bd392d" | 89 | revision = "6055a8f761d5892502228aa62249e122f8bd392d" |
@@ -99,6 +105,12 @@ | |||
99 | version = "v1.0.0" | 105 | version = "v1.0.0" |
100 | 106 | ||
101 | [[projects]] | 107 | [[projects]] |
108 | branch = "master" | ||
109 | name = "github.com/shopspring/decimal" | ||
110 | packages = ["."] | ||
111 | revision = "e3482495ff4cba75613e4177ed79825c890058a9" | ||
112 | |||
113 | [[projects]] | ||
102 | name = "github.com/ugorji/go" | 114 | name = "github.com/ugorji/go" |
103 | packages = ["codec"] | 115 | packages = ["codec"] |
104 | revision = "9831f2c3ac1068a78f50999a30db84270f647af6" | 116 | revision = "9831f2c3ac1068a78f50999a30db84270f647af6" |
@@ -112,7 +124,13 @@ | |||
112 | "blowfish", | 124 | "blowfish", |
113 | "ssh/terminal" | 125 | "ssh/terminal" |
114 | ] | 126 | ] |
115 | revision = "650f4a345ab4e5b245a3034b110ebc7299e68186" | 127 | revision = "432090b8f568c018896cd8a0fb0345872bbac6ce" |
128 | |||
129 | [[projects]] | ||
130 | branch = "master" | ||
131 | name = "golang.org/x/net" | ||
132 | packages = ["websocket"] | ||
133 | revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" | ||
116 | 134 | ||
117 | [[projects]] | 135 | [[projects]] |
118 | branch = "master" | 136 | branch = "master" |
@@ -138,6 +156,6 @@ | |||
138 | [solve-meta] | 156 | [solve-meta] |
139 | analyzer-name = "dep" | 157 | analyzer-name = "dep" |
140 | analyzer-version = 1 | 158 | analyzer-version = 1 |
141 | inputs-digest = "c9af022a586632799c6259f6c48eef8dad7080e36b96e8cb5cb905b316c4cb9b" | 159 | inputs-digest = "d3c9b3094ed174bcf1631e3a998a75d557c65c195d7a8fd5ca9912f71f334ce1" |
142 | solver-name = "gps-cdcl" | 160 | solver-name = "gps-cdcl" |
143 | solver-version = 1 | 161 | solver-version = 1 |
@@ -56,3 +56,11 @@ | |||
56 | [prune] | 56 | [prune] |
57 | go-tests = true | 57 | go-tests = true |
58 | unused-packages = true | 58 | unused-packages = true |
59 | |||
60 | [[constraint]] | ||
61 | branch = "master" | ||
62 | name = "github.com/jloup/poloniex" | ||
63 | |||
64 | [[constraint]] | ||
65 | branch = "master" | ||
66 | name = "github.com/shopspring/decimal" | ||
diff --git a/api/market_config.go b/api/market_config.go index 3fd10ae..ce79184 100644 --- a/api/market_config.go +++ b/api/market_config.go | |||
@@ -3,7 +3,9 @@ package api | |||
3 | import ( | 3 | import ( |
4 | "fmt" | 4 | "fmt" |
5 | 5 | ||
6 | "github.com/shopspring/decimal" | ||
6 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" | 7 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/db" |
8 | "immae.eu/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Front/markets" | ||
7 | ) | 9 | ) |
8 | 10 | ||
9 | type MarketConfigQuery struct { | 11 | type MarketConfigQuery struct { |
@@ -42,6 +44,56 @@ func (q MarketConfigQuery) Run() (interface{}, *Error) { | |||
42 | return config.Config, nil | 44 | return config.Config, nil |
43 | } | 45 | } |
44 | 46 | ||
47 | type MarketBalanceQuery struct { | ||
48 | In struct { | ||
49 | User db.User | ||
50 | Market string | ||
51 | Currency string | ||
52 | } | ||
53 | } | ||
54 | |||
55 | var Poloniex *markets.Poloniex | ||
56 | |||
57 | func init() { | ||
58 | Poloniex = markets.NewPoloniex() | ||
59 | Poloniex.StartTicker() | ||
60 | } | ||
61 | |||
62 | func (q MarketBalanceQuery) ValidateParams() *Error { | ||
63 | if q.In.Market != "poloniex" { | ||
64 | return &Error{BadRequest, "invalid market name", fmt.Errorf("'%v' is not a valid market name", q.In.Market)} | ||
65 | } | ||
66 | |||
67 | // TODO: we should request market for available currencies. | ||
68 | if q.In.Currency != "BTC" || q.In.Currency != "USDT" { | ||
69 | return &Error{BadRequest, "invalid currency, accept [BTC, USDT]", fmt.Errorf("'%v' is not a valid currency", q.In.Currency)} | ||
70 | } | ||
71 | |||
72 | return nil | ||
73 | } | ||
74 | |||
75 | func (q MarketBalanceQuery) Run() (interface{}, *Error) { | ||
76 | config, err := db.GetUserMarketConfig(q.In.User.Id, q.In.Market) | ||
77 | if err != nil { | ||
78 | return nil, NewInternalError(err) | ||
79 | } | ||
80 | |||
81 | if config.Config["key"] == "" || config.Config["secret"] == "" { | ||
82 | return nil, &Error{BadRequest, "your credentials for this market are not setup", fmt.Errorf("'%v' credentials are not setup", q.In.Market)} | ||
83 | } | ||
84 | |||
85 | balance, err := Poloniex.GetBalance(config.Config["key"], config.Config["secret"], q.In.Currency) | ||
86 | if err != nil { | ||
87 | return nil, NewInternalError(err) | ||
88 | } | ||
89 | |||
90 | result := struct { | ||
91 | Balance decimal.Decimal `json:balance` | ||
92 | }{balance} | ||
93 | |||
94 | return &result, nil | ||
95 | } | ||
96 | |||
45 | type UpdateMarketConfigQuery struct { | 97 | type UpdateMarketConfigQuery struct { |
46 | In struct { | 98 | In struct { |
47 | User db.User | 99 | User db.User |
diff --git a/api/routes.go b/api/routes.go index d7e712c..385db9a 100644 --- a/api/routes.go +++ b/api/routes.go | |||
@@ -41,6 +41,7 @@ var Groups = []Group{ | |||
41 | []Route{ | 41 | []Route{ |
42 | {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"}, | 42 | {"GET", []gin.HandlerFunc{GetMarketConfig}, "/:name"}, |
43 | {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"}, | 43 | {"POST", []gin.HandlerFunc{UpdateMarketConfig}, "/:name/update"}, |
44 | {"GET", []gin.HandlerFunc{GetMarketBalance}, "/:name/balance"}, | ||
44 | }, | 45 | }, |
45 | }, | 46 | }, |
46 | } | 47 | } |
@@ -111,6 +112,16 @@ func GetMarketConfig(c *gin.Context) { | |||
111 | RunQuery(query, c) | 112 | RunQuery(query, c) |
112 | } | 113 | } |
113 | 114 | ||
115 | func GetMarketBalance(c *gin.Context) { | ||
116 | query := &MarketBalanceQuery{} | ||
117 | |||
118 | query.In.User = GetUser(c) | ||
119 | query.In.Market = c.Param("name") | ||
120 | query.In.Currency = "BTC" | ||
121 | |||
122 | RunQuery(query, c) | ||
123 | } | ||
124 | |||
114 | func UpdateMarketConfig(c *gin.Context) { | 125 | func UpdateMarketConfig(c *gin.Context) { |
115 | query := &UpdateMarketConfigQuery{} | 126 | query := &UpdateMarketConfigQuery{} |
116 | 127 | ||
diff --git a/cmd/web/js/api.js b/cmd/web/js/api.js index e2acd1d..5c19fdf 100644 --- a/cmd/web/js/api.js +++ b/cmd/web/js/api.js | |||
@@ -53,6 +53,17 @@ var ApiEndpoints = { | |||
53 | return '/market/' + params.name; | 53 | return '/market/' + params.name; |
54 | } | 54 | } |
55 | }, | 55 | }, |
56 | 'MARKET_BALANCE': { | ||
57 | 'type': 'GET', | ||
58 | 'auth': true, | ||
59 | 'parameters': [ | ||
60 | {'name': 'name', 'mandatory': true, 'inquery': false}, | ||
61 | {'name': 'currency', 'mandatory': true, 'inquery': true}, | ||
62 | ], | ||
63 | 'buildUrl': function(params) { | ||
64 | return '/market/' + params.name + '/balance'; | ||
65 | } | ||
66 | }, | ||
56 | 'UPDATE_MARKET': { | 67 | 'UPDATE_MARKET': { |
57 | 'type': 'POST', | 68 | 'type': 'POST', |
58 | 'auth': true, | 69 | 'auth': true, |
diff --git a/markets/poloniex.go b/markets/poloniex.go new file mode 100644 index 0000000..c55a3da --- /dev/null +++ b/markets/poloniex.go | |||
@@ -0,0 +1,149 @@ | |||
1 | package markets | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | |||
6 | "github.com/jloup/poloniex" | ||
7 | "github.com/shopspring/decimal" | ||
8 | ) | ||
9 | |||
10 | type CurrencyPair struct { | ||
11 | Name string | ||
12 | Rate decimal.Decimal | ||
13 | } | ||
14 | |||
15 | type Poloniex struct { | ||
16 | TickerCache map[string]CurrencyPair | ||
17 | |||
18 | publicClient *poloniex.Poloniex | ||
19 | updateTickerChan chan CurrencyPair | ||
20 | } | ||
21 | |||
22 | func NewPoloniex() *Poloniex { | ||
23 | client, _ := poloniex.NewClient("", "") | ||
24 | |||
25 | return &Poloniex{ | ||
26 | TickerCache: make(map[string]CurrencyPair), | ||
27 | updateTickerChan: nil, | ||
28 | publicClient: client, | ||
29 | } | ||
30 | } | ||
31 | |||
32 | func (p *Poloniex) GetBalance(apiKey, apiSecret, base_currency string) (decimal.Decimal, error) { | ||
33 | client, _ := poloniex.NewClient(apiKey, apiSecret) | ||
34 | |||
35 | accounts, err := client.TradeReturnAvailableAccountBalances() | ||
36 | if err != nil { | ||
37 | return decimal.Zero, err | ||
38 | } | ||
39 | |||
40 | marginBalance, err := p.computeAccountBalance(accounts.Margin, base_currency) | ||
41 | if err != nil { | ||
42 | return decimal.Zero, err | ||
43 | } | ||
44 | |||
45 | exchangeBalance, err := p.computeAccountBalance(accounts.Exchange, base_currency) | ||
46 | if err != nil { | ||
47 | return decimal.Zero, err | ||
48 | } | ||
49 | |||
50 | return decimal.Sum(marginBalance, exchangeBalance), nil | ||
51 | } | ||
52 | |||
53 | func (p *Poloniex) computeAccountBalance(account map[string]decimal.Decimal, base_currency string) (decimal.Decimal, error) { | ||
54 | var total decimal.Decimal | ||
55 | |||
56 | for currency, amount := range account { | ||
57 | pair, err := p.GetCurrencyPair(base_currency, currency) | ||
58 | if err != nil { | ||
59 | return decimal.Zero, err | ||
60 | } | ||
61 | |||
62 | total = total.Add(amount.Mul(pair.Rate)) | ||
63 | } | ||
64 | |||
65 | return total, nil | ||
66 | } | ||
67 | |||
68 | func (p *Poloniex) GetCurrencyPair(curr1, curr2 string) (CurrencyPair, error) { | ||
69 | pairName := fmt.Sprintf("%s_%s", curr1, curr2) | ||
70 | |||
71 | fmt.Println(pairName) | ||
72 | if curr1 == curr2 { | ||
73 | return CurrencyPair{pairName, decimal.NewFromFloat(1.0)}, nil | ||
74 | } | ||
75 | |||
76 | pair, ok := p.TickerCache[pairName] | ||
77 | if !ok { | ||
78 | pair, err := p.fetchTicker(pairName) | ||
79 | if err != nil { | ||
80 | return CurrencyPair{}, err | ||
81 | } | ||
82 | |||
83 | return pair, nil | ||
84 | } | ||
85 | |||
86 | return pair, nil | ||
87 | } | ||
88 | |||
89 | func (p *Poloniex) fetchTicker(pair string) (CurrencyPair, error) { | ||
90 | tickers, err := p.publicClient.PubReturnTickers() | ||
91 | if err != nil { | ||
92 | return CurrencyPair{}, err | ||
93 | } | ||
94 | |||
95 | if ticker, ok := tickers[pair]; ok { | ||
96 | pair := CurrencyPair{Name: pair, Rate: ticker.Last} | ||
97 | |||
98 | if p.updateTickerChan != nil { | ||
99 | p.updateTickerChan <- pair | ||
100 | } | ||
101 | |||
102 | return pair, nil | ||
103 | } else { | ||
104 | return CurrencyPair{}, fmt.Errorf("pair '%v' not in tickers", pair) | ||
105 | } | ||
106 | } | ||
107 | |||
108 | func (p *Poloniex) StartTicker() error { | ||
109 | stream, err := poloniex.NewWSClient() | ||
110 | if err != nil { | ||
111 | return err | ||
112 | } | ||
113 | |||
114 | err = stream.SubscribeTicker() | ||
115 | if err != nil { | ||
116 | return err | ||
117 | } | ||
118 | |||
119 | p.updateTickerChan = make(chan CurrencyPair) | ||
120 | |||
121 | go func() { | ||
122 | for { | ||
123 | quit := false | ||
124 | select { | ||
125 | case data, ok := <-stream.Subs["ticker"]: | ||
126 | if !ok { | ||
127 | quit = true | ||
128 | } else { | ||
129 | ticker := data.(poloniex.WSTicker) | ||
130 | p.TickerCache[ticker.CurrencyPair] = CurrencyPair{Name: ticker.CurrencyPair, Rate: decimal.NewFromFloat(ticker.Last)} | ||
131 | } | ||
132 | |||
133 | case pair, ok := <-p.updateTickerChan: | ||
134 | if !ok { | ||
135 | quit = true | ||
136 | } else { | ||
137 | p.TickerCache[pair.Name] = pair | ||
138 | } | ||
139 | } | ||
140 | if quit { | ||
141 | //TODO: logit | ||
142 | p.updateTickerChan = nil | ||
143 | break | ||
144 | } | ||
145 | } | ||
146 | }() | ||
147 | |||
148 | return nil | ||
149 | } | ||