Advertisement
Guest User

Untitled

a guest
Nov 22nd, 2017
115
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.19 KB | None | 0 0
  1. package main
  2.  
  3. import (
  4.     "encoding/json"
  5.     "errors"
  6.     "fmt"
  7.     "math"
  8.     "net/http"
  9.     "strconv"
  10.     "strings"
  11. )
  12.  
  13. // Types
  14. // Note: These could probably be somewhere else, but
  15. // I'd rather just give you a single file for this project.
  16.  
  17. // Quote : GDAX Quote Container.
  18. type Quote struct {
  19.     Price  float64
  20.     Size   float64
  21.     Orders float64
  22. }
  23.  
  24. // Product : Minimal Product Container.
  25. type Product struct {
  26.     ID string `json:"id"`
  27. }
  28.  
  29. // Book : Order Book Container.
  30. type Book struct {
  31.     Message  string          `json:"message"`
  32.     Sequence int             `json:"sequence"`
  33.     Bids     [][]interface{} `json:"bids"`
  34.     Asks     [][]interface{} `json:"asks"`
  35. }
  36.  
  37. // reqBody : Request Template
  38. type reqBody struct {
  39.     Action        string `json:"action"`
  40.     BaseCurrency  string `json:"base_currency"`
  41.     QuoteCurrency string `json:"quote_currency"`
  42.     Amount        string `json:"amount"`
  43. }
  44.  
  45. // resBody : Response Template
  46. type resBody struct {
  47.     Total    string `json:"total"`
  48.     Price    string `json:"price"`
  49.     Currency string `json:"currency"`
  50. }
  51.  
  52. // End Types
  53.  
  54. var (
  55.     defaultLevel = 2
  56.     gdaxAPI      = "https://api-public.sandbox.gdax.com"
  57.     done         = make(chan struct{})
  58.     productMap   = makeProductMap()
  59. )
  60.  
  61. // Creates a map of products to whether they are regular products or inverses.
  62. // Regular products are marked with 1 and inversed products (e.g USD-BTC) are marked -1.
  63. func makeProductMap() map[string]float64 {
  64.     s := make(map[string]float64)
  65.     products := getProducts()
  66.     for _, element := range products {
  67.         s[element.ID] = 1
  68.         s[reverseProduct(element.ID)] = -1
  69.     }
  70.     return s
  71. }
  72.  
  73. // "BTC-USD" <-> "USD->BTC"
  74. func reverseProduct(product string) string {
  75.     indivialProducts := strings.Split(product, "-")
  76.     indivialProducts[0], indivialProducts[1] = indivialProducts[1], indivialProducts[0]
  77.     return strings.Join(indivialProducts, "-")
  78. }
  79.  
  80. // "buy" <-> "sell"
  81. func reverseAction(action string) string {
  82.     if action == "sell" {
  83.         return "buy"
  84.     }
  85.     return "sell"
  86. }
  87.  
  88. // Returns a list of Coinbase Products using the GDAX API.
  89. //
  90. // Note: This could be hard coded somewhere but it saves having to maintain
  91. // that list and only populates once on boot. Cache could be added to
  92. // increase efficiency as this shouldn't change too often.
  93. func getProducts() []Product {
  94.     endpoint := getAPIEndpoint("products", "", 0)
  95.     r, err := http.Get(endpoint)
  96.  
  97.     if err != nil {
  98.         log(err)
  99.     }
  100.  
  101.     product := []Product{}
  102.  
  103.     decoder := json.NewDecoder(r.Body)
  104.     decoder.Decode(&product)
  105.  
  106.     return product
  107. }
  108.  
  109. // Returns an order Book with asks and bids.
  110. func getOrderbook(product string) (book Book, err error) {
  111.     endpoint := getAPIEndpoint("orderbook", product, defaultLevel)
  112.     r, err := http.Get(endpoint)
  113.  
  114.     if err != nil {
  115.         log(err)
  116.         return Book{}, err
  117.     }
  118.  
  119.     book = Book{}
  120.  
  121.     decoder := json.NewDecoder(r.Body)
  122.     err = decoder.Decode(&book)
  123.  
  124.     if err != nil {
  125.         log(err)
  126.         return Book{}, err
  127.     }
  128.  
  129.     return book, nil
  130. }
  131.  
  132. // Endpoint "Router". Segments out REST calls away from the actual logic.
  133. func getAPIEndpoint(local string, product string, level int) string {
  134.     url := gdaxAPI
  135.     switch local {
  136.     case "products":
  137.         url = fmt.Sprintf("%s/products", url)
  138.     case "orderbook":
  139.         url = fmt.Sprintf("%s/products/%s/book?level=%d", url, product, level)
  140.     }
  141.     return url
  142. }
  143.  
  144. // Very minimal error logging function, but very easy to integrate any other type of logging
  145. // by modifying the contents of this function with whatever logging platform Coinbase uses.
  146. func log(err error) {
  147.     fmt.Printf("Log: %+v\n", err)
  148. }
  149.  
  150. // Given a product, action, and amount, return the total and moving average price.
  151. // Inverted assets (e.g "USD->BTC") are handled within this function, as well as regular assets.
  152. // In the event of an error, the title and price will be zero and an order size error
  153. // will be generated and passed back to the calling function.
  154. func processProduct(product string, action string, amount string) (total float64, movingAvg float64, err error) {
  155.     val, ok := productMap[product]
  156.  
  157.     if !ok {
  158.         // Product, nor it's inverse, are supported by Coinbase.
  159.         return 0, 0, errors.New("Product combination does not exist in GDAX API")
  160.     }
  161.  
  162.     isInvertedAsset := val == -1 // Inverted Asset assertion
  163.  
  164.     if isInvertedAsset {
  165.         // Invert the Inverted Asset so Quote works
  166.         product = reverseProduct(product)
  167.         // Reverse the action. E.g action=buy, base=USD, quote=BTC.
  168.         // To get US, we have to sell btc, and should be looking at the bids (sells)
  169.         // instead of the asks (buys).
  170.         action = reverseAction(action)
  171.     }
  172.  
  173.     quotes, _ := getQuotes(product, action, isInvertedAsset)
  174.  
  175.     need, err := strconv.ParseFloat(amount, 10)
  176.     if err != nil {
  177.         log(err)
  178.     }
  179.  
  180.     remaining := need
  181.     if need <= 0 {
  182.         return 0, 0, errors.New("Requests must contain a positive amount")
  183.     }
  184.  
  185.     // Assuming the GDAX API returns a the bids/asks in priority order, it is
  186.     // sufficient to traverse them until we run out of BTC/ETH/USD/etc.
  187.     for _, order := range quotes {
  188.         scaleFactor := 1.0 // Standard scale value for non-inverted values
  189.         if isInvertedAsset {
  190.             // For inverted values, we use the original (e.g "BTC->USD" price).
  191.             // so that we can calculate how much money (Or other currency)
  192.             // each order could potentially equal out to (e.g 9.68 * 10000)
  193.             scaleFactor = (1 / order.Price)
  194.         }
  195.         // Find the minimum of the remaining requirement and what is available.
  196.         fillVal := math.Min(scaleFactor*order.Size, remaining)
  197.         // Total is simply the price * how much we're taking from this order.
  198.         total += order.Price * fillVal
  199.         // Update remaining
  200.         remaining -= fillVal
  201.         // We know that we've satisfied x percent, where x is the opposite of what remains.
  202.         // Thererfore we take 1 - remainingPercentage which is just rem/need.
  203.         filledPercent := (1 - (remaining / need))
  204.         // Use the filled percentage to find its contribution to our average price.
  205.         movingAvg += order.Price * filledPercent
  206.         if remaining == 0.0 {
  207.             break
  208.         }
  209.     }
  210.  
  211.     if remaining > 0 {
  212.         // If we're run out of orders to fill and still have a remaining balance, we can't fill the order.
  213.         return 0, 0, errors.New("Order size is larger than exchange can provide")
  214.     }
  215.  
  216.     return total, movingAvg, nil
  217.  
  218. }
  219.  
  220. func validateRequest(req reqBody) bool {
  221.     if req.Action != "buy" && req.Action != "sell" {
  222.         return false
  223.     }
  224.     return true
  225. }
  226.  
  227. func rootHandler(w http.ResponseWriter, r *http.Request) {
  228.     req := reqBody{}
  229.     err := json.NewDecoder(r.Body).Decode(&req)
  230.     if err != nil || !validateRequest(req) {
  231.         w.WriteHeader(http.StatusBadRequest)
  232.         w.Write([]byte("Malformed/Invalid Request. See README for more information."))
  233.         return
  234.     }
  235.  
  236.     product := fmt.Sprintf("%s-%s", req.BaseCurrency, req.QuoteCurrency)
  237.  
  238.     total := 0.0
  239.     movingAvg := 0.0
  240.  
  241.     total, movingAvg, err = processProduct(product, req.Action, req.Amount)
  242.  
  243.     if err != nil {
  244.         w.WriteHeader(http.StatusBadRequest)
  245.         w.Write([]byte(fmt.Sprintf("Error: %s", err.Error())))
  246.         return
  247.     }
  248.  
  249.     err = json.NewEncoder(w).Encode(&resBody{
  250.         Total:    fmt.Sprintf("%f", total),
  251.         Price:    fmt.Sprintf("%f", movingAvg),
  252.         Currency: req.QuoteCurrency,
  253.     })
  254.  
  255.     if err != nil {
  256.         w.WriteHeader(http.StatusBadRequest)
  257.         w.Write([]byte(fmt.Sprintf("Failed to Encode Response: %s", err.Error())))
  258.     }
  259. }
  260.  
  261. func getQuotes(product string, action string, isInvertedAsset bool) (q []Quote, err error) {
  262.     books, err := getOrderbook(product)
  263.  
  264.     if err != nil {
  265.         return []Quote{}, err
  266.     }
  267.  
  268.     actionItem := books.Asks
  269.     if action == "sell" {
  270.         actionItem = books.Bids
  271.     }
  272.  
  273.     quotes := []Quote{}
  274.  
  275.     for _, element := range actionItem {
  276.         priceVal, err := strconv.ParseFloat(element[0].(string), 64)
  277.         if err != nil {
  278.             log(err)
  279.         }
  280.         sizeVal, err := strconv.ParseFloat(element[1].(string), 64)
  281.         if err != nil {
  282.             log(err)
  283.         }
  284.         orderVal := element[2].(float64)
  285.         if isInvertedAsset {
  286.             // For inverted values, invert the price.
  287.             // e.g 1 BTC = ~8000 USD, therefore 1 / ~8000 BTC = 1 USD
  288.             priceVal = (1 / priceVal)
  289.         }
  290.         quotes = append(quotes, Quote{
  291.             Price:  priceVal,
  292.             Size:   sizeVal,
  293.             Orders: orderVal,
  294.         })
  295.     }
  296.  
  297.     return quotes, nil
  298. }
  299.  
  300. func main() {
  301.     // HTTP Server Setup
  302.     http.HandleFunc("/", rootHandler)
  303.     http.ListenAndServe(":8080", nil)
  304.     <-done // Use channel to hang server.
  305. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement