/* =========================================================== * TradeManager : An application to trade strategies for the Java(tm) platform * =========================================================== * * (C) Copyright 2011-2011, by Simon Allen and Contributors. * * Project Info: org.trade * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Java is a trademark or registered trademark of Oracle, Inc. * in the United States and other countries.] * * (C) Copyright 2011-2011, by Simon Allen and Contributors. * * Original Author: Simon Allen; * Contributor(s): -; * * Changes * ------- * */ package org.trade.strategy.data; import java.math.BigDecimal; import java.util.Collections; import java.util.LinkedList; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; import javax.persistence.Transient; import org.jfree.data.general.SeriesChangeEvent; import org.jfree.data.time.ohlc.OHLCSeriesCollection; import org.trade.persistent.dao.CodeValue; import org.trade.persistent.dao.Strategy; import org.trade.strategy.data.base.RegularTimePeriod; import org.trade.strategy.data.candle.CandleItem; import org.trade.strategy.data.stochasticoscillator.StochasticOscillatorItem; /** * Fast, Slow or Full * * There are three versions of the Stochastic Oscillator available on * SharpCharts. The Fast Stochastic Oscillator is based on George Lane's * original formulas for %K and %D. %K in the fast version that appears rather * choppy. %D is the 3-day SMA of %K. In fact, Lane used %D to generate buy or * sell signals based on bullish and bearish divergences. Lane asserts that a %D * divergence is the "only signal which will cause you to buy or sell." Because * %D in the Fast Stochastic Oscillator is used for signals, the Slow Stochastic * Oscillator was introduced to reflect this emphasis. The Slow Stochastic * Oscillator smooths %K with a 3-day SMA, which is exactly what %D is in the * Fast Stochastic Oscillator. Notice that %K in the Slow Stochastic Oscillator * equals %D in the Fast Stochastic Oscillator (chart 2). * * %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100 * * Fast Stochastic Oscillator: * * Fast %K = %K basic calculation * * Fast %D = 3-period SMA of Fast %K * * Slow Stochastic Oscillator: * * Slow %K = Fast %K smoothed with 3-period SMA * * Slow %D = 3-period SMA of Slow * * %K The Full Stochastic Oscillator is a fully customizable version of the Slow * Stochastic Oscillator. Users can set the look-back period, the number of * periods to slow %K and the number of periods for the %D moving average. The * default parameters were used in these examples: Fast Stochastic Oscillator * (14,3), Slow Stochastic Oscillator (14,3) and Full Stochastic Oscillator * (14,3,3). * * Full Stochastic Oscillator: * * Full %K = Fast %K smoothed with X-period SMA Full %D = X-period SMA of Full * %K * * * Developed by Larry Williams, Williams %R is a momentum indicator that is the * inverse of the Fast Stochastic Oscillator. Also referred to as %R, Williams * %R reflects the level of the close relative to the highest high for the * look-back period. In contrast, the Stochastic Oscillator reflects the level * of the close relative to the lowest low. %R corrects for the inversion by * multiplying the raw value by -100. As a result, the Fast Stochastic * Oscillator and Williams %R produce the exact same lines, only the scaling is * different. Williams %R oscillates from 0 to -100. Readings from 0 to -20 are * considered overbought. Readings from -80 to -100 are considered oversold. * Unsurprisingly, signals derived from the Stochastic Oscillator are also * applicable to Williams %R. * * %R = (Highest High - Close)/(Highest High - Lowest Low) * -100 * * Lowest Low = lowest low for the look-back period Highest High = highest high * for the look-back period %R is multiplied by -100 correct the inversion and * move the decimal. * * * @since 1.0.4 * * @see OHLCSeriesCollection * @author Simon Allen * @version $Revision: 1.0 $ */ @Entity @DiscriminatorValue("StochasticOscillatorSeries") public class StochasticOscillatorSeries extends IndicatorSeries { private static final long serialVersionUID = 20183087035446657L; public static final String LENGTH = "Length"; public static final String PERCENT_D = "PercentD"; public static final String KSMOOTHING = "KSmoothing"; public static final String INVERSE = "Inverse"; private Integer length; private Integer percentD; private Integer kSmoothing; private Boolean inverse; /* * Vales used to calculate StochasticOscillator. These need to be reset when * the series is cleared. */ private double sumFullKRValues = 0.0; private double sumFullDValues = 0.0; private LinkedList<Double> yyValues = new LinkedList<Double>(); private LinkedList<Double> fullKRValues = new LinkedList<Double>(); private LinkedList<Double> fullDValues = new LinkedList<Double>(); /** * Creates a new empty series. By default, items added to the series will be * sorted into ascending order by period, and duplicate periods will not be * allowed. * * * * @param strategy * Strategy * @param name * String * @param type * String * @param description * String * @param displayOnChart * Boolean * @param chartRGBColor * Integer * @param subChart * Boolean */ public StochasticOscillatorSeries(Strategy strategy, String name, String type, String description, Boolean displayOnChart, Integer chartRGBColor, Boolean subChart) { super(strategy, name, type, description, displayOnChart, chartRGBColor, subChart); } /** * Constructor for StochasticOscillatorSeries. * * @param strategy * Strategy * @param name * String * @param type * String * @param description * String * @param displayOnChart * Boolean * @param chartRGBColor * Integer * @param subChart * Boolean * @param length * Integer * @param smoothing * Integer * @param percentD * Integer */ public StochasticOscillatorSeries(Strategy strategy, String name, String type, String description, Boolean displayOnChart, Integer chartRGBColor, Boolean subChart, Integer length, Integer kSmoothing, Integer percentD) { super(strategy, name, type, description, displayOnChart, chartRGBColor, subChart); this.length = length; this.kSmoothing = kSmoothing; this.percentD = percentD; } public StochasticOscillatorSeries() { super(IndicatorSeries.StochasticOscillatorSeries); } /** * Method clone. * * @return Object * @throws CloneNotSupportedException */ public Object clone() throws CloneNotSupportedException { StochasticOscillatorSeries clone = (StochasticOscillatorSeries) super.clone(); clone.yyValues = new LinkedList<Double>(); clone.fullKRValues = new LinkedList<Double>(); clone.fullDValues = new LinkedList<Double>(); return clone; } /** * Removes all data items from the series and, unless the series is already * empty, sends a {@link SeriesChangeEvent} to all registered listeners. * Clears down and resets all the local calculated fields. */ public void clear() { super.clear(); sumFullKRValues = 0.0; sumFullDValues = 0.0; yyValues.clear(); fullKRValues.clear(); fullDValues.clear(); } /** * Returns the time period for the specified item. * * @param index * the item index. * * * @return The time period. */ public RegularTimePeriod getPeriod(int index) { final StochasticOscillatorItem item = (StochasticOscillatorItem) getDataItem(index); return item.getPeriod(); } /** * Adds a data item to the series. * * @param period * the period. * @param stochasticOscillator * the StochasticOscillator. */ public void add(RegularTimePeriod period, BigDecimal stochasticOscillator) { if (!this.isEmpty()) { StochasticOscillatorItem item0 = (StochasticOscillatorItem) this.getDataItem(0); if (!period.getClass().equals(item0.getPeriod().getClass())) { throw new IllegalArgumentException("Can't mix RegularTimePeriod class types."); } } super.add(new StochasticOscillatorItem(period, stochasticOscillator), true); } /** * Adds a data item to the series. * * * @param notify * the notify listeners. * @param dataItem * StochasticOscillatorItem */ public void add(StochasticOscillatorItem dataItem, boolean notify) { if (!this.isEmpty()) { StochasticOscillatorItem item0 = (StochasticOscillatorItem) this.getDataItem(0); if (!dataItem.getPeriod().getClass().equals(item0.getPeriod().getClass())) { throw new IllegalArgumentException("Can't mix RegularTimePeriod class types."); } } super.add(dataItem, notify); } /** * Method getLength. * * @return Integer */ @Transient public Integer getLength() { try { if (null == this.length) this.length = (Integer) CodeValue.getValueCode(LENGTH, this.getCodeValues()); if (this.length < 1) this.length = 1; } catch (Exception e) { this.length = 1; } return this.length; } /** * Method setLength. * * @param length * Integer */ public void setLength(Integer length) { this.length = length; } /** * Method getPercentD. * * @return Integer */ @Transient public Integer getPercentD() { try { if (null == this.percentD) this.percentD = (Integer) CodeValue.getValueCode(PERCENT_D, this.getCodeValues()); if (this.percentD < 1) this.percentD = 1; } catch (Exception e) { this.percentD = 1; } return this.percentD; } /** * Method setPercentD. * * @param percentD * Integer */ public void setPercentD(Integer percentD) { this.percentD = percentD; } /** * Method getKSmoothing. * * @return Integer */ @Transient public Integer getKSmoothing() { try { if (null == this.kSmoothing) this.kSmoothing = (Integer) CodeValue.getValueCode(KSMOOTHING, this.getCodeValues()); if (this.kSmoothing < 1) this.kSmoothing = 1; } catch (Exception e) { this.kSmoothing = 1; } return this.kSmoothing; } /** * Method setKSmoothing. * * @param kSmoothing * Integer */ public void setSmoothing(Integer kSmoothing) { this.kSmoothing = kSmoothing; } /** * Method getInverse. * * @return Boolean */ @Transient public Boolean getInverse() { try { if (null == this.inverse) this.inverse = (Boolean) CodeValue.getValueCode(INVERSE, this.getCodeValues()); } catch (Exception e) { this.inverse = null; } return this.inverse; } /** * Method setInverse. * * @param inverse * Boolean */ public void setInverse(Boolean inverse) { this.inverse = inverse; } /** * Method createSeries. * * @param source * CandleDataset * @param seriesIndex * int */ public void createSeries(CandleDataset source, int seriesIndex) { if (source.getSeries(seriesIndex) == null) { throw new IllegalArgumentException("Null source (CandleDataset)."); } for (int i = 0; i < source.getSeries(seriesIndex).getItemCount(); i++) { this.updateSeries(source.getSeries(seriesIndex), i, true); } } /** * Method updateSeries. * * @param source * CandleSeries * @param skip * int * @param newBar * boolean */ public void updateSeries(CandleSeries source, int skip, boolean newBar) { if (source == null) { throw new IllegalArgumentException("Null source (CandleSeries)."); } if (getLength() == null || getLength() < 1) { throw new IllegalArgumentException("SMA period must be greater than zero."); } if (source.getItemCount() > skip) { // get the current data item... CandleItem candleItem = (CandleItem) source.getDataItem(skip); if (0 != candleItem.getClose()) { if (this.yyValues.size() == getLength()) { /* * If the item does not exist in the series then this is a * new time period and so we need to remove the last in the * set and add the new periods values. Otherwise we just * update the last value in the set. Sum is just used for * performance save having to sum the last set of values * each time. */ if (newBar) { this.yyValues.removeLast(); this.yyValues.addFirst(candleItem.getClose()); } else { this.yyValues.removeFirst(); this.yyValues.addFirst(candleItem.getClose()); } } else { if (newBar) { this.yyValues.addFirst(candleItem.getClose()); } else { this.yyValues.removeFirst(); this.yyValues.addFirst(candleItem.getClose()); } } if (this.yyValues.size() == getLength()) { double high = Collections.max(this.yyValues); double low = Collections.min(this.yyValues); /* * %K = (Current Close - Lowest Low)/(Highest High - Lowest * Low) * 100 * * %D = 3-day SMA of %K * * Lowest Low = lowest low for the look-back period Highest * High = highest high for the look-back period %K is * multiplied by 100 to move the decimal point two places */ double fastKR = 0; if ((high - low) > 0) fastKR = ((candleItem.getClose() - low) / (high - low)) * 100; if (this.getInverse()) { /* * %R = (Highest High - Close)/(Highest High - Lowest * Low) * -100 * * Lowest Low = lowest low for the look-back period * Highest High = highest high for the look-back period * %R is multiplied by -100 correct the inversion and * move the decimal. */ fastKR = ((high - candleItem.getClose()) / (high - low)) * -100; } if (this.fullKRValues.size() == this.getKSmoothing()) { /* * If the item does not exist in the series then this is * a new time period and so we need to remove the last * in the set and add the new periods values. Otherwise * we just update the last value in the set. Sum is just * used for performance save having to sum the last set * of values each time. */ if (newBar) { sumFullKRValues = sumFullKRValues - this.fullKRValues.getLast() + fastKR; this.fullKRValues.removeLast(); this.fullKRValues.addFirst(fastKR); } else { sumFullKRValues = sumFullKRValues - this.fullKRValues.getFirst() + fastKR; this.fullKRValues.removeFirst(); this.fullKRValues.addFirst(fastKR); } } else { if (newBar) { sumFullKRValues = sumFullKRValues + fastKR; this.fullKRValues.addFirst(fastKR); } else { sumFullKRValues = sumFullKRValues + fastKR - this.fullKRValues.getFirst(); this.fullKRValues.removeFirst(); this.fullKRValues.addFirst(fastKR); } } if (this.fullKRValues.size() == this.getKSmoothing()) { double fullKR = sumFullKRValues / this.getKSmoothing(); if (this.fullDValues.size() == this.getPercentD()) { /* * If the item does not exist in the series then * this is a new time period and so we need to * remove the last in the set and add the new * periods values. Otherwise we just update the last * value in the set. Sum is just used for * performance save having to sum the last set of * values each time. */ if (newBar) { sumFullDValues = sumFullDValues - this.fullDValues.getLast() + fullKR; this.fullDValues.removeLast(); this.fullDValues.addFirst(fullKR); } else { sumFullDValues = sumFullDValues - this.fullDValues.getFirst() + fullKR; this.fullDValues.removeFirst(); this.fullDValues.addFirst(fullKR); } } else { if (newBar) { sumFullDValues = sumFullDValues + fullKR; this.fullDValues.addFirst(fullKR); } else { sumFullDValues = sumFullDValues + fullKR - this.fullDValues.getFirst(); this.fullDValues.removeFirst(); this.fullDValues.addFirst(fullKR); } } if (this.fullDValues.size() == this.getPercentD()) { double fullD = sumFullDValues / this.getPercentD(); if (newBar) { StochasticOscillatorItem dataItem = new StochasticOscillatorItem(candleItem.getPeriod(), new BigDecimal(fullD)); this.add(dataItem, false); } else { StochasticOscillatorItem dataItem = (StochasticOscillatorItem) this .getDataItem(this.getItemCount() - 1); dataItem.setStochasticOscillator(fullD); } } } } } } } /** * Method printSeries. * */ public void printSeries() { for (int i = 0; i < this.getItemCount(); i++) { StochasticOscillatorItem dataItem = (StochasticOscillatorItem) this.getDataItem(i); _log.debug("Type: " + this.getType() + " Time: " + dataItem.getPeriod().getStart() + " Value: " + dataItem.getStochasticOscillator()); } } }