Commits

Chris Thunes committed 9fc3815

Add stock quote applet

Comments (0)

Files changed (6)

brewtab-ircbot/src/main/java/com/brewtab/ircbot/Bot.java

 import com.brewtab.ircbot.applets.GroupHugApplet;
 import com.brewtab.ircbot.applets.SpellApplet;
 import com.brewtab.ircbot.applets.StatsApplet;
+import com.brewtab.ircbot.applets.StockApplet;
 import com.brewtab.ircbot.applets.TextsFromLastNightApplet;
 import com.brewtab.ircbot.applets.TumblrApplet;
 import com.brewtab.ircbot.applets.UrbanDictionaryApplet;
         appletsListener.registerApplet(new EightBallApplet(), "8ball");
         appletsListener.registerApplet(new UrbanDictionaryApplet(), "urban");
         appletsListener.registerApplet(new GoogleSuggestsApplet(), "gs");
+        appletsListener.registerApplet(new StockApplet(), "stock");
     }
 
     private Connection createConnection() throws Exception {

brewtab-ircbot/src/main/java/com/brewtab/ircbot/applets/StockApplet.java

+package com.brewtab.ircbot.applets;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.cli.BasicParser;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+
+import com.brewtab.irc.User;
+import com.brewtab.irc.client.Channel;
+import com.brewtab.ircbot.applets.stock.Quote;
+import com.brewtab.ircbot.applets.stock.QuoteField;
+
+public class StockApplet implements BotApplet {
+    private Options options;
+    private CommandLineParser parser;
+
+    public StockApplet() {
+        options = new Options();
+        options.addOption("v", false, "verbose");
+        parser = new BasicParser();
+    }
+
+    private List<String> getResponse(String[] args) {
+        CommandLine cmdline;
+
+        try {
+            cmdline = parser.parse(options, args);
+        } catch (ParseException e) {
+            return Arrays.asList("Error: " + e.getMessage());
+        }
+
+        List<String> symbols = Arrays.asList(cmdline.getArgs());
+
+        if (symbols.size() == 0) {
+            return Arrays.asList("Error: at least one stock symbol must be given");
+        }
+
+        List<QuoteField> fields = Arrays.asList(
+            QuoteField.SYMBOL,
+            QuoteField.NAME,
+            QuoteField.LAST_TRADE_PRICE_ONLY,
+            QuoteField.CHANGE,
+            QuoteField.PERCENT_CHANGE,
+
+            QuoteField.MARKET_CAP,
+            QuoteField.PE_RATIO,
+            QuoteField.FIFTY_TWO_WEEK_HIGH,
+            QuoteField.FIFTY_TWO_WEEK_LOW
+            );
+
+        List<Quote> quotes = Quote.forSymbols(symbols, fields);
+        List<String> lines = new ArrayList<String>();
+
+        if (cmdline.hasOption('v')) {
+            for (Quote quote : quotes) {
+                lines.add(String.format("%s: %s", quote.get(QuoteField.SYMBOL), quote.get(QuoteField.NAME)));
+                lines.add(String.format("  Current: %s (%s, %s)",
+                    quote.get(QuoteField.LAST_TRADE_PRICE_ONLY),
+                    quote.get(QuoteField.CHANGE),
+                    quote.get(QuoteField.PERCENT_CHANGE)
+                    ));
+
+                for (QuoteField field : fields.subList(5, fields.size())) {
+                    lines.add(String.format("  %s %s", field.getDescription() + ":", quote.get(field)));
+                }
+            }
+        } else {
+            for (Quote quote : quotes) {
+                lines.add(String.format("%s: %s (%s, %s)",
+                    quote.get(QuoteField.SYMBOL),
+                    quote.get(QuoteField.LAST_TRADE_PRICE_ONLY),
+                    quote.get(QuoteField.CHANGE),
+                    quote.get(QuoteField.PERCENT_CHANGE)
+                    ));
+            }
+        }
+
+        return lines;
+    }
+
+    @Override
+    public void run(Channel channel, User from, String arg0, String[] args, String unparsed) {
+        List<String> lines = getResponse(args);
+        channel.writeMultiple(lines.toArray(new String[lines.size()]));
+    }
+
+    public static void main(String[] args) {
+        for (String line : new StockApplet().getResponse(args)) {
+            System.out.println(line);
+        }
+    }
+}

brewtab-ircbot/src/main/java/com/brewtab/ircbot/applets/stock/Quote.java

+package com.brewtab.ircbot.applets.stock;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.brewtab.ircbot.util.CSVUtils;
+import com.brewtab.ircbot.util.URLBuilder;
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+
+public class Quote {
+    private static final Logger log = LoggerFactory.getLogger(Quote.class);
+    private static final String baseURL = "http://finance.yahoo.com/d/quotes.csv";
+
+    private EnumMap<QuoteField, String> values;
+
+    public Quote(Map<QuoteField, String> values) {
+        this.values = new EnumMap<QuoteField, String>(values);
+    }
+
+    public String get(QuoteField field) {
+        String v = values.get(field);
+
+        if (v == null) {
+            throw new IllegalArgumentException("Field not populated");
+        }
+
+        return v;
+    }
+
+    public List<QuoteField> fields() {
+        return ImmutableList.copyOf(values.keySet());
+    }
+
+    private static List<List<String>> query(List<String> symbols, List<QuoteField> fields) {
+        StringBuilder sb = new StringBuilder();
+
+        for (QuoteField field : fields) {
+            sb.append(field.getTag());
+        }
+
+        String tags = sb.toString();
+
+        try {
+            URLBuilder urlBuilder = new URLBuilder(baseURL);
+            urlBuilder.setParameter("s", Joiner.on(' ').join(symbols));
+            urlBuilder.setParameter("f", tags);
+
+            log.info("Stock request: {}", urlBuilder.toString());
+
+            InputStream s = urlBuilder.toUrl().openStream();
+            ByteArrayOutputStream data = new ByteArrayOutputStream();
+            byte[] buff = new byte[1024];
+
+            while (true) {
+                int n = s.read(buff);
+
+                if (n < 0) {
+                    break;
+                }
+
+                data.write(buff, 0, n);
+            }
+
+            s.close();
+
+            String csvData = data.toString();
+            String[] csvLines = csvData.split("\n");
+            List<List<String>> results = new ArrayList<List<String>>();
+
+            for (String csvLine : csvLines) {
+                results.add(CSVUtils.csvSplit(csvLine));
+            }
+
+            return results;
+        } catch (Exception e) {
+            throw Throwables.propagate(e);
+        }
+    }
+
+    public static Quote forSymbol(String symbol, List<QuoteField> fields) {
+        return forSymbols(Arrays.asList(symbol), fields).get(0);
+    }
+
+    public static List<Quote> forSymbols(List<String> symbols, List<QuoteField> fields) {
+        List<Quote> quotes = new ArrayList<Quote>(symbols.size());
+        List<List<String>> quotesValues = query(symbols, fields);
+
+        for (List<String> values : quotesValues) {
+            if (values.size() != fields.size()) {
+                throw new RuntimeException("Number of fields returned does not match number of fields requested");
+            }
+
+            Map<QuoteField, String> m = new EnumMap<QuoteField, String>(QuoteField.class);
+
+            for (int i = 0; i < fields.size(); i++) {
+                m.put(fields.get(i), values.get(i));
+            }
+
+            quotes.add(new Quote(m));
+        }
+
+        return quotes;
+    }
+}

brewtab-ircbot/src/main/java/com/brewtab/ircbot/applets/stock/QuoteField.java

+package com.brewtab.ircbot.applets.stock;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum QuoteField {
+    ASK("a", "Ask"),
+    BID("b", "Bid"),
+    CHANGE_AND_PERCENT_CHANGE("c", "Change & Percent Change"),
+    DIVIDEND_PER_SHARE("d", "Dividend/Share"),
+    EARNINGS_PER_SHARE("e", "Earnings/Share"),
+    DAY_LOW("g", "Day's Low"),
+    DAY_HIGH("h", "Day's High"),
+    MORE_INFO("i", "More Info"),
+    FIFTY_TWO_WEEK_LOW("j", "52-week Low"),
+    FIFTY_TWO_WEEK_HIGH("k", "52-week High"),
+    LAST_TRADE_WITH_TIME("l", "Last Trade (With Time)"),
+    DAYS_RANGE("m", "Day's Range"),
+    NAME("n", "Name"),
+    OPEN("o", "Open"),
+    PREVIOUS_CLOSE("p", "Previous Close"),
+    EX_DIVIDEND_DATE("q", "Ex-Dividend Date"),
+    PE_RATIO("r", "P/E Ratio"),
+    SYMBOL("s", "Symbol"),
+    VOLUME("v", "Volume"),
+    FIFTY_TWO_WEEK_RANGE("w", "52-week Range"),
+    EXCHANGE("x", "Stock Exchange"),
+    DIVIDEND_YIELD("y", "Dividend Yield"),
+    AVERAGE_DAILY_VOLUME("a2", "Average Daily Volume"),
+    ASK_SIZE("a5", "Ask Size"),
+    ASK_REAL_TIME("b2", "Ask (Real-time)"),
+    BID_REAL_TIME("b3", "Bid (Real-time)"),
+    BOOK_VALUE("b4", "Book Value"),
+    BID_SIZE("b6", "Bid Size"),
+    CHANGE("c1", "Change"),
+    COMMISSION("c3", "Commission"),
+    CHANGE_REAL_TIME("c6", "Change (Real-time)"),
+    AFTER_HOURS_CHANGE_REAL_TIME("c8", "After Hours Change (Real-time)"),
+    LAST_TRADE_DATE("d1", "Last Trade Date"),
+    TRADE_DATE("d2", "Trade Date"),
+    ERROR_INDICATION("e1", "Error Indication (returned for symbol changed / invalid)"),
+    EPS_ESTIMATE_CURRENT_YEAR("e7", "EPS Estimate Current Year"),
+    EPS_ESTIMATE_NEXT_YEAR("e8", "EPS Estimate Next Year"),
+    EPS_ESTIMATE_NEXT_QUARTER("e9", "EPS Estimate Next Quarter"),
+    FLOAT_SHARES("f6", "Float Shares"),
+    HOLDINGS_GAIN_PERCENT("g1", "Holdings Gain Percent"),
+    ANNUALIZED_GAIN("g3", "Annualized Gain"),
+    HOLDINGS_GAIN("g4", "Holdings Gain"),
+    HOLDINGS_GAIN_PERCENT_REAL_TIME("g5", "Holdings Gain Percent (Real-time)"),
+    HOLDINGS_GAIN_REAL_TIME("g6", "Holdings Gain (Real-time)"),
+    ORDER_BOOK_REAL_TIME("i5", "Order Book (Real-time)"),
+    MARKET_CAP("j1", "Market Cap"),
+    MARKET_CAP_REAL_TIME("j3", "Market Cap (Real-time)"),
+    EBITDA("j4", "EBITDA"),
+    CHANGE_FROM_52_WEEK_LOW("j5", "Change From 52-week Low"),
+    PERCENT_CHANGE_FROM_52_WEEK_LOW("j6", "Percent Change From 52-week Low"),
+    LAST_TRADE_REAL_TIME_WITH_TIME("k1", "Last Trade (Real-time) With Time"),
+    PERCENT_CHANGE_REAL_TIME("k2", "Change Percent (Real-time)"),
+    LAST_TRADE_SIZE("k3", "Last Trade Size"),
+    CHANGE_FROM_52_WEEK_HIGH("k4", "Change From 52-week High"),
+    PERCEBT_CHANGE_FROM_52_WEEK_HIGH("k5", "Percebt Change From 52-week High"),
+    LAST_TRADE_PRICE_ONLY("l1", "Last Trade (Price Only)"),
+    HIGH_LIMIT("l2", "High Limit"),
+    LOW_LIMIT("l3", "Low Limit"),
+    DAYS_RANGE_REAL_TIME("m2", "Day's Range (Real-time)"),
+    FIFTY_DAY_MOVING_AVERAGE("m3", "50-day Moving Average"),
+    TWO_HUNDRED_DAY_MOVING_AVERAGE("m4", "200-day Moving Average"),
+    CHANGE_FROM_200_DAY_MOVING_AVERAGE("m5", "Change From 200-day Moving Average"),
+    PERCENT_CHANGE_FROM_200_DAY_MOVING_AVERAGE("m6", "Percent Change From 200-day Moving Average"),
+    CHANGE_FROM_50_DAY_MOVING_AVERAGE("m7", "Change From 50-day Moving Average"),
+    PERCENT_CHANGE_FROM_50_DAY_MOVING_AVERAGE("m8", "Percent Change From 50-day Moving Average"),
+    NOTES("n4", "Notes"),
+    PRICE_PAID("p1", "Price Paid"),
+    PERCENT_CHANGE("p2", "Change in Percent"),
+    PRICE_PER_SALES("p5", "Price/Sales"),
+    PRICE_PER_BOOK("p6", "Price/Book"),
+    DIVIDEND_PAY_DATE("r1", "Dividend Pay Date"),
+    PE_RATIO_REAL_TIME("r2", "P/E Ratio (Real-time)"),
+    PEG_RATIO("r5", "PEG Ratio"),
+    PRICE_EPS_ESTIMATE_CURRENT_YEAR("r6", "Price/EPS Estimate Current Year"),
+    PRICE_EPS_ESTIMATE_NEXT_YEAR("r7", "Price/EPS Estimate Next Year"),
+    SHARES_OWNED("s1", "Shares Owned"),
+    SHORT_RATIO("s7", "Short Ratio"),
+    LAST_TRADE_TIME("t1", "Last Trade Time"),
+    TRADE_LINKS("t6", "Trade Links"),
+    TICKER_TREND("t7", "Ticker Trend"),
+    ONE_YEAR_TARGET_PRICE("t8", "1 Year Target Price"),
+    HOLDINGS_VALUE("v1", "Holdings Value"),
+    HOLDINGS_VALUE_REAL_TIME("v7", "Holdings Value (Real-time)"),
+    DAYS_VALUE_CHANGE("w1", "Day's Value Change"),
+    DAYS_VALUE_CHANGE_REAL_TIME("w4", "Day's Value Change (Real-time)");
+
+    private static Map<String, QuoteField> byTag = new HashMap<String, QuoteField>();
+
+    static {
+        for (QuoteField field : values()) {
+            byTag.put(field.getTag(), field);
+        }
+    }
+
+    private String tag;
+    private String description;
+
+    QuoteField(String tag, String description) {
+        this.tag = tag;
+        this.description = description;
+    }
+
+    public String getTag() {
+        return tag;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public static QuoteField fromTag(String tag) {
+        return byTag.get(tag);
+    }
+}

brewtab-ircbot/src/main/java/com/brewtab/ircbot/util/CSVUtils.java

+package com.brewtab.ircbot.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CSVUtils {
+
+    public static List<String> csvSplit(String csvLine) {
+        ArrayList<String> fields = new ArrayList<String>();
+        StringBuilder sb = new StringBuilder();
+        int state = 0;
+        char c;
+
+        for (int i = 0; i < csvLine.length(); i++) {
+            c = csvLine.charAt(i);
+
+            switch (state) {
+            case 0:
+                /* Consume whitespace */
+                if (Character.isWhitespace(c)) {
+                    break;
+                }
+                state = 1;
+
+            case 1: // Reading field
+                if (c == '\"') {
+                    /* Enter quoted string */
+                    state = 2;
+                } else if (Character.isWhitespace(c) || c == ',') {
+                    /* End of field */
+                    fields.add(sb.toString());
+                    sb = new StringBuilder();
+
+                    if (c == ',') {
+                        state = 0;
+                    } else {
+                        state = 4;
+                    }
+                } else {
+                    sb.append(c);
+                }
+                break;
+
+            case 2: // Inside quotes
+                if (c == '\\') {
+                    state = 3;
+                } else if (c == '"') {
+                    state = 1;
+                } else {
+                    sb.append(c);
+                }
+                break;
+
+            case 3: // Escape character
+                if (c == '"') {
+                    sb.append('"');
+                }
+                state = 2;
+                break;
+
+            case 4: // End of argument
+                if (c == ',') {
+                    state = 0;
+                } else if (!Character.isWhitespace(c)) {
+                    throw new IllegalArgumentException("Expected comma after field, " + csvLine);
+                }
+                break;
+            }
+        }
+
+        if (state == 2 || state == 3) {
+            throw new IllegalArgumentException("Unmatched quote, " + csvLine);
+        }
+
+        /* Store last field */
+        if (sb.length() > 0) {
+            fields.add(sb.toString());
+        }
+
+        return fields;
+    }
+
+}

brewtab-ircbot/src/main/java/com/brewtab/ircbot/util/URLBuilder.java

         this.queryItems.put(key, value);
     }
 
+    public URL toUrl() throws MalformedURLException {
+        return new URL(toString());
+    }
+
     @Override
     public String toString() {
         StringBuilder buffer = new StringBuilder();