8. A Stock Ticker on the Pedestal Framework Server

We’ve done an introduction to Pedestal. Now we’ll go deeper by building an application that sends pricing information from the server to the web application in real time. This will be similar to a trading desk application with a stock price ticker and the capability to place stock orders asynchronously.

Assumptions

In this chapter we assume the following:

Image You know about the HTML5 feature called EventSource or are willing to learn how it works. (You may have heard of WebSockets. The really short summary is that WebSockets are two-way and EventSources are one-way, server to client).

Image You have worked through the previous chapter, which introduces the Pedestal Server framework.

Image You have a passing familiarity with JavaScript and the JQuery framework.

Benefits

The benefits of this chapter are that you’ll learn how to build an event source with the Pedestal framework and receive stock orders as HTTP requests using a Pedestal server.

The Recipe—Code

To build the event source in Pedestal server, follow these steps. We’ll create a new pedestal application using the Leiningen template. Then we’ll create a static page with CSS and JavaScript to read the EventSource, update the page, and submit requests. On the server we’ll create a prices feed and feed this out to the EventSource. On the server we’ll also listen for orders and act appropriately.

1. Create a new Pedestal Service application called stock-ticker-demo.

lein new pedestal-service stock-ticker-demo
cd stock-ticker-demo

2. Create the directory resources/public/css.

3. Download the twitter bootstrap file using your web browser from the website http://getbootstrap.com/.

4. Expand the bootstrap zip file and copy the file at bootstrap/css/bootstrap.css to your newly created directory at resources/public/css.

5. Create a new file resources/public/stocks.html with the following contents:

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
  <title>Stock info</title>
  <script
src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js">
</script>
  <link rel="stylesheet" type="text/css" href="css/bootstrap.css">
</head>
<style>body {margin-left:100px;} td {padding:5px;}</style>
<body>
<h1>Prices</h1>

<table>
  <tr>
    <td>AAPL</td>
    <td><div id="AAPL" >1.00</div></td>
    <td><button class="btn" id="AAPL-BTN">Buy</button></td>
  </tr>
  <tr>
    <td>AMZN</td>
    <td><div id="AMZN" >1.00</div></td>
    <td><button class="btn" id="AMZN-BTN">Buy</button></td>
  </tr>
  <tr>
    <td>GOOG</td>
    <td><div id="GOOG" >1.00</div></td>
    <td><button class="btn" id="GOOG-BTN">Buy</button></td>
  </tr>
  <tr>
    <td>YHOO</td>
    <td><div id="YHOO" >1.00</div></td>
    <td><button class="btn" id="YHOO-BTN">Buy</button></td>
  </tr>
</table>

<script>
  // reading
  var es = new EventSource('/prices'),

  // a message without a type was fired
  es.onmessage = function(e) { console.log("message without type" + e.data +
" ")};

  //Listen for message type price
  es.addEventListener('price', function(e) {
    console.log("e.data = " + e.data);
    var priceinfo = $.parseJSON(e.data)
    console.log("priceinfo json object = " + priceinfo);
    console.log("priceinfo price = " + priceinfo.price);
    console.log("priceinfo stock-code = " + priceinfo["stock-code"]);
    $("#" + priceinfo["stock-code"]).text(priceinfo.price);
  }, false);

function clickButton(stockCode) {
  $("#"+stockCode+"-BTN").click(function() {
    var orderinfo = { "stock-code": stockCode, "price": $("#"+stockCode).
text()};
    var orderJSON = JSON.stringify(orderinfo);
    console.log("orderJSON: " + orderJSON);
    $.post( '/prices', { order : orderJSON});
  });
}

clickButton("AAPL");
clickButton("AMZN");
clickButton("GOOG");
clickButton("YHOO");

  </script>
  </body>
</html>

6. Modify the project.clj file to look like the following:

(defproject stock-ticker-demo "0.0.1-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.7.0-beta2"]
                 [io.pedestal/pedestal.service "0.4.0"]
                 [io.pedestal/pedestal.jetty "0.4.0"]
                 [ch.qos.logback/logback-classic "1.1.3" :exclusions
[org.slf4j/slf4j-api]]
                 [org.slf4j/jul-to-slf4j "1.7.12"]
                 [org.slf4j/jcl-over-slf4j "1.7.12"]
                 [org.slf4j/log4j-over-slf4j "1.7.12"]
                 [org.clojure/data.json "0.2.6"]
                 [org.clojure/core.async "0.1.346.0-17112a-alpha"]]
  :min-lein-version "2.0.0"
  :resource-paths ["config", "resources"]
  :profiles {:dev {:aliases {"run-dev" ["trampoline" "run" "-m" "stock-
ticker-demo.server/run-dev"]}
                   :dependencies [[io.pedestal/pedestal.service-tools
"0.4.0"]]}}
  :main ^{:skip-aot true} stock-ticker-demo.server)

7. Now create the file src/stock_ticker_demo/price_feed.clj with the following contents:

(ns stock-ticker-demo.price-feed)

(def stock-map {"YHOO" {:max-price 31.1 :min-price 11.51
                        :wavelength 90 :starting-point 0.5
                        :eps 2.15}
                "AAPL" {:max-price 408.38 :min-price 84.11
                        :wavelength 60 :starting-point 0
                        :eps 31.25}
                "GOOG" {:max-price 809.1 :min-price 292.96
                        :wavelength 50 :starting-point 0.75
                        :eps 29.31}
                "AMZN" {:max-price 274.7 :min-price 42.7
                        :wavelength 45 :starting-point 0.25
                        :eps 1.15}})

(def stock-codes (keys stock-map))

(defn time-model
  "Model the price of a stock on a sine wave."
  [time-secs stock-map]
  (let [max-price (:max-price stock-map)
        min-price (:min-price stock-map)
        wavelength (:wavelength stock-map)
        med-price (+ (/ (- max-price min-price) 2) min-price)
        amplitude (- max-price med-price)
        starting-point (:starting-point stock-map)]
    (+ (* (Math/sin (- (/ (* 2 Math/PI time-secs) wavelength)
                       (* 2 Math/PI starting-point wavelength)))
          amplitude)
       med-price)))

(defn curr-price
  "Given a stock code - get a price for the current point in time."
  [stock-code]
  (format "%.2f"
          (time-model
           (/ (java.lang.System/currentTimeMillis) 60)
           (stock-map stock-code))))

8. Now make the following changes to src/stock_ticker_demo/service.clj:

(ns stock-ticker-demo.service
  (:require [io.pedestal.http :as bootstrap]
            [io.pedestal.http.route :as route]
            [io.pedestal.http.body-params :as body-params]
            [io.pedestal.http.route.definition :refer [defroutes]]
            [ring.util.response :as ring-resp]
            [io.pedestal.http.sse :as sse]
            [clojure.data.json :as json]
            [stock-ticker-demo.price-feed :as price-feed]
            [clojure.core.async :as async])
  (:import  [java.util Date]
            [java.util.concurrent Executors]))

(defn pump-stock-prices
  "Given a count - iterate returning a series of stock prices."
  [event-ch num-iterations ctx]
  (let [stock-code  (rand-nth price-feed/stock-codes)
        price (price-feed/curr-price stock-code)
        stock-info  (price-feed/stock-map stock-code)
        eps (:eps stock-info)]
    (async/put! event-ch {:name "price"
                          :data (json/write-str
                                 {:stock-code stock-code :price price
                                  :eps eps})})
    (println
     (str stock-code " " price " " eps " " stock-info " "
          (java.util.Date.) " ")))
  (Thread/sleep  1000)
  (if (> num-iterations 0)
    (recur event-ch (dec num-iterations) ctx)
    (do
      (async/put! event-ch {:name "close" :data ""})
      (async/close! event-ch))))

(defn prices-stream
  "Starts sending price events to the channel."
  [event-ch ctx]
  (pump-stock-prices event-ch 100 ctx))

(defn prices-called
  "Feedback for when a stock order is placed on the web page."
  [{{order-json "order"} :form-params}]
  (let [order (json/read-str order-json :key-fn keyword)
        {stock-code :stock-code price :price} order]
    (println
     (str "An order has come in for: " stock-code " at: " price)))
  {:status 204})

(defn about-page
  [request]
  (ring-resp/response (format "Clojure %s - served from %s"
                              (clojure-version)
                              (route/url-for ::about-page))))

(defn home-page
  [request]
  (ring-resp/response "Hello World!"))

(defroutes routes
  [[["/" {:get home-page}
     ^:interceptors [(body-params/body-params) bootstrap/html-body]
     ["/prices" {:get [::prices (sse/start-event-stream prices-stream)]
                 :post prices-called}]
     ["/about" {:get about-page}]]]])

(def service {:env :prod
              ::bootstrap/routes routes
              ::bootstrap/resource-path "/public"
              ::bootstrap/type :jetty
              ::bootstrap/port 8080})

Testing the Solution

That should be it. Let’s test it now.

1. In your command prompt at the top level of the project, run Leiningen:

lein run

You should see something similar to the following output:

INFO  org.eclipse.jetty.server.Server - jetty-8.1.9.v20130131
INFO  o.e.jetty.server.AbstractConnector - Started
[email protected]:8080

2. We’ll test the EventSource by taking a look at the pricing feed. In your web browser, open the link http://localhost:8080/prices.

You should see output that looks like the following:

event: price
data:{"stock-code":"AMZN","price":"194.03","eps":1.15}

event: price
data:{"stock-code":"YHOO","price":"29.22","eps":2.15}

event: price
data:{"stock-code":"YHOO","price":"18.84","eps":2.15}

Figure 8.1 shows what your output should look like in your web browser.

Image

Figure 8.1 Output from /prices EventSource

Now we’ll look at our stocks page.

3. In your web browser, navigate to the following page:

http://localhost:8080/stocks.html

You should see what is shown in Figure 8.2.

Image

Figure 8.2 A simple stock-ordering page with real-time prices

4. Now if you click one of the Buy buttons, you should see something similar to the following in your command prompt window:

An order has come in for: GOOG at: 501.61

Notes on the Recipe

Looking at the HTML file resources/public/stocks.html, note the following in the header:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></
script>
  <link rel="stylesheet" type="text/css" href="css/bootstrap.css">

This loads up the jQuery library and the Twitter Bootstrap CSS.

Note the following in the body:

<table>
  <tr>
    <td>AAPL</td>
    <td><div id="AAPL" >1.00</div></td>
    <td><button class="btn" id="AAPL-BTN">Buy</button></td>
  </tr>
...

This is a list that contains the stock codes and a default price for several stocks. It also contains a Buy button to create stock orders.

Note the following in the script:

var es = new EventSource('/prices'),

  // a message without a type was fired
  es.onmessage = function(e) { console.log("message without type" + e.data +
" ")};

This loads up an object for the EventSource hosted at the path /prices. As an example, the EventSource data will look like this:

event: price
data:{"stock-code":"AMZN","price":"225.12","eps":1.15}

event: price
data:{"stock-code":"AAPL","price":"401.86","eps":31.25}

Here we see an event type called price, and an event data containing a JSON object with stock price information.

In the code above we attach a listener function to the EventSource object to handle the case where the event has no type, just for safety. This function won’t trip when the example above comes through because it has an event type price.

Note the following from the script:

//Listen for message type price
  es.addEventListener('price', function(e) {
    console.log("e.data = " + e.data);
    var priceinfo = $.parseJSON(e.data);
    console.log("priceinfo json object = " + priceinfo);
    console.log("priceinfo price = " + priceinfo.price);
    console.log("priceinfo stock-code = " + priceinfo["stock-code"]);
$("#" + priceinfo["stock-code"]).text(priceinfo.price);
  }, false);

This adds a listener function to the EventSource for events of type price. It then loads a priceinfo object from the JSON string in the data of the event. It extracts the price and stock code from the priceinfo object, looks at the relevant page element for this stock code, and overwrites the price for that stock code on the page.

Note the following from the script:

function clickButton(stockCode) {
  $("#"+stockCode+"-BTN").click(function() {
    var orderinfo = { "stock-code": stockCode, "price": $("#"+stockCode).text()};
    var orderJSON = JSON.stringify(orderinfo)
    console.log("orderJSON: " + orderJSON);
    $.post( '/prices', { order : orderJSON});
  });
}

This adds a click function to the Buy button for the stock code. When the user clicks the Buy button, the function builds a JavaScript object with the stock code and the price on the page at the time the button was clicked. It then serializes this object to a JSON string and then does an AJAX POST to send the order information to the /prices URL on the server.

Note that in the project.clj we added the lines:

                 [org.clojure/data.json "0.2.5"]
                 [org.clojure/core.async "0.1.346.0-17112a-alpha"]]

This enables us to output JSON from the server to be read by the JavaScript client. It also brings in the Clojure core.async library to queue up responses to publish to the client via SSE.

Note the following var definition from price_feed.clj:

 (def stock-map {"YHOO" {:max-price 31.1 :min-price 11.51
                        :wavelength 90 :starting-point 0.5
                        :eps 2.15}

These are inputs to a model representing fluctuations in stock prices. We choose to model this with a simple sine wave and use maxima and minima to calculate median point and amplitude. We define the rate of fluctuations with the wavelength and ensure that all the models don’t start at 0 by defining a starting point in radians. We also add a key for earnings per share, so that value-based determinations can be made from the price. Note that the price-to-earnings-ratio method is far too simplistic a model to be used in a commercial sense. (Note: Nothing about this example should be considered financial advice.)

Note the following function from price_feed.clj:

 (defn time-model
  "Model the price of a stock on a sine wave."
  [time-secs stock-map]
  (let [max-price (:max-price stock-map)
        min-price (:min-price stock-map)
        wavelength (:wavelength stock-map)
        med-price (+ (/ (- max-price min-price) 2) min-price)
        amplitude (- max-price med-price)
        starting-point (:starting-point stock-map)]
    (+ (* (Math/sin (- (/ (* 2 Math/PI time-secs) wavelength)
                       (* 2 Math/PI starting-point wavelength)))
          amplitude)
       med-price)))

Here is where we implement our sine wave model. We imagine this sine wave continuing into the future for infinity, and we can look at it at any point in time. For a given map for a stock code and a point in time, we’ll return a price for that stock. From the min and max, we work out a median price and amplitude, and we work out the price using the point in time.

Note the following from price_feed.clj:

 (defn curr-price
  "Given a stock code - get a price for the current point in time."
  [stock-code]
  (format "%.2f"
          (time-model
           (/ (java.lang.System/currentTimeMillis) 60)
           (stock-map stock-code))))

This function basically says, “For this stock code, get me the price right now.” It will retrieve the stock map and feed it into the sine wave function with the current time and return a price.

In the file service.clj, note the following in the namespace definition:

            [io.pedestal.http.sse :as sse]
            [clojure.data.json :as json]
            [stock-ticker-demo.price-feed :as price-feed]
            [clojure.core.async :as async])
  (:import  [java.util Date]
            [java.util.concurrent Executors]))

This gives us the capability to issue server-side events for our EventSource. It brings in a JSON library so we can convert Clojure maps to JSON strings. We bring in the logic from our price feed namespace. We also use the Java Date class and the Java concurrent libraries.

Note the pump-stock-prices function in service.clj:

(defn pump-stock-prices
  "Given a count - iterate returning a series of stock prices."
  [event-ch num-iterations ctx]
  (let [stock-code  (rand-nth price-feed/stock-codes)
        price (price-feed/curr-price stock-code)
        stock-info (price-feed/stock-map stock-code)
        eps (:eps stock-info)]
    (async/put! event-ch {:name "price"
                          :data (json/write-str
                                 {:stock-code stock-code :price price
                                  :eps eps})})
    (println
     (str stock-code " " price " " eps " " stock-info " "
          (java.util.Date.) " ")))
  (Thread/sleep  1000)
  (if (> num-iterations 0)
    (recur event-ch (dec num-iterations) ctx)
    (do
      (async/put! event-ch {:name "close" :data ""})
      (async/close! event-ch))))

This function randomly chooses a stock-code. It then retrieves the stock-info for this and puts this on the core.async channel for the EventSource. It then publishes the current iteration to the stdout. It then sleeps for one second. Then it does a recursive loop for the number of iterations in the parameter num-iterations. If it is out of iterations, it will close the EventSource and the channel. Note in particular that in the line stock-info (price-feed/stock-map stock-code) the string stock-code is used to pass into the map of the stock-map. In this case the map becomes a function that takes the key as a parameter.

In service.clj, we see the following function prices-stream:

(defn prices-stream
  "Starts sending price events to the channel."
  [event-ch ctx]
  (pump-stock-prices event-ch 100 ctx))

This function handles the new request from the defroutes macro, which calls the start-event-stream function. This kicks off the pump-stock-prices function with request channel and the request context. It also sets the iteration limit for the prices at 100, so the prices will be pumped for 100 seconds.

In service.clj, we use the function prices-called to handle an order when a Buy button on the website is clicked:

(defn prices-called
  "Feedback for when a stock order is placed on the web page"
  [{{order-json "order"} :form-params}]
  (let [order (json/read-str order-json :key-fn keyword)
        {stock-code :stock-code price :price} order]
    (println
     (str "An order has come in for: " stock-code " at: " price)))
  {:status 204})

A POST request is sent down to the routes function and then delegated to this function. Here we extract out the Buy order information to confirm the message has been received. This function is an exercise in destructuring. In the parameter list, instead of getting a named parameter, we assume it is a map of the request information, like this: [{{order-json "order"} :form-params}]. We then extract the value of the :form-params key, which is another map, and pass that to another destructuring, which takes the value that has the "order" key, and store that in order-json.

On the next line we read the JSON string into a Clojure map with order (json/read-str order-json :key-fn keyword). We then do another destructuring to key the stock-code and price with {stock-code :stock-code price :price} order. After that we print out what we have found and return an http 204 status success message.

In service.clj, we added the following to the defroutes macro call after the ^:interceptors line:

   ["/prices" {:get [::prices (sse/start-event-stream prices-stream)]
              :post prices-called}]

This enabled the defroutes macro call to handle GET and POST requests to the /prices path.

Conclusion

Here we’ve seen the capability for Pedestal to be used in a scenario involving long-running requests. We’ve published pricing information using the HTML5 EventSource standard in a way where request state was not bound to a thread and received AJAX posts representing orders.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.42.116