com.scottjjohnson.finance.analysis.WeeksTightCalculator.java Source code

Java tutorial

Introduction

Here is the source code for com.scottjjohnson.finance.analysis.WeeksTightCalculator.java

Source

/*
 * Copyright 2014 Scott J. Johnson (http://scottjjohnson.com)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.scottjjohnson.finance.analysis;

import java.util.Date;
import java.util.List;

import org.apache.commons.math3.stat.regression.SimpleRegression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scottjjohnson.finance.analysis.beans.WeeklyQuoteBean;
import com.scottjjohnson.finance.analysis.beans.WeeksTightBean;
import com.scottjjohnson.util.DateUtils;

/**
 * Implements an algorithm for finding a Weeks Tight chart pattern.
 * 
 * A Weeks Tight is a chart pattern where a stock's closing price is within a range of about 1% for at least 3 weeks.
 * 
 * A breakout from a Weeks Tight pattern is a lower risk opportunity to add to an existing stock position. For
 * aggressive investors it can be used to start a position.
 * 
 * The buy point is 10 cents above the highest intraday stock price during the pattern. To be buyable, the breakout
 * should be on volume at least 40% greater than the 50 day average volume. Prior to the Weeks Tight, the stock should
 * have had a significant prior uptrend after breaking out of a proper base (for example, a cup with handle).
 * 
 * This algorithm checks for closing price within 1.025%, that the buy point is above the 50 day SMA and that the stock
 * had a prior uptrend with a trendline with a 30 degree slope. Before buying any Weeks Tight reported by this program,
 * verify the stock has broken out of a proper base and made significant gains prior to the Weeks Tight forming.
 * 
 * New rule: If the intra-week high for the final 2 weeks of a WT are 2.5% or more below the high for the previous week
 * and the week after the WT is within that intra-week high, then ignore the previous week when calculating buy point.
 * Example: SLXP in July 2014. 3WT with intra-week highs of 140.49, 136.62, and 134.24. High for the Week after WT
 * completed was 136. So BP is 136.62 not 140.49. <a href=
 * "http://news.investors.com/investing-sector-leaders-review/072814-710645-salix-pharmaceuticals-forms-pattern.htm"
 * >More info.</a>
 * 
 * @see <a href="http://education.investors.com/investors-corner/629900-3-weeks-tight-can-bolster-your-returns.htm">3
 *      Weeks Tight Can Bolster Your Returns</a>
 */
public class WeeksTightCalculator {

    private static final Logger LOGGER = LoggerFactory.getLogger(WeeksTightCalculator.class);

    private static final int MIN_WEEKS_IN_A_WEEKS_TIGHT = 3;
    private static final int MAX_AGE_IN_WEEKS_OF_WT = 4; // ignore anything WT that completed > 4 weeks ago
    private static final float WEEKS_TIGHT_TOLERANCE = 0.0105f; // 1.05% -- allows for WT that would round down to 1.0%.
    private static final float SLXP_RULE_DEVIATION = 1.025f; // ratio for ignoring deviations in high early in pattern.
    private static final double MINIMUM_SLOPE_OF_TRENDLINE_UPTREND = 0.03d; // how steep of an uptrend is required prior
                                                                            // to WT
    private static final int MINIMUM_UPTREND_PERIOD_WEEKS = 6; // period over which there must be an uptrend for WT to
                                                               // be considered

    /**
     * Find a Weeks Tight pattern.
     * 
     * @param weeklyQuotes
     *            quotes list
     * @param FiftyDaySMA
     *            stock price of the 50 day SMA. Used to drop WT with buy point < than the 50 day SMA.
     * @return Weeks Tight bean
     */
    public WeeksTightBean calculate(List<WeeklyQuoteBean> weeklyQuotes, Float FiftyDaySMA) {

        WeeksTightBean wt = null;

        if (weeklyQuotes != null && weeklyQuotes.size() > 0) {

            String ticker = weeklyQuotes.get(0).getSymbol();

            LOGGER.debug("Searching for WT for symbol {}", ticker);

            wt = scanQuotesForWeeksTight(weeklyQuotes);

            // if the buy point is below the 50 day SMA then drop it.
            if (wt != null && wt.getBuyPoint() < FiftyDaySMA) {
                LOGGER.debug("Dropping WT for {} because buy point of {} is below 50 day SMA of {}", ticker,
                        wt.getBuyPoint(), FiftyDaySMA);
                wt = null;
            }
        } else
            LOGGER.error("Skipping symbol because weekly quotes list is null or empty.");

        return wt;
    }

    /**
     * Iterates over quote list looking for a Weeks Tight
     * 
     * @param quotes
     *            list of stock quote beans
     * @return Weeks Tight bean
     */
    protected WeeksTightBean scanQuotesForWeeksTight(List<WeeklyQuoteBean> quotes) {

        float minAdjClose = Float.MAX_VALUE;
        float maxAdjClose = 0f;
        float maxHigh = 0f;
        float minLow = 0f;
        float maxHighWeekAfter = Float.MAX_VALUE;
        float minCloseAnytimeAfter = Float.MAX_VALUE;
        float variance = 0f;

        String ticker = quotes.get(0).getSymbol();

        WeeklyQuoteBean possibleWeekPatternCompleted = null;
        WeeklyQuoteBean priorWeek = null;

        WeeksTightBean wt = null;
        int i = quotes.size();
        int j = 0;

        Date today = DateUtils.getStockExchangeCalendar().getTime();

        // loop through each week in reverse order starting at the current week-ending.
        // stop at i=1 because minimum weeks tight is 3. Only look back MAX_AGE_IN_WEEKS_OF_WT weeks.
        while (wt == null && i > 2 && i > quotes.size() - MAX_AGE_IN_WEEKS_OF_WT - 1) {

            i--;
            j = i;

            possibleWeekPatternCompleted = quotes.get(i);

            if (possibleWeekPatternCompleted.getWeekEndingDate().after(today)) {
                continue; // the bean is for the current week and the week is not complete yet so skip it
            }

            minAdjClose = possibleWeekPatternCompleted.getAdj_Close();
            maxAdjClose = possibleWeekPatternCompleted.getAdj_Close();
            maxHigh = possibleWeekPatternCompleted.getAdj_High();
            minLow = possibleWeekPatternCompleted.getAdj_Low();

            LOGGER.debug("Finding weeks tight for week ending {}.",
                    possibleWeekPatternCompleted.getWeekEndingDate());

            // for the week-ending, see how far back we can go and be within
            // tolerance for a weeks tight
            while (j > 0) {
                j--;

                priorWeek = quotes.get(j);

                minAdjClose = Math.min(minAdjClose, priorWeek.getAdj_Close());
                maxAdjClose = Math.max(maxAdjClose, priorWeek.getAdj_Close());

                LOGGER.debug("Checking week {}.", priorWeek.getWeekEndingDate());

                variance = Math.abs((minAdjClose / maxAdjClose) - 1);
                LOGGER.debug("variance = {}.", (variance));

                if (variance <= WEEKS_TIGHT_TOLERANCE) {
                    int patternLength = i - j + 1;

                    // This is the special "SLXP" rule mentioned above. It ignores drops > 2.5% in the week high earlier
                    // than 2 weeks into the pattern.
                    if (patternLength >= 3 && priorWeek.getAdj_High() >= SLXP_RULE_DEVIATION * maxHigh
                            && maxHighWeekAfter < maxHigh) {
                        // skip updating the maxHigh
                    } else
                        maxHigh = Math.max(maxHigh, priorWeek.getAdj_High());

                    minLow = Math.min(minLow, priorWeek.getAdj_Low());

                    // make sure we're at least 3 weeks into the Weeks Tight
                    if (patternLength >= MIN_WEEKS_IN_A_WEEKS_TIGHT && isStockInUptrend(
                            quotes.subList(Math.max(j - MINIMUM_UPTREND_PERIOD_WEEKS, 0), j + 1))) {
                        wt = new WeeksTightBean();
                        wt.setPatternEndingDate(possibleWeekPatternCompleted.getWeekEndingDate());
                        wt.setTickerSymbol(possibleWeekPatternCompleted.getSymbol());
                        wt.setWeeksTightLength(patternLength);
                        wt.setHighestPrice(maxHigh);
                        wt.setLowestPrice(minLow);
                    }

                    LOGGER.debug("WT bean = {}", wt);
                } else
                    break; // not in a weeks tight so skip to the next week
            }

            if (wt == null) {

                // if we're not in a WT save the high for this week in case we need it for the special "SLXP" rule
                // mentioned in the Javadoc.
                maxHighWeekAfter = possibleWeekPatternCompleted.getAdj_High();

                // also save the lowest close prior to the WT
                minCloseAnytimeAfter = Math.min(minCloseAnytimeAfter, possibleWeekPatternCompleted.getAdj_Close());
            }
        }

        if (wt != null) {
            if (minCloseAnytimeAfter < wt.getLowestPrice()) {
                LOGGER.debug(
                        "Dropping WT for {} because stock closed the week below the low of the pattern after the pattern completed.",
                        ticker, wt.getLowestPrice(), minCloseAnytimeAfter);
                wt = null;
            }
        }
        return wt;
    }

    /**
     * Determines if a stock's weekly closing prices are trending up.
     * 
     * @param quotes
     *            list of stock quote beans
     * @return true if the weekly closing prices are trending up, otherwise false
     */
    private boolean isStockInUptrend(List<WeeklyQuoteBean> quotes) {
        SimpleRegression r = new SimpleRegression(true);

        int numberOfQuotes = quotes.size();

        // Normalize all of the prices relative to the first quote's closing price.
        double firstClosingPrice = quotes.get(0).getAdj_Close();

        LOGGER.debug("Uptrend check: date = {}, first closing price = {}.", quotes.get(0).getWeekEndingDate(),
                firstClosingPrice);

        double d = 0;
        for (WeeklyQuoteBean quote : quotes) {
            LOGGER.debug("Uptrend check: date = {}, d = {}, data = {}, {}.", quote.getWeekEndingDate(), d,
                    d / numberOfQuotes, quote.getAdj_Close() / firstClosingPrice);
            r.addData(d / numberOfQuotes, quote.getAdj_Close() / firstClosingPrice);
            d++;
        }

        LOGGER.debug("Slope = {}, intercept = {}, min slope = {}.", r.getSlope(), r.getIntercept(),
                MINIMUM_SLOPE_OF_TRENDLINE_UPTREND);

        if (r.getSlope() >= MINIMUM_SLOPE_OF_TRENDLINE_UPTREND)
            return true;
        else
            return false;
    }
}