package org.marketcetera.options;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Pattern;
import org.marketcetera.trade.Option;
import org.marketcetera.trade.OptionType;
import org.marketcetera.util.misc.ClassVersion;
/* $License$ */
/**
* Various option related utilities.
*
* @author <a href="mailto:will@marketcetera.com">Will Horn</a>
* @version $Id: OptionUtils.java 16154 2012-07-14 16:34:05Z colin $
* @since 2.0.0
*/
@ClassVersion("$Id: OptionUtils.java 16154 2012-07-14 16:34:05Z colin $")
public class OptionUtils {
private static Calendar getSaturdayAfterThirdFriday(int month, int year) {
Calendar cal = new GregorianCalendar(year, month, 1);
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
int firstFridayOffset = (Calendar.FRIDAY - dayOfWeek + 7)%7;
cal.add(Calendar.DAY_OF_MONTH, firstFridayOffset + 15); // two weeks and a day
return cal;
}
/**
* Adds day to a YYYYMM expiry string. The day is the Saturday after the
* third Friday of the month (US standard rules). If the provided expiry is
* not YYYYMM, or cannot be normalized for any reason, null is returned.
*
* @param expiry an option expiry string
* @return the expiry in YYYYMMDD, or null
*/
public static String normalizeUSEquityOptionExpiry(String expiry) {
if (expiry.length() == 6) {
try {
int expiryYear = Integer.parseInt(expiry.substring(0, 4));
int expiryMonth = Integer.parseInt(expiry.substring(4));
if (expiryMonth > 0 && expiryMonth < 13) {
Calendar cal = getSaturdayAfterThirdFriday(expiryMonth - 1,
expiryYear);
int expiryDay = cal.get(Calendar.DAY_OF_MONTH);
return String.format("%s%02d", expiry, expiryDay); //$NON-NLS-1$
}
} catch (NumberFormatException e) {
// unsupported format, return expiry as is
}
}
return null;
}
/**
* Normalizes the supplied expiry date with a day if it doesn't
* include the day, ie. if it's in YYYYMM format. If the supplied
* expiry doesn't need to be normalized or cannot be normalized, a
* null value is returned back.
* <p>
* This method looks for a {@link OptionExpiryNormalizer custom} option
* expiry normalization implementation. If one is found, that implementation
* is used to carry out the option expiry normalization. If no such
* implementation is found the
* {@link #normalizeUSEquityOptionExpiry(String) US option expiry}
* normalization is applied.
*
* @param inExpiry the option expiry string.
*
* @return the expiry in YYYYMMDD, if the supplied expiry was normalized,
* null if it wasn't.
*/
public static String normalizeEquityOptionExpiry(String inExpiry) {
OptionExpiryNormalizer normalizer = getNormalizer();
if(normalizer != null) {
return normalizer.normalizeEquityOptionExpiry(inExpiry);
}
return normalizeUSEquityOptionExpiry(inExpiry);
}
/**
* Gets the <code>OptionType</code> value for the given character
* interpreted as an OSI-compliant symbol value.
*
* @param inOsiChar a <code>char</code> value
* @return an <code>OptionType</code> value corresponding with the given
* <code>character</code> or {@link OptionType#Unknown}
*/
public static OptionType getOptionTypeForOSICharacter(char inOsiChar)
{
switch(inOsiChar) {
case 'P':
return OptionType.Put;
case 'C':
return OptionType.Call;
default:
return OptionType.Unknown;
}
}
/**
* Gets an <code>Option</code> from the given full symbol if the given symbol
* complies with the option format set out in the
* <a href="http://www.theocc.com/initiatives/symbology/default.jsp">Option Symbology Initiative</a>.
*
* @param inFullSymbol a <code>String</code> value
* @return an <code>Option</code> value compliant with the OSI
* @throws IllegalArgumentException if the given full symbol is not OSI-compliant
*/
public static Option getOsiOptionFromString(String inFullSymbol)
{
// this is basic check for the symbol to see if syntactically conforms to the OSI standard - it does not check
// for valid dates, for example, just that the right type of character is at the right place in the symbol
if(OSI_SYMBOL_PATTERN.matcher(inFullSymbol).matches()) {
// we now know that the symbol is 21 characters long and can be split into sensical pieces
String symbol = inFullSymbol.substring(0,
6);
// note that the Option object expects a four-character year. the OSI standard doesn't specify how two-character
// years are to be interpreted.
String expiryYear = String.valueOf(getFullYear(Integer.parseInt(inFullSymbol.substring(6,
8))));
String expiryMonth = inFullSymbol.substring(8,
10);
String expiryDay = inFullSymbol.substring(10,
12);
OptionType type = getOptionTypeForOSICharacter(inFullSymbol.charAt(12));
BigDecimal strikeWhole = new BigDecimal(inFullSymbol.substring(13,
18));
BigDecimal strikeDecimal = new BigDecimal(inFullSymbol.substring(18,
21));
BigDecimal strike = new BigDecimal(String.format("%s.%s", //$NON-NLS-1$
strikeWhole,
strikeDecimal));
Option osiOption = new Option(symbol,
String.format("%s%s%s", //$NON-NLS-1$
expiryYear,
expiryMonth,
expiryDay),
strike,
type);
return osiOption;
}
throw new IllegalArgumentException();
}
/**
* Gets a string symbol as specified by <a
* href="http://www.theocc.com/initiatives/symbology/default.jsp">Option
* Symbology Initiative</a> from an <code>Option</code>. The provided option
* must have property values compliant with OSI tuples. Otherwise, an
* exception may be thrown, or an invalid OSI symbol may be returned.
*
* @param inOption an <code>Option</code> value
* @return a <code>String</code> value compliant with the OSI
* @throws IllegalArgumentException
* if the given option symbol is greater than 6 characters, if
* the expiry is not 8 digits, if the type is not {@code
* OptionType.Call} or {@code OptionType.Put}, or if the strike
* is negative, has more than 5 digits to the left of the
* decimal point, or more than 3 digits to the right.
*/
public static String getOsiSymbolFromOption(Option inOption)
{
String symbol = inOption.getSymbol();
if (symbol.length() > 6) {
throw new IllegalArgumentException();
}
String expiry = inOption.getExpiry().substring(2);
if (expiry.length() != 6) {
throw new IllegalArgumentException();
}
for (char c : expiry.toCharArray()) {
if (!Character.isDigit(c)) {
throw new IllegalArgumentException();
}
}
BigDecimal strike = inOption.getStrikePrice();
if (strike.signum() == -1 || strike.scale() > 3) {
throw new IllegalArgumentException();
}
long longStrike = inOption.getStrikePrice().movePointRight(3).longValue();
if (longStrike > 99999999) {
throw new IllegalArgumentException();
}
char type;
switch (inOption.getType()) {
case Call:
type = 'C';
break;
case Put:
type = 'P';
break;
default:
throw new IllegalArgumentException();
}
return String.format("%-6s%s%s%08d", //$NON-NLS-1$
symbol, expiry, type, longStrike);
}
/**
* Returns a full year complete with century from the given
* year-only value.
*
* <p>This method assumes that the given value is a number in the
* interval [0,99]. The returned value will be the given year plus
* the current century. If the resulting value < today, the returned
* value will be in the next century instead.
*
* @param inYear an <code>int</code> containing a number between 0 and 99 inclusive
* @return an <code>int</code> value containing a full year representation (century and year)
* @throws IllegalArgumentException if the given year is outside of the interval [0,99]
*/
private static int getFullYear(int inYear)
{
if(inYear < 0 ||
inYear > 99) {
throw new IllegalArgumentException();
}
// determine current century as an int
int currentYear = GregorianCalendar.getInstance().get(Calendar.YEAR);
int currentCentury = (currentYear / 100) * 100;
int extrapolatedYear = currentCentury + inYear;
// completeYear contains an int representing a specific year
if(extrapolatedYear < currentYear) {
extrapolatedYear += 100;
}
return extrapolatedYear;
}
/**
* Load the custom option expiry normalizer if any.
*
* @return the custom option expiry normalizer if found, null otherwise.
*/
private static OptionExpiryNormalizer getNormalizer() {
if (sNormalizerLoaded) {
return sOptionExpiryNormalizer;
}
synchronized (OptionUtils.class) {
if (!sNormalizerLoaded) {
Class<OptionExpiryNormalizer> normalizerClass = OptionExpiryNormalizer.class;
//Use the context class loader when unit testing to facilitate unit testing
/*
* The following section of code uses the classloader for this jar
* for loading the custom loader in a production install.
* It is written to use the thread context classloader within
* a unit test run to facilitate testing of this code from within
* a unit test.
* It is not desirable to use the context classloader in production
* as it might yield different results depending on the context
* it is invoked from.
*/
ClassLoader cl = sIsTest
? Thread.currentThread().getContextClassLoader()
: normalizerClass.getClassLoader();
OptionExpiryNormalizer normalizer = null;
try {
ServiceLoader<OptionExpiryNormalizer> loader = ServiceLoader.load(
normalizerClass, cl);
Iterator<OptionExpiryNormalizer> iter = loader.iterator();
if (iter.hasNext()) {
normalizer = iter.next();
}
} catch (Exception e) {
Messages.LOG_ERROR_LOADING_OPTION_EXPIRY_NORMALIZER.warn(OptionUtils.class, e);
} catch (ServiceConfigurationError e) {
Messages.LOG_ERROR_LOADING_OPTION_EXPIRY_NORMALIZER.warn(OptionUtils.class, e);
} finally {
if (normalizer != null) {
Messages.LOG_OPTION_EXPIRY_NORMALIZER_CUSTOMIZED.info(
OptionUtils.class,
normalizer.getClass().getName());
}
sOptionExpiryNormalizer = normalizer;
sNormalizerLoaded = true;
}
}
return sOptionExpiryNormalizer;
}
}
/**
* This method is provided to facilitate testing. It's not meant to be
* used outside of unit-testing.
*/
static void resetNormalizerLoaded() {
sNormalizerLoaded = false;
}
/**
* Sets up the class for testing. When setup for testing,
* the {@link #getNormalizer()} method uses the Thread context classloader
* to load the custom option normalizer instead of the current class'
* classloader.
* <p>
* This method is only meant to be invoked from a unit test.
*/
static void setupForTest() {
sIsTest = true;
}
/**
* this pattern does a basic syntax check of a symbol to see if it complies with the OSI
* note that this pattern does not check the expiry date to see if it is completely valid, for example Feb 31st would be valid
*/
private static final Pattern OSI_SYMBOL_PATTERN = Pattern.compile(".{6}\\d{2}(0\\d|1[0-2])(0[1-9]|[12]\\d|3[01])(C|P)\\d{5}\\d{3}"); //$NON-NLS-1$
private static volatile OptionExpiryNormalizer sOptionExpiryNormalizer;
private static volatile boolean sNormalizerLoaded = false;
private static volatile boolean sIsTest = false;
}