Index page

Stock Example
Minimal Example
Line Element Example
Scrolling Example
Normalize Example
Compounding Example
Dynamic Data Example
     Example index : Stock Example

Stock Example
Graphs the historical prices of a stock.

This example shows most of the Webgraph API in the context of a realistic application, such as graphing the historical prices of a stock.

back to index

The source code:

/**
 * @version $Id: StockExample.java,v 1.2 2000/11/13 18:47:17 Administrator Exp $
 */

import com.smartmoney.webgraph.*;
import com.smartmoney.webgraph.stocks.*;
import com.smartmoney.webgraph.stats.*;

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

/**
 * StockExample
 *
 * A sample application that exercises the important parts of the
 * webgraph public interface.  We create several GraphElements which
 * share the same GraphModel; we provide hiding and showing of graph
 * elements; we show two Graphs simultaneously, and have them
 * respond to each other's mouse events; we allow interactive panning
 * of the visible data set via a scrollbar; we show the use of
 * Graph.setNormalize(); we show the use of logarithmic and
 * linear scales on the Y axis; we use different Formatters; we show
 * the use of autoscaling and when to turn it off.
 *
 * @author <a href="mailto:jdf@pobox.com">Jonathan Feinberg</a>
 */
public class StockExample
        extends Applet
        implements ItemListener, AdjustmentListener, LegendListener, StockConstants
{
    static private final int ONE_MONTH = 5 * 4;  // 20 business days
    static private final int ONE_YEAR = 52 * 5;
    // How many days of stock data do we want?
    static private final int DAYS = ONE_YEAR * 5;  // 5 business years

    static private final int INITIAL_VIEW = ONE_YEAR;

    // What color do we give our main stock graph elements
    static private final Color mainColorDefault = Color.blue;

    // The "g" panel shows the stock prices; "v" shows the share
    // volume.  We can't display both in the same Graph because
    // they have different scales (typically, prices are on the order
    // of 10^2 and are displayed in dollars, while volume is on the
    // order of 10^6 and displayed in counting numbers).
    BusinessDateGraph priceGraph, volumeGraph;

    MovingAverageFilter maFilter;

    // The Graphs work by managing any number of graph elements.
    GraphElement line, hlc, candle, dividends, splits;
    GraphElement regression, movingAverage, bollinger;
    GraphElement volume, onBalanceVolume, moneyFlowIndex;
    StochasticOscillatorElement soK;

    // This object encapsulates raw data for an imaginary stock
    // instrument over some arbitrary number of days.  In a real-world
    // stock-graphing application, you'd need something like this that
    // actually retrieves stock data instead of making it up at
    // random.
    FakeStock mainFakeStock;
    String mainName;

    // Some other "instruments" to compare to our main stock
    String[] stockNames = new String[]{"MSFT", "T", "ATT", "C"};
    Color[] stockColors = new Color[]{Color.red, Color.green,
                                      Color.orange, Color.magenta};
    // We will keep track of the graph elements for the competitor
    // stocks in an array.  The LegendPanel objects allow the user to
    // show and hide individual elements (they are simply a checkbox
    // with a color square).
    GraphElement[] stocks;
    LegendPanel[] legends;

    // We use a Choice to select the currently visible time range
    Choice cRange;

    // We use a Choice to select the desired stock analysis
    Choice cAnalysis;

    // We use checkboxes to control which view of the main stock data
    // is active (simple line graph, HLC bars, or candles).
    Checkbox cbLine, cbHLC, cbCandle;

    // A checkbox to switch between log and linear scales
    Checkbox cbLog;

    // We use a scrollbar to control panning over the entire range of
    // the graph when viewing a subset.  See adjustmentValueChanged(),
    // below, for implementation details.
    Scrollbar panner;

    public void init()
    {
        ParameterParser parser = new ParameterParser(this, true);

        for (int i = 0; i < stockNames.length; i++)
        {
            String param = "stock" + (i + 1);
            stockColors[i] = parser.getColor(param + "color", stockColors[i]);
            stockNames[i] = parser.getString(param + "name", stockNames[i]);
        }

        setBackground(Color.white);

        // Create the main performance graph
        priceGraph = new BusinessDateGraph();
        // Create the volume graph.
        volumeGraph = new BusinessDateGraph();

        // And the control panel
        Panel controls = new Panel();
        //controls.setLayout(new FlowLayout(FlowLayout.LEFT));
        controls.setLayout(new GridLayout(0, 1));
        controls.setBackground(Color.white);

        // We always want to graph the volume with 0 at the bottom,
        // even though the volume will never go that low.
        volumeGraph.setGraphMinimum(0);
        // We don't want the volume graph to rescale itself when the
        // range of visible Y values changes through panning or
        // zooominpriceGraph.
        volumeGraph.setAutoScale(false);

        // We want each graph to display callouts even when the mouse
        // is moving in the other graph.
        priceGraph.addCalloutListener(volumeGraph);
        volumeGraph.addCalloutListener(priceGraph);

        // By default, a graph displays Y values as if they were
        // double-precision floating point numbers.  But the top graph
        // is in dollars, and the bottom graph is in whole numbers
        // (shares sold).
        String currencyFormatString = getParameter("currencyformat");
        if (currencyFormatString == null)
            priceGraph.setYFormatter(Formatters.CURRENCY);
        else
            priceGraph.setYFormatter(currencyFormatString);
        volumeGraph.setYFormatter(Formatters.PLAIN);

        // We'll display the date on the top graph only.
        volumeGraph.setDrawXValue(false);
        volumeGraph.setDrawXLabels(false);
        volumeGraph.setDrawYLabels(false);

        // This line of code represents whatever you have to do to
        // retrieve information about a given instrument.  Here we use
        // a utility class that generates a reasonable-looking random
        // stock history.
        mainFakeStock = new FakeStock(DAYS);

        // Create a StockModel that we can render with various
        // GraphElements.  Elements render models.  Several elements
        // can share the same model, which makes the Graph fairly
        // efficient.  This is a StockModel (which provides various
        // prices per date); later, we'll use some regular GraphModels.
        StockDataSource mainData
                = new StockDataSource(mainFakeStock.dates,
                        mainFakeStock.open,
                        mainFakeStock.high,
                        mainFakeStock.low,
                        mainFakeStock.close,
                        mainFakeStock.volume);
        DataSource priceData = new StockPriceFilter(mainData);
        DataSource volumeData = new StockVolumeFilter(mainData);

        GraphModel priceModel = new GraphModel(priceData);

        // Create several different elements, each of which emphasizes
        // a different aspect of the StockModel.

        Color mainColor = parser.getColor("mainstockcolor", mainColorDefault);
        mainName = parser.getString("mainstockname", "UBM");

        // A simple line to show the close price.
        line = new LineElement(priceModel, CLOSE);
        line.setColor(mainColor);
        // setLabel() puts an informative string into the element's callout
        line.setLabel(mainName);
        // We will only show one view of the model at at time.
        line.setVisible(false);
        // Now add it to the graph.
        priceGraph.addGraphElement(line);

        // A High/Low/Close graph element.
        hlc = new HLCElement(priceModel);
        hlc.setLabel(mainName);
        hlc.setColor(mainColor);
        priceGraph.addGraphElement(hlc);

        // A candle element (indicating high, low, open, close)
        candle = new CandleElement(priceModel);
        candle.setLabel(mainName);
        candle.setColor(mainColor);
        candle.setVisible(false);
        priceGraph.addGraphElement(candle);

        // Here we show dividends and splits by creating "annotation
        // models", models that refers to another model.  In the
        // following construction, for example, we're saying "create a
        // model that annotates the ubmModel model; for each date in
        // the mainFakeStock.divDates array, annotate the corresponding data
        // point in ubmModel with the corresponding string in
        // mainFakeStock.divDescriptions."

        AnnotationDataFilter adf
                = new AnnotationDataFilter(mainData,
                        CLOSE,
                        mainFakeStock.divDates,
                        mainFakeStock.divDescriptions,
                        AnnotationDataFilter.REJECT);
        AnnotationModel dm = new AnnotationModel(adf, priceModel);
        // The LetterAnnotationElement renders the AnnotationModel by
        // drawing a letter inside a cirle.
        dividends = new LetterAnnotationElement(dm, "D");
        dividends.setColor(mainColor);
        dividends.setLabel(mainName + " Dividend:");
        priceGraph.addGraphElement(dividends);
        // we'll only show it when the line element is visible
        dividends.setVisible(false);

        AnnotationDataFilter sdf
                = new AnnotationDataFilter(mainData,
                        CLOSE,
                        mainFakeStock.splitDates,
                        mainFakeStock.splitDescriptions,
                        AnnotationDataFilter.REJECT);
        AnnotationModel sm = new AnnotationModel(sdf, priceModel);
        splits = new LetterAnnotationElement(sm, "S");
        splits.setColor(mainColor);
        splits.setLabel(mainName + " Splits");
        splits.setVisible(false);
        priceGraph.addGraphElement(splits);

        // Now some fancy analysis graph elements
        regression = new LineElement(new RegressionModel(priceData, priceModel), CLOSE);
        regression.setColor(Color.gray);
        regression.setLabel(mainName + " Regression");
        regression.setVisible(false);
        priceGraph.addGraphElement(regression);

        maFilter = new MovingAverageFilter(priceData, 20);
        movingAverage = new LineElement(new GraphModel(maFilter, priceModel), CLOSE);
        movingAverage.setColor(Color.gray);
        movingAverage.setLabel(mainName + " 20-Day");
        movingAverage.setVisible(false);
        priceGraph.addGraphElement(movingAverage);

        bollinger = new BollingerBandsElement
                (new GraphModel(new BollingerBandsFilter(mainData), priceModel));
        bollinger.setColor(mainColor);  // for the callout
        bollinger.setLabel(mainName);
        bollinger.setVisible(false);
        priceGraph.addGraphElement(bollinger);

        // For the volume graph, we'll make a "shadow" element.
        volume = new ShadowElement(new GraphModel(volumeData));
        volume.setColor(mainColor);
        volume.setLabel(mainName + " Volume:");
        volumeGraph.addGraphElement(volume);

        onBalanceVolume = new LineElement(new GraphModel
                (new OnBalanceVolumeFilter(mainData)));
        onBalanceVolume.setColor(mainColor);
        onBalanceVolume.setLabel("OBV");
        onBalanceVolume.setVisible(false);
        volumeGraph.addGraphElement(onBalanceVolume);

        moneyFlowIndex = new LineElement(new GraphModel
                (new MoneyFlowIndexFilter(mainData)));
        moneyFlowIndex.setColor(mainColor);
        moneyFlowIndex.setLabel("MFI");
        moneyFlowIndex.setVisible(false);
        volumeGraph.addGraphElement(moneyFlowIndex);

        GraphModel soModel =
                new GraphModel
                        (new StochasticOscillatorFilter(mainData, 14, 3));
        soK = new StochasticOscillatorElement(soModel,
                StochasticOscillatorFilter.PERCENT_D, StochasticOscillatorFilter.PERCENT_K);
        soK.setKPeriodColor(Color.black);
        soK.setDPeriodColor(Color.red);
        soK.setLabel("Stochastic Osc.");
        soK.setVisible(false);
        volumeGraph.addGraphElement(soK);

        // Create the line elements for the "competition" stocks.
        stocks = new GraphElement[stockNames.length];
        for (int i = 0; i < stocks.length; i++)
        {
            FakeStock f = new FakeStock(DAYS);
            stocks[i] = new LineElement
                    (new GraphModel
                            (new SimpleDataSource(f.dates, f.close)));
            stocks[i].setColor(stockColors[i]);
            stocks[i].setLabel(stockNames[i]);
            stocks[i].setVisible(false);
            priceGraph.addGraphElement(stocks[i]);
        }

        // Here's a scrollbar that will control panning
        panner = new Scrollbar(Scrollbar.HORIZONTAL,
                DAYS - INITIAL_VIEW, INITIAL_VIEW,
                0, DAYS);
        panner.addAdjustmentListener(this);
        setGraphViews(DAYS - INITIAL_VIEW, INITIAL_VIEW);

        // Now some controls
        cbLog = new Checkbox("Use Log Scale");
        cbLog.addItemListener(this);

        controls.add(cbLog);

        controls.add(new Label("Time Period:"));
        cRange = new Choice();
        cRange.add("One Month");
        cRange.add("Three Months");
        cRange.add("Six Months");
        cRange.add("One Year");
        cRange.add("Five Years");
        cRange.addItemListener(this);

        controls.add(cRange);
        cRange.select(3);

        controls.add(new Label("Analysis:"));
        cAnalysis = new Choice();
        cAnalysis.add("No Analysis");
        cAnalysis.add("Regression Line");
        cAnalysis.add("20 Day Average");
        cAnalysis.add("50 Day Average");
        cAnalysis.add("Bollinger Bands");
        cAnalysis.add("On-Balance Volume");
        cAnalysis.add("Money Flow Index");
        cAnalysis.add("Stochastic Oscillator");
        cAnalysis.addItemListener(this);

        controls.add(cAnalysis);

        controls.add(new Spacer(100, 10));

        controls.add(new Label("View " + mainName + " as:"));
        CheckboxGroup cbg = new CheckboxGroup();
        cbLine = new Checkbox("Line", cbg, true);
        cbLine.addItemListener(this);

        controls.add(cbLine);
        cbHLC = new Checkbox("High/Low/Close", cbg, false);
        cbHLC.addItemListener(this);

        controls.add(cbHLC);
        cbCandle = new Checkbox("Candle", cbg, false);
        cbCandle.addItemListener(this);

        controls.add(cbCandle);

        // Start with HLC graph showing
        cbHLC.setState(true);

        controls.add(new Spacer(100, 10));

        controls.add(new Label("Compare " + mainName + " to:"));
        legends = new LegendPanel[stocks.length];
        for (int i = 0; i < stocks.length; i++)
        {
            legends[i] =
                    new LegendPanel(stockColors[i], stockNames[i], (LegendListener) this);
            controls.add(legends[i]);
        }

        // Layout the applet
        int h = getSize().height;
        int w = getSize().width;
        int gw = (int) (w * .75);
        int gh = (int) (h * .75);
        priceGraph.setSize(new Dimension(gw, gh));
        volumeGraph.setSize(new Dimension(gw, h - gh));
        Panel graphs = new Panel();
        graphs.setLayout(new BorderLayout());
        graphs.add(priceGraph, BorderLayout.CENTER);
        graphs.add(volumeGraph, BorderLayout.SOUTH);

        Panel pannedGraphs = new Panel();
        pannedGraphs.setLayout(new BorderLayout());
        pannedGraphs.add(graphs, BorderLayout.CENTER);
        pannedGraphs.add(panner, BorderLayout.SOUTH);

        setLayout(null);
        add(pannedGraphs);
        add(controls);
        pannedGraphs.setBounds(0, 0, gw, h);
        controls.setBounds(gw + 4, 0, w - gw - 4, h);

        GraphParameterParser.parse(priceGraph, parser);
        GraphParameterParser.parse(volumeGraph, parser);

        setRange(INITIAL_VIEW - 1);
    }

    // Here we hide and show the various views of the ubmModel based
    // on the state of some checkboxes.  We only show the dividends
    // and splits if the line element is selected.
    private void elementTypeSelected(Checkbox c)
    {
        dividends.setVisible(c == cbLine || c == cbHLC);
        splits.setVisible(c == cbLine || c == cbHLC);
        line.setVisible(c == cbLine);
        hlc.setVisible(c == cbHLC);
        candle.setVisible(c == cbCandle);
    }

    // This method shows the use of the Graph's setView() method,
    // which allows you to control the range of x values currently
    // visible.  The X values provided do not necessarily have to
    // correspond to values that appear in the X data, but this
    // implementation is simple and winds up looking good, too.
    private void setGraphViews(int startIndex, int range)
    {
        Date start = mainFakeStock.dates[startIndex];
        Date end = mainFakeStock.dates[startIndex + range - 1];
        priceGraph.setView(start, end);
        volumeGraph.setView(start, end);
    }

    // Given a range (i.e., the number of business days visible in the
    // graphs), set up the panner scrollbar to reflect the new range,
    // and make sure the graph still looks good.
    private void setRange(int range)
    {
        // Choose a harmonious X label format
        if (range < ONE_MONTH * 6)
            priceGraph.setXLabelFormatter(Formatters.LONG_DATE);
        else if (range < ONE_YEAR * 5)
            priceGraph.setXLabelFormatter(Formatters.MONTH_YEAR);
        else
            priceGraph.setXLabelFormatter(Formatters.YEAR);

        // We only wish to allow the display of Candle and HLC
        // graph elements below certain ranges, since they become
        // illegible when viewing many dates at once.
        if (range > ONE_MONTH * 6 && !cbLine.getState())
        {
            cbLine.setState(true);
            elementTypeSelected(cbLine);
        } else if (range > ONE_MONTH * 3 && cbCandle.getState())
        {
            cbHLC.setState(true);
            elementTypeSelected(cbHLC);
        }
        cbHLC.setEnabled(range <= ONE_MONTH * 6);
        cbCandle.setEnabled(range <= ONE_MONTH * 3);
        int start = panner.getValue();
        if (start + range >= DAYS)
            start -= start + range - DAYS;
        panner.setValues(start, range, 0, DAYS);
        setGraphViews(start, range);
    }

    public void itemStateChanged(ItemEvent e)
    {
        if (e.getItemSelectable() instanceof Checkbox)
        {
            Checkbox cb = (Checkbox) e.getItemSelectable();
            if (cb == cbLog)
            {
                // Here's how you tell a Graph to use logarithmic
                // Y scale (and how you tell it to switch back to
                // linear scale).
                if (cb.getState())
                    priceGraph.useLogScale();
                else
                    priceGraph.useLinearScale();
            } else
                elementTypeSelected(cb);
        } else if (e.getItemSelectable() instanceof Choice)
        {
            Choice c = (Choice) e.getItemSelectable();
            int n = c.getSelectedIndex();
            if (c == cRange)
            {
                if (n == 0)
                    setRange(ONE_MONTH);
                else if (n == 1)
                    setRange(ONE_MONTH * 3);
                else if (n == 2)
                    setRange(ONE_MONTH * 6);
                else if (n == 3)
                    setRange(ONE_YEAR);
                else if (n == 4) setRange(ONE_YEAR * 5);
            } else if (c == cAnalysis)
            {
                volume.setVisible(!(n >= 5));
                regression.setVisible(n == 1);
                movingAverage.setVisible(n == 2 || n == 3);
                bollinger.setVisible(n == 4);
                onBalanceVolume.setVisible(n == 5);
                moneyFlowIndex.setVisible(n == 6);
                soK.setVisible(n == 7);
                boolean volumeAnalysis = (n >= 5);
                volumeGraph.setDrawYLabels(volumeAnalysis);
                volumeGraph.setAutoScale(volumeAnalysis);
                volumeGraph.unsetGraphMinimum();
                volumeGraph.unsetGraphMaximum();
                if (n == 7 || !volumeAnalysis)
                    volumeGraph.setGraphMinimum(0);
                if (n == 7)
                    volumeGraph.setGraphMaximum(1);
                volumeGraph.setYFormatter(((n == 6) || (n == 7)) ?
                        Formatters.PERCENT : Formatters.PLAIN);
                if (n == 2 || n == 3)
                {
                    movingAverage.setLabel(mainName + " " + (n == 2 ? "20" : "50") + " Day");
                    maFilter.setPeriod(n == 2 ? 20 : 50);
                }
                if (onBalanceVolume.getVisible())
                    volumeGraph.setMessage("On-Balance Volume");
                else if (moneyFlowIndex.getVisible())
                    volumeGraph.setMessage("Money Flow Index");
                else
                    volumeGraph.setMessage(null);
            }
        }
    }

    // Handle messages from the panner scrollbar.  See the setView()
    // method, above.
    public void adjustmentValueChanged(AdjustmentEvent e)
    {
        Scrollbar s = (Scrollbar) e.getAdjustable();
        int value = e.getValue();
        setGraphViews(value, s.getVisibleAmount());
    }

    // The LegendPanels inform us that they've been clicked via this
    // method.  Note: if any of the competing stocks are showing, we
    // want to "normalize" the graph, i.e., to display the various
    // elements as percent change from the left of the graph.  Here's
    // how you do it.
    public void legendChanged(LegendPanel lp)
    {
        int count = 0;
        for (int i = 0; i < legends.length; i++)
        {
            if (legends[i].getState()) count++;
            stocks[i].setVisible(legends[i].getState());
        }
        priceGraph.setNormalize(count > 0);
    }

}





Copyright (c) 2006 SmartMoney.com