Scalafication of a Java Equity Quote Application Part 1
01 July 2011 2 comments
Reading time:
11 minutes
Word count:
2299
Here follows in these series of three article is a description of Scala application that ported from Java. I would prefer to call this work a “Scalafication”.
Many people are thinking about Scala Adoption as it gets important interest from the wider community. Did you know that Professor Martin Odersky won the Java Top Ambassador award at the JAX Conference 2011? I think as you might do that Java the programming language is now very mature and that it is getting long in the tooth. However, Java is still very important for open source libraries, because the vast majority of the framework and library landscape, out there, has been very built with Java the programming language. Therefore interoperability is going to be important, and whenever they get around to my favourite [fictional] JSR 555, which will standardise the interoperability between Java and Beyond Java, the holy grail of meta-object protocol, virtual classes, static and dynamic languages and the JVM byte code executor will be a momentous occasion in the history of the Java platform. However, I digress and pat myself down due my excitement.
(Please start your favour MP3 player now on the track: Holst – The Planets – “Jupiter”. You are going need some inspiration.)
Overall Project Summary
The task for this Java project was to retrieve stock quotes from Yahoo Financial Quotes services, store these quotes in a repository, and all prospective users to interrogate the repository for a quotes. If the quote does exist in the repository then the application should go the Yahoo web service and pull down the quote. If it is newer store it in the cache and then return the result to the user.
Project Requirement
The Java application stored the latest quote on a data store (so that, say, a banking risk manager department could compare quotes to some trade that may have been made by the user through some other system). The application displayed the equity stock quote (the average of bid and ask prices) to the user.
The Java application was a prototype. The client envisioned Spring MVC and Hibernate as the web framework solution, which of course meant a relational database. The prototype was a single-user, and build with Apache Maven.
User Stories
For the original Java prototype, there were two user stories:
S1 “A trader wants to see the stock quote for a given ticker symbol so that she can buy and sell the relevant financial instrument at a good market price”.
S1-Acceptance Criteria
A1-1 “Type in a Yahoo ticker symbol and within one second see the current price. The current price should be approximately equal to that available from a Google search”
A1-2 “Type in an invalid ticker symbol and within one second see an error message”
S2 “A risk analyst wants to view a repository of ticker symbols together with the latest price in each case as last viewed by the stock system’s user so that I can compare these to prices traded by the user on other systems.”
S2-Acceptance Criteria
A2-1 “From an empty data store, use the stock system to view ORCL and then RHT noting their current value. Then query the data store to ensure those values have been recorded.”
A2-2 “Wait till the Oracle price has changed (a quarter of an hour during NY trading hours 2:30PM to 9:05PM UK time should be sufficient) and then re-query ORCL noting the new price. Then re-query the data store to ensure that the value had been updated for ORCL and the RHT value is unchanged”.
Original Java Interfaces
In order to port the prototype easy I started the Java interface and the unit tests.
Here is a design-by-contract interface for a quote repository
package uk.co.xenonique.stockquoteapp; import java.util.List; /** * A contract for a stock quote repository * @author Peter */ public interface QuoteRepository { public boolean isEmpty(); public int size(); public void clear(); public void store( StockQuote quote ); public StockQuote get( String symbol ); public boolean contains( String symbol ); public List<String> getSymbols(); }
Here, with QuoteRepository, the contract is to store and retrieve a stock quote by a symbol. You can check if the repository is empty or not, how many quotes it stores and clear it out. You can enquire also if stock quote is part of the repository by symbols, and finally you can a list collection of stock exchange symbols. By inspection, this interface, looks to be an associative collection, a map type.
Here is an implementation that uses a repository key value storage. Notice that I chose the ConcurrentHashMap to store the key value. Strictly, speaking it could have been a HashMap too, since the prototype was a single-user. Where have you seen a single-user system in existance if ever?
package uk.co.xenonique.stockquoteapp; import java.util.*; import java.util.concurrent.*;; /** * A key value store representation of the stock quote repository * @author Peter */ public class QuoteRepositoryKeyValueStore implements QuoteRepository { private Map<String, StockQuote> store = new ConcurrentHashMap<String, StockQuote>(); @Override public void clear() { store.clear(); } @Override public boolean contains(String symbol) { return store.containsKey(symbol); } @Override public StockQuote get(String symbol) { return store.get(symbol); } @Override public boolean isEmpty() { return store.isEmpty(); } @Override public int size() { return store.size(); } @Override public void store(StockQuote quote) { store.put(quote.getSymbol(), quote); } @Override public List<String> getSymbols() { List<String> symbols = new ArrayList<String>(); symbols.addAll( store.keySet() ); Collections.sort(symbols); return symbols; } }
Here is interface for a quote retriever
package uk.co.xenonique.stockquoteapp; public interface QuoteRetriever { public StockQuote getStockQuote( String symbol ); }
This is a contract to retrieve a stock quote from a system and return it the user.
Here is an implementation of it that talks to the Yahoo web service.
/** * */ package uk.co.xenonique.stockquoteapp; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.StringTokenizer; /** * Yahoo web site stock retriever * @author Peter */ public class QuoteRetrieverYahooImpl implements QuoteRetriever { public final static String TOKEN_SYMBOL="@SYMBOL@"; // private final static String URL_FRAGMENT="https://finance.yahoo.com/d/quotes.csv?s=@SYMBOL@&f=sb2b3jk"; private final static String DEFAULT_URL_FRAGMENT="https://finance.yahoo.com/d/quotes.csv?s="+TOKEN_SYMBOL+"&f=sb2b3"; private String urlFragment = DEFAULT_URL_FRAGMENT; @Override public StockQuote getStockQuote(String symbol) { String queryUrl = urlFragment.replace(TOKEN_SYMBOL, symbol); try { URL url = new URL( queryUrl ); HttpURLConnection connection = (HttpURLConnection)url.openConnection(); connection.setRequestMethod("GET"); connection.connect(); InputStream is = connection.getInputStream(); BufferedReader reader = new BufferedReader( new InputStreamReader(is) ); try { String line = reader.readLine(); if ( line != null ) { // System.out.println("*DEBUG* : line="+line); String stockSymbol; String askPrice; String bidPrice; StringTokenizer stk = new StringTokenizer(line, ","); int countTokens = stk.countTokens(); if ( countTokens < 3 ) { throw new DataRetrievalException("Data service at URL=["+queryUrl+"] does not supply enough fields (3 != "+countTokens+")"); } stockSymbol = stk.nextToken(); if ( stockSymbol.length() > 0 && stockSymbol.charAt(0) == '"' && stockSymbol.charAt(stockSymbol.length()-1) == '"') { stockSymbol = stockSymbol.substring(1, stockSymbol.length()-1 ); } askPrice = stk.nextToken(); if ( "N/A".equals(askPrice)) { throw new UnknownStockSymbolException("Unknown stock quote symbol ["+stockSymbol+"] price not available"); } bidPrice = stk.nextToken(); if ( "N/A".equals(bidPrice)) { throw new UnknownStockSymbolException("Unknown stock quote symbol ["+stockSymbol+"] price not available"); } return new StockQuote(stockSymbol, askPrice, bidPrice); } else { throw new DataRetrievalException("Data unavailable from quote service URL=["+queryUrl+"]"); } } finally { if ( reader != null) { try { reader.close(); } catch (Exception e) { } } if ( is != null) { try { is.close(); } catch (Exception e) { } } } } catch (MalformedURLException e) { throw new ConnectionException("unable to connect to the remote quote service URL=["+queryUrl+"]", e); } catch (IOException e) { throw new ConnectionException("I/O failure reading service URL=["+queryUrl+"]", e); } } public String getUrlFragment() { return urlFragment; } public void setUrlFragment(String urlFragment) { this.urlFragment = urlFragment; } }
This is all very straight forward Java Networking that you all should know and love by now.
Here is the equity stock quote data class
package uk.co.xenonique.stockquoteapp; import java.math.BigDecimal; import java.math.RoundingMode; public class StockQuote { private final String symbol; private final BigDecimal askPrice; private final BigDecimal bidPrice; private final BigDecimal meanPrice; private final static BigDecimal TWO= new BigDecimal("2.0"); public StockQuote(String symbol, String askPrice, String bidPrice) { this( symbol, new BigDecimal(askPrice), new BigDecimal(bidPrice)); } public StockQuote(String symbol, BigDecimal askPrice, BigDecimal bidPrice) { this.symbol = symbol; this.askPrice = askPrice.setScale(2); this.bidPrice = bidPrice.setScale(2); this.meanPrice = calcMeanPrice(askPrice, bidPrice); } /** * Copy constructor * @param ref the reference */ public StockQuote(StockQuote ref) { this.symbol = ref.symbol; this.askPrice = ref.askPrice.setScale(2); this.bidPrice = ref.bidPrice.setScale(2); this.meanPrice = bidPrice.add( askPrice.subtract(bidPrice).divide( TWO ) ); } private BigDecimal calcMeanPrice(BigDecimal askPrice, BigDecimal bidPrice) { return bidPrice.add( askPrice.subtract(bidPrice).divide( TWO ) ).setScale(2, RoundingMode.DOWN ); } public String getSymbol() { return symbol; } public BigDecimal getAskPrice() { return askPrice; } public BigDecimal getBidPrice() { return bidPrice; } public BigDecimal getMeanPrice() { return meanPrice; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((askPrice == null) ? 0 : askPrice.hashCode()); result = prime * result + ((bidPrice == null) ? 0 : bidPrice.hashCode()); result = prime * result + ((symbol == null) ? 0 : symbol.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; StockQuote other = (StockQuote) obj; if (askPrice == null) { if (other.askPrice != null) return false; } else if (!askPrice.equals(other.askPrice)) return false; if (bidPrice == null) { if (other.bidPrice != null) return false; } else if (!bidPrice.equals(other.bidPrice)) return false; if (symbol == null) { if (other.symbol != null) return false; } else if (!symbol.equals(other.symbol)) return false; return true; } @Override public String toString() { return "StockQuote [ symbol=" + symbol +", askPrice=" + askPrice + ", bidPrice=" + bidPrice + ", meanPrice="+meanPrice+"]"; } }
I need you to notice that it is an immutable object class. It is a stock quote that aggregates the symbol, ask and bid prices together. It borrows from C++ with a copy constructor, it has the hashCode() and equals() and a toString().
Finally, lets look at a unit test that ties it all together
package uk.co.xenonique.stockquoteapp; import org.junit.*; import static org.junit.Assert.*; public class QuoteRetrieverYahooTest { @BeforeClass public static void loadProxySettings() { StockQuoteApp.applyConnectionPropertiesFile(); } @Test public void shouldRetrieveStockQuote() { QuoteRetriever retriever = new QuoteRetrieverYahooImpl(); StockQuote quote = retriever.getStockQuote("ORCL"); assertNotNull(quote); assertEquals("ORCL", quote.getSymbol()); assertNotNull( quote.getAskPrice()); assertNotNull( quote.getBidPrice()); assertNotNull( quote.getMeanPrice()); } @Test(expected=UnknownStockSymbolException.class) public void shouldRaiseErrorWithUnknownStockSymbol() { QuoteRetriever retriever = new QuoteRetrieverYahooImpl(); retriever.getStockQuote("UNKNOWN"); } @Test(expected=ConnectionException.class) public void shouldRaiseConnectionFailure() { QuoteRetrieverYahooImpl retriever = new QuoteRetrieverYahooImpl(); retriever.setUrlFragment("https://foo.finance.yahoo.com/"+QuoteRetrieverYahooImpl.TOKEN_SYMBOL); retriever.getStockQuote("MFST"); } }
There is another unit test to verify the operation of the key value repository.
package uk.co.xenonique.stockquoteapp; import java.util.List; import org.junit.*; import static org.junit.Assert.*; public class QuoteRepositoryKeyValueTest { @Test public void shouldInitialiseWell() { QuoteRepository store = new QuoteRepositoryKeyValueStore(); assertTrue(store.isEmpty()); assertEquals(0,store.size()); } @Test public void shouldStoreAndRetrieve() { QuoteRepository store = new QuoteRepositoryKeyValueStore(); StockQuote expected1 = new StockQuote("PILGRIM", "123.45", "123.12" ); // Exercise the copy constructor in order to verify the store is not just using object references! store.store(new StockQuote(expected1)); assertFalse(store.isEmpty()); assertEquals(1,store.size()); assertEquals( expected1, store.get("PILGRIM")); // Put an updated stock quote in the repository with same symbol for good measure StockQuote expected2 = new StockQuote("PILGRIM", "555.55", "555.12" ); store.store(expected2); assertFalse(store.isEmpty()); assertEquals(1,store.size()); // The quote inside the store is not equal to the old stock quote assertNotSame( expected1, store.get("PILGRIM")); // The quote inside the store is equal to the new stock quote assertEquals( expected2, store.get("PILGRIM")); } @Test public void shouldStoreAndRetrieveMultipleQuotes() { QuoteRepository store = new QuoteRepositoryKeyValueStore(); assertTrue(store.isEmpty()); assertEquals(0,store.size()); StockQuote quotes[] = new StockQuote[] { new StockQuote( "LYG", "4.020", "4.015" ), new StockQuote( "RBS.L", "39.58", "39.48" ), new StockQuote( "HSBC.L", "646.65", "646.62" ), new StockQuote( "ORCL", "27.75", "27.74" ), }; for (StockQuote quote: quotes) { store.store(quote); } assertEquals( quotes.length, store.size()); assertFalse(store.isEmpty()); for (StockQuote quote: quotes) { assertTrue( store.contains(quote.getSymbol())); assertEquals( quote, store.get(quote.getSymbol())); } List<String> symbols = store.getSymbols(); assertEquals( quotes.length, symbols.size()); for (StockQuote quote: quotes) { assertTrue( symbols.contains(quote.getSymbol())); } store.clear(); assertTrue(store.isEmpty()); assertEquals(0,store.size()); } }
These were the essential parts of the Stock Quote application written in Java. I have left some notable bits and bobs, such the exceptions, and also the main program. You can treat it as an exercise-for-the-reader to tie it all together. I will publish the Java main program later on.
The static method call StockQuoteApp.applyConnectionPropertiesFile() is a method, that initialised the Java system properties with a configuration that allowed the application to work behind a company Internet proxy. The properties file looks like this:
# File: ``Connection.properties'' # ========================================== # Uncomment and set the proxy gateway for your environment # # Uncomment this line to use a Internet Proxy http.proxySet=true # Define the proxy hostmachine e.g. gateway.declan.server1 http.proxyHost=webcacge.proxyhost.mydomain.com # Define the port number of the proxy e.g. 8080 http.proxyPort=8080 # Define the login name http.proxyUser=PILG1472 # Define the password http.proxyPassword=PILGPASS # Define the non proxy hosts http.nonProxyHosts=otcdev57,otcdev1287,otcdev1244 # End
Next time. I will describe how I Scalafied these classes.