Java tutorial
/* * 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; } }