Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- package main
- import (
- "encoding/json"
- "errors"
- "fmt"
- "math"
- "net/http"
- "strconv"
- "strings"
- )
- // Types
- // Note: These could probably be somewhere else, but
- // I'd rather just give you a single file for this project.
- // Quote : GDAX Quote Container.
- type Quote struct {
- Price float64
- Size float64
- Orders float64
- }
- // Product : Minimal Product Container.
- type Product struct {
- ID string `json:"id"`
- }
- // Book : Order Book Container.
- type Book struct {
- Message string `json:"message"`
- Sequence int `json:"sequence"`
- Bids [][]interface{} `json:"bids"`
- Asks [][]interface{} `json:"asks"`
- }
- // reqBody : Request Template
- type reqBody struct {
- Action string `json:"action"`
- BaseCurrency string `json:"base_currency"`
- QuoteCurrency string `json:"quote_currency"`
- Amount string `json:"amount"`
- }
- // resBody : Response Template
- type resBody struct {
- Total string `json:"total"`
- Price string `json:"price"`
- Currency string `json:"currency"`
- }
- // End Types
- var (
- defaultLevel = 2
- gdaxAPI = "https://api-public.sandbox.gdax.com"
- done = make(chan struct{})
- productMap = makeProductMap()
- )
- // Creates a map of products to whether they are regular products or inverses.
- // Regular products are marked with 1 and inversed products (e.g USD-BTC) are marked -1.
- func makeProductMap() map[string]float64 {
- s := make(map[string]float64)
- products := getProducts()
- for _, element := range products {
- s[element.ID] = 1
- s[reverseProduct(element.ID)] = -1
- }
- return s
- }
- // "BTC-USD" <-> "USD->BTC"
- func reverseProduct(product string) string {
- indivialProducts := strings.Split(product, "-")
- indivialProducts[0], indivialProducts[1] = indivialProducts[1], indivialProducts[0]
- return strings.Join(indivialProducts, "-")
- }
- // "buy" <-> "sell"
- func reverseAction(action string) string {
- if action == "sell" {
- return "buy"
- }
- return "sell"
- }
- // Returns a list of Coinbase Products using the GDAX API.
- //
- // Note: This could be hard coded somewhere but it saves having to maintain
- // that list and only populates once on boot. Cache could be added to
- // increase efficiency as this shouldn't change too often.
- func getProducts() []Product {
- endpoint := getAPIEndpoint("products", "", 0)
- r, err := http.Get(endpoint)
- if err != nil {
- log(err)
- }
- product := []Product{}
- decoder := json.NewDecoder(r.Body)
- decoder.Decode(&product)
- return product
- }
- // Returns an order Book with asks and bids.
- func getOrderbook(product string) (book Book, err error) {
- endpoint := getAPIEndpoint("orderbook", product, defaultLevel)
- r, err := http.Get(endpoint)
- if err != nil {
- log(err)
- return Book{}, err
- }
- book = Book{}
- decoder := json.NewDecoder(r.Body)
- err = decoder.Decode(&book)
- if err != nil {
- log(err)
- return Book{}, err
- }
- return book, nil
- }
- // Endpoint "Router". Segments out REST calls away from the actual logic.
- func getAPIEndpoint(local string, product string, level int) string {
- url := gdaxAPI
- switch local {
- case "products":
- url = fmt.Sprintf("%s/products", url)
- case "orderbook":
- url = fmt.Sprintf("%s/products/%s/book?level=%d", url, product, level)
- }
- return url
- }
- // Very minimal error logging function, but very easy to integrate any other type of logging
- // by modifying the contents of this function with whatever logging platform Coinbase uses.
- func log(err error) {
- fmt.Printf("Log: %+v\n", err)
- }
- // Given a product, action, and amount, return the total and moving average price.
- // Inverted assets (e.g "USD->BTC") are handled within this function, as well as regular assets.
- // In the event of an error, the title and price will be zero and an order size error
- // will be generated and passed back to the calling function.
- func processProduct(product string, action string, amount string) (total float64, movingAvg float64, err error) {
- val, ok := productMap[product]
- if !ok {
- // Product, nor it's inverse, are supported by Coinbase.
- return 0, 0, errors.New("Product combination does not exist in GDAX API")
- }
- isInvertedAsset := val == -1 // Inverted Asset assertion
- if isInvertedAsset {
- // Invert the Inverted Asset so Quote works
- product = reverseProduct(product)
- // Reverse the action. E.g action=buy, base=USD, quote=BTC.
- // To get US, we have to sell btc, and should be looking at the bids (sells)
- // instead of the asks (buys).
- action = reverseAction(action)
- }
- quotes, _ := getQuotes(product, action, isInvertedAsset)
- need, err := strconv.ParseFloat(amount, 10)
- if err != nil {
- log(err)
- }
- remaining := need
- if need <= 0 {
- return 0, 0, errors.New("Requests must contain a positive amount")
- }
- // Assuming the GDAX API returns a the bids/asks in priority order, it is
- // sufficient to traverse them until we run out of BTC/ETH/USD/etc.
- for _, order := range quotes {
- scaleFactor := 1.0 // Standard scale value for non-inverted values
- if isInvertedAsset {
- // For inverted values, we use the original (e.g "BTC->USD" price).
- // so that we can calculate how much money (Or other currency)
- // each order could potentially equal out to (e.g 9.68 * 10000)
- scaleFactor = (1 / order.Price)
- }
- // Find the minimum of the remaining requirement and what is available.
- fillVal := math.Min(scaleFactor*order.Size, remaining)
- // Total is simply the price * how much we're taking from this order.
- total += order.Price * fillVal
- // Update remaining
- remaining -= fillVal
- // We know that we've satisfied x percent, where x is the opposite of what remains.
- // Thererfore we take 1 - remainingPercentage which is just rem/need.
- filledPercent := (1 - (remaining / need))
- // Use the filled percentage to find its contribution to our average price.
- movingAvg += order.Price * filledPercent
- if remaining == 0.0 {
- break
- }
- }
- if remaining > 0 {
- // If we're run out of orders to fill and still have a remaining balance, we can't fill the order.
- return 0, 0, errors.New("Order size is larger than exchange can provide")
- }
- return total, movingAvg, nil
- }
- func validateRequest(req reqBody) bool {
- if req.Action != "buy" && req.Action != "sell" {
- return false
- }
- return true
- }
- func rootHandler(w http.ResponseWriter, r *http.Request) {
- req := reqBody{}
- err := json.NewDecoder(r.Body).Decode(&req)
- if err != nil || !validateRequest(req) {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Malformed/Invalid Request. See README for more information."))
- return
- }
- product := fmt.Sprintf("%s-%s", req.BaseCurrency, req.QuoteCurrency)
- total := 0.0
- movingAvg := 0.0
- total, movingAvg, err = processProduct(product, req.Action, req.Amount)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(fmt.Sprintf("Error: %s", err.Error())))
- return
- }
- err = json.NewEncoder(w).Encode(&resBody{
- Total: fmt.Sprintf("%f", total),
- Price: fmt.Sprintf("%f", movingAvg),
- Currency: req.QuoteCurrency,
- })
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(fmt.Sprintf("Failed to Encode Response: %s", err.Error())))
- }
- }
- func getQuotes(product string, action string, isInvertedAsset bool) (q []Quote, err error) {
- books, err := getOrderbook(product)
- if err != nil {
- return []Quote{}, err
- }
- actionItem := books.Asks
- if action == "sell" {
- actionItem = books.Bids
- }
- quotes := []Quote{}
- for _, element := range actionItem {
- priceVal, err := strconv.ParseFloat(element[0].(string), 64)
- if err != nil {
- log(err)
- }
- sizeVal, err := strconv.ParseFloat(element[1].(string), 64)
- if err != nil {
- log(err)
- }
- orderVal := element[2].(float64)
- if isInvertedAsset {
- // For inverted values, invert the price.
- // e.g 1 BTC = ~8000 USD, therefore 1 / ~8000 BTC = 1 USD
- priceVal = (1 / priceVal)
- }
- quotes = append(quotes, Quote{
- Price: priceVal,
- Size: sizeVal,
- Orders: orderVal,
- })
- }
- return quotes, nil
- }
- func main() {
- // HTTP Server Setup
- http.HandleFunc("/", rootHandler)
- http.ListenAndServe(":8080", nil)
- <-done // Use channel to hang server.
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement