+ * Configure credentials and symbols via {@code intrinio/config.json} (loaded by the + * equities and options {@code Config.load()} helpers), or construct configs inline. + *
+ */ +public class GreekSampleApp { + + private static Timer timer; + private static GreekClient greekClient; + private static DataCache dataCache; + private static final ConcurrentHashMap+ * Produces implied volatility (via Newton–Raphson), delta, gamma, theta, vega, + * and ask/bid implied volatilities from the latest underlying price and option quote/trade. + * This calculator is pure and thread-safe; callers may invoke it concurrently. + *
+ */ +public final class BlackScholesGreekCalculator { + + /** Lower bound for binary-search style vol sweeps (reserved / historical). */ private static final double LOW_VOL = 0.0D; + /** Upper bound for binary-search style vol sweeps (reserved / historical). */ private static final double HIGH_VOL = 5.0D; + /** Volatility convergence tolerance (reserved / historical). */ private static final double VOL_TOLERANCE = 1e-12D; + /** Minimum z-score for the normal CDF approximation. */ private static final double MIN_Z_SCORE = -8.0D; + /** Maximum z-score for the normal CDF approximation. */ private static final double MAX_Z_SCORE = 8.0D; - private static final double root2Pi = Math.sqrt(2.0D * Math.PI); + /** Cached {@code sqrt(2 * pi)} for the normal PDF/CDF. */ + private static final double ROOT_2_PI = Math.sqrt(2.0D * Math.PI); + /** Seconds in a mean Gregorian year (365.25 days). */ + private static final double SECONDS_PER_YEAR = 31557600.0D; - public static Greek calculate(double riskFreeInterestRate, double dividendYield, double underlyingPrice, double latestEventUnixTimestamp, double marketPrice, boolean isPut, double strike, Date expirationDate) { - if (marketPrice <= 0.0D || riskFreeInterestRate <= 0.0D || underlyingPrice <= 0.0D) - return new Greek(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, false); + private BlackScholesGreekCalculator() { + } + + /** + * Calculates Black–Scholes Greeks for a single option observation. + * + * @param riskFreeInterestRate Continuous risk-free rate as a decimal (e.g. 0.05 for 5%) + * @param dividendYield Continuous dividend yield as a decimal + * @param underlyingPrice Latest underlying equity price + * @param latestEventUnixTimestamp Unix timestamp in seconds of the option event + * @param marketPrice Mid/market option price used for primary IV and Greeks + * @param askPrice Option ask price (used for ask IV; may be 0) + * @param bidPrice Option bid price (used for bid IV; may be 0) + * @param isPut {@code true} for puts, {@code false} for calls + * @param strike Option strike price + * @param expirationDate Option expiration instant + * @return A {@link Greek} result; {@link Greek#isValid()} is {@code false} when inputs are unusable + */ + public static Greek calculate(double riskFreeInterestRate, + double dividendYield, + double underlyingPrice, + double latestEventUnixTimestamp, + double marketPrice, + double askPrice, + double bidPrice, + boolean isPut, + double strike, + Instant expirationDate) { + if (marketPrice <= 0.0D || riskFreeInterestRate <= 0.0D || underlyingPrice <= 0.0D) { + return new Greek(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, false); + } double yearsToExpiration = getYearsToExpiration(latestEventUnixTimestamp, expirationDate); - if (yearsToExpiration <= 0.0D || strike <= 0.0D) - return new Greek(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, false); + if (yearsToExpiration <= 0.0D || strike <= 0.0D) { + return new Greek(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, false); + } - double impliedVolatility = calcImpliedVolatility(isPut, underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, dividendYield, marketPrice); - if (impliedVolatility == 0.0D) - return new Greek(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, false); + double impliedVolatility = calcImpliedVolatility( + isPut, underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, dividendYield, marketPrice); + if (impliedVolatility == 0.0D) { + return new Greek(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D, false); + } + + // Mirror C#: ask/bid IV only attempted when askPrice > 0 + double askImpliedVolatility = (askPrice > 0.0D) + ? calcImpliedVolatility(isPut, underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, dividendYield, askPrice) + : 0.0D; + double bidImpliedVolatility = (askPrice > 0.0D) + ? calcImpliedVolatility(isPut, underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, dividendYield, bidPrice) + : 0.0D; // Compute common values once for all Greeks to avoid redundant calcs double sqrtT = Math.sqrt(yearsToExpiration); @@ -37,63 +95,153 @@ public static Greek calculate(double riskFreeInterestRate, double dividendYield, double gamma = expQt * phiD1 / (underlyingPrice * impliedVolatility * sqrtT); double vega = 0.01D * underlyingPrice * expQt * sqrtT * phiD1; - // Theta with correct dividend adjustments + // Theta with dividend adjustments, scaled to calendar days double term1 = expQt * underlyingPrice * phiD1 * impliedVolatility / (2.0D * sqrtT); double term2 = riskFreeInterestRate * strike * expRt * (isPut ? (1.0D - nD2) : nD2); double term3 = dividendYield * underlyingPrice * expQt * (isPut ? (1.0D - nD1) : nD1); - double theta = isPut ? (-term1 + term2 - term3) / 365.25D : (-term1 - term2 + term3) / 365.25D; + double theta = isPut + ? (-term1 + term2 - term3) / 365.25D + : (-term1 - term2 + term3) / 365.25D; + + return new Greek(impliedVolatility, delta, gamma, theta, vega, askImpliedVolatility, bidImpliedVolatility, true); + } - return new Greek(impliedVolatility, delta, gamma, theta, vega, true); + /** + * Overload accepting {@link ZonedDateTime} expiration (as returned by option contract helpers). + */ + public static Greek calculate(double riskFreeInterestRate, + double dividendYield, + double underlyingPrice, + double latestEventUnixTimestamp, + double marketPrice, + double askPrice, + double bidPrice, + boolean isPut, + double strike, + ZonedDateTime expirationDate) { + return calculate(riskFreeInterestRate, dividendYield, underlyingPrice, latestEventUnixTimestamp, + marketPrice, askPrice, bidPrice, isPut, strike, expirationDate.toInstant()); } - private static double calcImpliedVolatility(boolean isPut, double underlyingPrice, double strike, double yearsToExpiration, double riskFreeInterestRate, double dividendYield, double marketPrice) { + /** + * Overload accepting {@link Date} expiration for callers that already hold a {@code Date}. + */ + public static Greek calculate(double riskFreeInterestRate, + double dividendYield, + double underlyingPrice, + double latestEventUnixTimestamp, + double marketPrice, + double askPrice, + double bidPrice, + boolean isPut, + double strike, + Date expirationDate) { + return calculate(riskFreeInterestRate, dividendYield, underlyingPrice, latestEventUnixTimestamp, + marketPrice, askPrice, bidPrice, isPut, strike, expirationDate.toInstant()); + } + + /** + * Legacy overload without explicit ask/bid prices (ask/bid IV will be 0). + */ + public static Greek calculate(double riskFreeInterestRate, + double dividendYield, + double underlyingPrice, + double latestEventUnixTimestamp, + double marketPrice, + boolean isPut, + double strike, + Date expirationDate) { + return calculate(riskFreeInterestRate, dividendYield, underlyingPrice, latestEventUnixTimestamp, + marketPrice, 0.0D, 0.0D, isPut, strike, expirationDate.toInstant()); + } + + /** + * Newton–Raphson implied volatility solve. + */ + private static double calcImpliedVolatility(boolean isPut, + double underlyingPrice, + double strike, + double yearsToExpiration, + double riskFreeInterestRate, + double dividendYield, + double marketPrice) { double tol = 1e-10D; double forward = underlyingPrice * Math.exp((riskFreeInterestRate - dividendYield) * yearsToExpiration); double m = forward / strike; double sigma = Math.sqrt(2.0D * Math.abs(Math.log(m)) / yearsToExpiration); - if (Double.isNaN(sigma) || sigma <= 0.0D) sigma = 0.3D; + if (Double.isNaN(sigma) || sigma <= 0.0D) { + sigma = 0.3D; + } int maxIter = 50; for (int iter = 0; iter < maxIter; iter++) { - double price = isPut ? calcPricePut(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield) : calcPriceCall(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); + double price = isPut + ? calcPricePut(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield) + : calcPriceCall(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); double diff = price - marketPrice; - if (Math.abs(diff) < tol) break; + if (Math.abs(diff) < tol) { + break; + } - double d1 = d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); - double vega = underlyingPrice * Math.exp(-dividendYield * yearsToExpiration) * Math.sqrt(yearsToExpiration) * normalPdf(d1); - if (Math.abs(vega) < 1e-10D) break; // avoid division by zero + double d1Val = d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); + double vega = underlyingPrice * Math.exp(-dividendYield * yearsToExpiration) + * Math.sqrt(yearsToExpiration) * normalPdf(d1Val); + if (Math.abs(vega) < 1e-10D) { + break; // avoid division by zero + } sigma -= diff / vega; - if (sigma <= 0.0D) sigma = 0.0001D; // prevent negative or zero + if (sigma <= 0.0D) { + sigma = 0.0001D; // prevent negative or zero + } } return sigma; } - private static double d1(double underlyingPrice, double strike, double yearsToExpiration, double riskFreeInterestRate, double sigma, double dividendYield) { - double numerator = Math.log(underlyingPrice / strike) + (riskFreeInterestRate - dividendYield + 0.5D * sigma * sigma) * yearsToExpiration; + private static double d1(double underlyingPrice, + double strike, + double yearsToExpiration, + double riskFreeInterestRate, + double sigma, + double dividendYield) { + double numerator = Math.log(underlyingPrice / strike) + + (riskFreeInterestRate - dividendYield + 0.5D * sigma * sigma) * yearsToExpiration; double denominator = sigma * Math.sqrt(yearsToExpiration); return numerator / denominator; } - private static double d2(double underlyingPrice, double strike, double yearsToExpiration, double riskFreeInterestRate, double sigma, double dividendYield) { - return d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield) - sigma * Math.sqrt(yearsToExpiration); + private static double d2(double underlyingPrice, + double strike, + double yearsToExpiration, + double riskFreeInterestRate, + double sigma, + double dividendYield) { + return d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield) + - sigma * Math.sqrt(yearsToExpiration); } private static double cumulativeNormalDistribution(double z) { - if (Math.abs(z) < 1.5D) + if (Math.abs(z) < 1.5D) { return cumulativeNormalDistributionSeries(z); + } - if (z > MAX_Z_SCORE) return 1.0D; - if (z < MIN_Z_SCORE) return 0.0D; + if (z > MAX_Z_SCORE) { + return 1.0D; + } + if (z < MIN_Z_SCORE) { + return 0.0D; + } boolean isNegative = z < 0.0D; - if (isNegative) z = -z; + if (isNegative) { + z = -z; + } double t = 1.0D / (1.0D + 0.2316419D * z); double poly = t * (0.319381530D + t * (-0.356563782D + t * (1.781477937D + t * (-1.821255978D + t * 1.330274429D)))); - double pdf = Math.exp(-0.5D * z * z) / root2Pi; + double pdf = Math.exp(-0.5D * z * z) / ROOT_2_PI; double tail = pdf * poly; return isNegative ? tail : 1.0D - tail; @@ -109,33 +257,52 @@ private static double cumulativeNormalDistributionSeries(double z) { term = term * absZ * absZ / i; i += 2.0D; } - double pdf = Math.exp(-0.5D * absZ * absZ) / root2Pi; + double pdf = Math.exp(-0.5D * absZ * absZ) / ROOT_2_PI; double half = pdf * sum; return z >= 0.0D ? 0.5D + half : 0.5D - half; } private static double normalPdf(double x) { - return Math.exp(-0.5D * x * x) / root2Pi; + return Math.exp(-0.5D * x * x) / ROOT_2_PI; } - private static double calcPriceCall(double underlyingPrice, double strike, double yearsToExpiration, double riskFreeInterestRate, double sigma, double dividendYield) { - double d1 = d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); - double d2 = d1 - sigma * Math.sqrt(yearsToExpiration); + private static double calcPriceCall(double underlyingPrice, + double strike, + double yearsToExpiration, + double riskFreeInterestRate, + double sigma, + double dividendYield) { + double d1Val = d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); + double d2Val = d1Val - sigma * Math.sqrt(yearsToExpiration); double discountedUnderlying = Math.exp(-dividendYield * yearsToExpiration) * underlyingPrice; double discountedStrike = Math.exp(-riskFreeInterestRate * yearsToExpiration) * strike; - return discountedUnderlying * cumulativeNormalDistribution(d1) - discountedStrike * cumulativeNormalDistribution(d2); + return discountedUnderlying * cumulativeNormalDistribution(d1Val) + - discountedStrike * cumulativeNormalDistribution(d2Val); } - private static double calcPricePut(double underlyingPrice, double strike, double yearsToExpiration, double riskFreeInterestRate, double sigma, double dividendYield) { - double d1 = d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); - double d2 = d1 - sigma * Math.sqrt(yearsToExpiration); + private static double calcPricePut(double underlyingPrice, + double strike, + double yearsToExpiration, + double riskFreeInterestRate, + double sigma, + double dividendYield) { + double d1Val = d1(underlyingPrice, strike, yearsToExpiration, riskFreeInterestRate, sigma, dividendYield); + double d2Val = d1Val - sigma * Math.sqrt(yearsToExpiration); double discountedUnderlying = Math.exp(-dividendYield * yearsToExpiration) * underlyingPrice; double discountedStrike = Math.exp(-riskFreeInterestRate * yearsToExpiration) * strike; - return discountedStrike * cumulativeNormalDistribution(-d2) - discountedUnderlying * cumulativeNormalDistribution(-d1); + return discountedStrike * cumulativeNormalDistribution(-d2Val) + - discountedUnderlying * cumulativeNormalDistribution(-d1Val); } - private static double getYearsToExpiration(double latestActivityUnixTime, Date expirationDate) { - double expiration = expirationDate.getTime() / 1000.0D; - return (expiration - latestActivityUnixTime) / 31557600.0D; + /** + * Years from the option event time to expiration, using a 365.25-day year. + * + * @param latestActivityUnixTime option event time in unix seconds + * @param expirationDate expiration instant + * @return fractional years to expiration (may be <= 0 if expired) + */ + private static double getYearsToExpiration(double latestActivityUnixTime, Instant expirationDate) { + double expiration = expirationDate.getEpochSecond() + expirationDate.getNano() / 1_000_000_000.0D; + return (expiration - latestActivityUnixTime) / SECONDS_PER_YEAR; } -} \ No newline at end of file +} diff --git a/src/intrinio/realtime/composite/CalculateNewGreek.java b/src/intrinio/realtime/composite/CalculateNewGreek.java index c45bd6b..3912889 100644 --- a/src/intrinio/realtime/composite/CalculateNewGreek.java +++ b/src/intrinio/realtime/composite/CalculateNewGreek.java @@ -1,6 +1,23 @@ package intrinio.realtime.composite; +/** + * Strategy used by {@link GreekClient} to compute and store Greeks for a single option contract. + *+ * Implementations typically read the latest equities/options state and supplemental rates from + * the provided cache objects, compute a {@link Greek}, and write it via + * {@link DataCache#setOptionGreekData}. They must tolerate concurrent, non-transactional + * cache state (fields may change while the calculator runs). + *
+ */ @FunctionalInterface public interface CalculateNewGreek { + + /** + * Compute and optionally store a new Greek for the given contract. + * + * @param optionsContractData contract-level cache slice + * @param securityData underlying security cache slice + * @param dataCache top-level composite cache + */ void calculateNewGreek(OptionsContractData optionsContractData, SecurityData securityData, DataCache dataCache); } diff --git a/src/intrinio/realtime/composite/CurrentDataCache.java b/src/intrinio/realtime/composite/CurrentDataCache.java index d8163b2..1f86462 100644 --- a/src/intrinio/realtime/composite/CurrentDataCache.java +++ b/src/intrinio/realtime/composite/CurrentDataCache.java @@ -4,301 +4,442 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +/** + * Default {@link DataCache} implementation. + *+ * Uses {@link ConcurrentHashMap} for securities and top-level supplemental data. + * Per-field market-data updates are dirty sets (no locks, no transactions): + * a writer may overwrite another writer's value if timestamps allow, and readers may observe + * partially updated aggregates. Callbacks are invoked synchronously on the updating thread; + * exceptions in callbacks are logged and swallowed so the feed is not disrupted. + *
+ */ class CurrentDataCache implements DataCache { + + /** Concurrent map of ticker → security sub-cache. */ private final ConcurrentHashMap+ * Trade and quote fields use non-locking dirty timestamp checks. Refresh and unusual activity + * always overwrite. Supplemental and Greek maps are concurrent. Concurrent writers may interleave; + * this is intentional and matches the C# non-transactional cache design. + *
*/ class CurrentOptionsContractData implements OptionsContractData { + + /** Option contract identifier. */ private final String contract; - private Trade latestTrade; - private Quote latestQuote; - private Refresh latestRefresh; - private UnusualActivity latestUnusualActivity; + + /** Latest trade (dirty-set by timestamp). */ + private volatile Trade latestTrade; + + /** Latest quote (dirty-set by timestamp). */ + private volatile Quote latestQuote; + + /** Latest refresh (always overwritten). */ + private volatile Refresh latestRefresh; + + /** Latest unusual activity (always overwritten). */ + private volatile UnusualActivity latestUnusualActivity; + + /** Concurrent map of contract-level supplemental numerics. */ private final ConcurrentHashMap+ * Latest equities trade/quote fields are updated with non-locking dirty timestamp checks. + * Nested option contracts live in a {@link ConcurrentHashMap}. Concurrent writers may interleave; + * this is intentional and matches the C# non-transactional cache design. + *
+ */ class CurrentSecurityData implements SecurityData { + + /** Equity ticker for this cache entry. */ private final String tickerSymbol; - private intrinio.realtime.equities.Trade latestTrade; - private intrinio.realtime.equities.Quote latestAskQuote; - private intrinio.realtime.equities.Quote latestBidQuote; + + /** Latest equities trade (dirty-set by timestamp). */ + private volatile intrinio.realtime.equities.Trade latestTrade; + + /** Latest equities ask quote (dirty-set by timestamp). */ + private volatile intrinio.realtime.equities.Quote latestAskQuote; + + /** Latest equities bid quote (dirty-set by timestamp). */ + private volatile intrinio.realtime.equities.Quote latestBidQuote; + + /** Concurrent map of option contract id → contract cache. */ private final ConcurrentHashMap+ * Concurrency model: this cache does not provide transactional + * snapshots. Updates use concurrent maps and “dirty” timestamp checks (accept a value only + * when it is newer than what is stored). Readers may observe a mix of old and new fields + * across concurrent writers. Callbacks run on the calling/update thread and should be short. + *
+ *+ * Obtain instances via {@link DataCacheFactory#create()}. + *
*/ public interface DataCache { + //region Supplementary Data + /** - * Get a supplementary data point from the general cache. + * Get a supplementary data point from the general (top-level) cache. + * + * @param key datum key + * @return the value, or {@code null} if absent */ Double getSupplementaryDatum(String key); /** * Set a supplementary data point in the general cache. + * The provided {@link SupplementalDatumUpdate} merges old and new values atomically per key. + * + * @param key datum key + * @param datum new value (may be {@code null} to clear via the update function) + * @param update merge function + * @return {@code true} if the stored value equals {@code datum} after the update */ boolean setSupplementaryDatum(String key, Double datum, SupplementalDatumUpdate update); /** - * Get all supplementary data stored at the top level general cache. + * Get all supplementary data stored at the top-level general cache. + * The returned map is a live, unmodifiable view of concurrent storage. + * + * @return unmodifiable view of top-level supplemental data */ Map+ * Each call to {@link #create()} returns a new, empty, thread-safe, non-transactional cache + * suitable for wiring into equities/options WebSocket handlers and {@link GreekClient}. + *
+ */ +public final class DataCacheFactory { + + private DataCacheFactory() { + } + + /** + * @return a new {@link DataCache} implementation + */ public static DataCache create() { return new CurrentDataCache(); } -} \ No newline at end of file +} diff --git a/src/intrinio/realtime/composite/Greek.java b/src/intrinio/realtime/composite/Greek.java index a7f3dc0..0a55bd0 100644 --- a/src/intrinio/realtime/composite/Greek.java +++ b/src/intrinio/realtime/composite/Greek.java @@ -1,3 +1,59 @@ package intrinio.realtime.composite; -public record Greek (double ImpliedVolatility, double Delta, double Gamma, double Theta, double Vega, boolean IsValid){} \ No newline at end of file +/** + * Immutable container for option Greek values produced by a Greek calculator + * (for example {@link BlackScholesGreekCalculator}). + *+ * Instances are intended to be stored in the non-transactional composite cache + * and published to callbacks. Equality is value-based so cache update functions + * can detect whether a newly computed Greek is identical to the previous value. + *
+ * + * @param impliedVolatility Mid/market implied volatility used for the primary Greeks + * @param delta Option delta + * @param gamma Option gamma + * @param theta Option theta (per calendar day) + * @param vega Option vega (per 1% volatility move) + * @param askImpliedVolatility Implied volatility solved from the ask price (0 if unavailable) + * @param bidImpliedVolatility Implied volatility solved from the bid price (0 if unavailable) + * @param isValid {@code true} when the calculation succeeded with usable inputs + */ +public record Greek( + double impliedVolatility, + double delta, + double gamma, + double theta, + double vega, + double askImpliedVolatility, + double bidImpliedVolatility, + boolean isValid) { + + /** + * Convenience constructor matching the historical field layout without ask/bid IVs. + * Ask and bid implied volatilities default to {@code 0.0}. + * + * @param impliedVolatility Mid/market implied volatility + * @param delta Option delta + * @param gamma Option gamma + * @param theta Option theta + * @param vega Option vega + * @param isValid Whether the calculation is valid + */ + public Greek(double impliedVolatility, double delta, double gamma, double theta, double vega, boolean isValid) { + this(impliedVolatility, delta, gamma, theta, vega, 0.0D, 0.0D, isValid); + } + + @Override + public String toString() { + return "Greek{" + + "impliedVolatility=" + impliedVolatility + + ", delta=" + delta + + ", gamma=" + gamma + + ", theta=" + theta + + ", vega=" + vega + + ", askImpliedVolatility=" + askImpliedVolatility + + ", bidImpliedVolatility=" + bidImpliedVolatility + + ", isValid=" + isValid + + '}'; + } +} diff --git a/src/intrinio/realtime/composite/GreekClient.java b/src/intrinio/realtime/composite/GreekClient.java new file mode 100644 index 0000000..090ac30 --- /dev/null +++ b/src/intrinio/realtime/composite/GreekClient.java @@ -0,0 +1,937 @@ +package intrinio.realtime.composite; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.EnumSet; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Calculates realtime option Greeks from a stream of equities and options trades/quotes, + * combined with REST-fetched risk-free rates and dividend yields. + *+ * This client is intentionally non-transactional: cache reads and writes + * use concurrent maps and dirty timestamp checks (see {@link CurrentDataCache}). Under load, + * a Greek may be computed from a mix of slightly stale and fresh fields. That matches the + * C# SDK design and prioritizes throughput over strict snapshot consistency. + *
+ *+ * Wire this client into your equities/options WebSocket handlers by calling + * {@link #onEquityTrade}, {@link #onEquityQuote}, {@link #onOptionsTrade}, and + * {@link #onOptionsQuote}. When an external {@link DataCache} is supplied, market data + * is expected to be written to that cache by the caller (or a parallel handler); this + * client still tracks seen tickers for dividend refresh. When no cache is supplied, + * this client owns an internal cache and writes trades/quotes into it. + *
+ *+ * REST calls use {@link HttpURLConnection} and Gson only (no additional dependencies). + *
+ */ +public class GreekClient { + + //region Constants + + /** Supplemental datum key for trailing dividend yield on a security. */ + public static final String DIVIDEND_YIELD_KEY_NAME = "DividendYield"; + + /** Top-level supplemental datum key for the risk-free interest rate. */ + public static final String RISK_FREE_INTEREST_RATE_KEY_NAME = "RiskFreeInterestRate"; + + /** Greek cache key used by the built-in Black–Scholes calculator. */ + public static final String BLACK_SCHOLES_KEY_NAME = "IntrinioBlackScholes"; + + /** Intrinio API v2 base URL. */ + private static final String API_BASE = "https://api-v2.intrinio.com"; + + //endregion Constants + + //region Data Members + + /** Shared (or self-owned) non-transactional market-data cache. */ + private final DataCache cache; + + /** Named Greek calculators registered via {@link #tryAddOrUpdateGreekCalculation}. */ + private final ConcurrentHashMap+ * No locking: calculators read volatile/concurrent cache state and may observe torn snapshots. + *
+ */ + private void updateGreeks(OptionsContractData optionsContractData, DataCache dataCache, SecurityData securityData) { + if (optionsContractData == null || securityData == null || dataCache == null) { + return; + } + for (CalculateNewGreek calculateNewGreek : calcLookup.values()) { + try { + calculateNewGreek.calculateNewGreek(optionsContractData, securityData, dataCache); + } catch (Exception e) { + log("Error in CalculateNewGreek: " + e.getMessage()); + } + } + } + + /** + * Built-in Black–Scholes path for quote-driven feeds (OPRA, etc.). + * Uses equity last trade, option quote mid, ask, and bid. + */ + private void blackScholesCalc(OptionsContractData optionsContractData, SecurityData securityData, DataCache dataCache) { + Double riskFreeInterestRate = dataCache.getSupplementaryDatum(RISK_FREE_INTEREST_RATE_KEY_NAME); + Double dividendYield = securityData.getSupplementaryDatum(DIVIDEND_YIELD_KEY_NAME); + intrinio.realtime.equities.Trade equitiesTrade = securityData.getLatestEquitiesTrade(); + intrinio.realtime.options.Quote optionsQuote = optionsContractData.getLatestQuote(); + + if (riskFreeInterestRate == null + || dividendYield == null + || equitiesTrade == null + || optionsQuote == null + || optionsQuote.askPrice() <= 0D + || optionsQuote.bidPrice() <= 0D) { + return; + } + + double mid = (optionsQuote.askPrice() + optionsQuote.bidPrice()) / 2.0D; + Greek result = BlackScholesGreekCalculator.calculate( + riskFreeInterestRate, + dividendYield, + equitiesTrade.price(), + optionsQuote.timestamp(), + mid, + optionsQuote.askPrice(), + optionsQuote.bidPrice(), + optionsQuote.isPut(), + optionsQuote.getStrikePrice(), + optionsQuote.getExpirationDate()); + + if (result.isValid()) { + dataCache.setOptionGreekData( + securityData.getTickerSymbol(), + optionsContractData.getContract(), + BLACK_SCHOLES_KEY_NAME, + result, + updateFuncGreek); + } + } + + /** + * Built-in Black–Scholes path for trade-driven feeds (Options Edge). + * Uses equity last trade and option last trade price as market price. + */ + private void blackScholesCalcOptionsEdge(OptionsContractData optionsContractData, SecurityData securityData, DataCache dataCache) { + Double riskFreeInterestRate = dataCache.getSupplementaryDatum(RISK_FREE_INTEREST_RATE_KEY_NAME); + Double dividendYield = securityData.getSupplementaryDatum(DIVIDEND_YIELD_KEY_NAME); + intrinio.realtime.equities.Trade equitiesTrade = securityData.getLatestEquitiesTrade(); + intrinio.realtime.options.Trade optionsTrade = optionsContractData.getLatestTrade(); + + if (riskFreeInterestRate == null + || dividendYield == null + || equitiesTrade == null + || equitiesTrade.price() <= 0.0D + || optionsTrade == null + || optionsTrade.price() <= 0D) { + return; + } + + Greek result = BlackScholesGreekCalculator.calculate( + riskFreeInterestRate, + dividendYield, + equitiesTrade.price(), + optionsTrade.timestamp(), + optionsTrade.price(), + optionsTrade.price(), + optionsTrade.price(), + optionsTrade.isPut(), + optionsTrade.getStrikePrice(), + optionsTrade.getExpirationDate()); + + if (result.isValid()) { + dataCache.setOptionGreekData( + securityData.getTickerSymbol(), + optionsContractData.getContract(), + BLACK_SCHOLES_KEY_NAME, + result, + updateFuncGreek); + } + } + + //endregion Private Greek Updates +} diff --git a/src/intrinio/realtime/composite/GreekDataUpdate.java b/src/intrinio/realtime/composite/GreekDataUpdate.java index 14c8b77..279351b 100644 --- a/src/intrinio/realtime/composite/GreekDataUpdate.java +++ b/src/intrinio/realtime/composite/GreekDataUpdate.java @@ -1,9 +1,17 @@ package intrinio.realtime.composite; /** - * The function used to update the Greek value in the cache. + * The function used to merge a Greek value into the cache for a given key. + * Invoked atomically per key by concurrent map update logic. */ @FunctionalInterface public interface GreekDataUpdate { + + /** + * @param key Greek series key + * @param oldValue previously stored value (may be {@code null}) + * @param newValue incoming value (may be {@code null}) + * @return value to store; {@code null} removes the mapping + */ Greek greekDataUpdate(String key, Greek oldValue, Greek newValue); } diff --git a/src/intrinio/realtime/composite/GreekUpdateFrequency.java b/src/intrinio/realtime/composite/GreekUpdateFrequency.java index fd55075..1c5f674 100644 --- a/src/intrinio/realtime/composite/GreekUpdateFrequency.java +++ b/src/intrinio/realtime/composite/GreekUpdateFrequency.java @@ -2,32 +2,64 @@ import java.util.EnumSet; +/** + * Bit-flag style enumeration controlling when {@link GreekClient} recalculates Greeks. + *+ * Combine values with {@link EnumSet} (or {@link #combine(EnumSet)}) and pass them to + * {@link GreekClient}. Multiple flags may be set so that Greeks recompute on several event types. + *
+ */ public enum GreekUpdateFrequency { + /** Recalculate on every options trade that updates the cache. */ EVERY_OPTIONS_TRADE_UPDATE(1), + /** Recalculate on every options quote that updates the cache. */ EVERY_OPTIONS_QUOTE_UPDATE(2), + /** Recalculate all contracts when the top-level risk-free rate supplemental datum changes. */ EVERY_RISK_FREE_INTEREST_RATE_UPDATE(4), + /** Recalculate a security's contracts when its dividend-yield supplemental datum changes. */ EVERY_DIVIDEND_YIELD_UPDATE(8), + /** Recalculate a security's contracts on every equities trade update. */ EVERY_EQUITY_TRADE_UPDATE(16), + /** Recalculate a security's contracts on every equities quote update. */ EVERY_EQUITY_QUOTE_UPDATE(32); + /** Power-of-two flag value (mirrors the C# {@code [Flags]} enum). */ private final int value; GreekUpdateFrequency(int value) { this.value = value; } + /** + * @return integer flag bit for this frequency + */ public int getValue() { return value; } + /** + * Combines an enum set into a single bitmask integer. + * + * @param set frequencies to combine + * @return bitwise OR of all flag values + */ public static int combine(EnumSet+ * Updates are non-transactional dirty sets unless noted. + *
*/ public interface OptionsContractData { + + /** + * @return option contract identifier (e.g. {@code AAPL__240119C00150000}) + */ String getContract(); + /** @return latest options trade, or {@code null} */ Trade getLatestTrade(); + + /** @return latest options quote, or {@code null} */ Quote getLatestQuote(); + + /** @return latest options refresh, or {@code null} */ Refresh getLatestRefresh(); + + /** @return latest unusual activity, or {@code null} */ UnusualActivity getLatestUnusualActivity(); + /** + * Dirty-set trade when {@code trade} is non-null and newer by timestamp. + */ boolean setTrade(Trade trade); - boolean setTrade(Trade trade, OnOptionsTradeUpdated onOptionsTradeUpdated, SecurityData securityData, DataCache dataCache); + + /** + * Dirty-set trade and invoke callback on success. + */ + boolean setTrade(Trade trade, + OnOptionsTradeUpdated onOptionsTradeUpdated, + SecurityData securityData, + DataCache dataCache); + + /** + * Dirty-set quote when {@code quote} is non-null and newer by timestamp. + */ boolean setQuote(Quote quote); - boolean setQuote(Quote quote, OnOptionsQuoteUpdated onOptionsQuoteUpdated, SecurityData securityData, DataCache dataCache); + + boolean setQuote(Quote quote, + OnOptionsQuoteUpdated onOptionsQuoteUpdated, + SecurityData securityData, + DataCache dataCache); + + /** + * Overwrite latest refresh (no timestamp comparison). + */ boolean setRefresh(Refresh refresh); - boolean setRefresh(Refresh refresh, OnOptionsRefreshUpdated onOptionsRefreshUpdated, SecurityData securityData, DataCache dataCache); + + boolean setRefresh(Refresh refresh, + OnOptionsRefreshUpdated onOptionsRefreshUpdated, + SecurityData securityData, + DataCache dataCache); + + /** + * Overwrite latest unusual activity (no timestamp comparison). + */ boolean setUnusualActivity(UnusualActivity unusualActivity); - boolean setUnusualActivity(UnusualActivity unusualActivity, OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, SecurityData securityData, DataCache dataCache); + + boolean setUnusualActivity(UnusualActivity unusualActivity, + OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, + SecurityData securityData, + DataCache dataCache); Double getSupplementaryDatum(String key); + boolean setSupplementaryDatum(String key, Double datum, SupplementalDatumUpdate update); - boolean setSupplementaryDatum(String key, Double datum, OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, SecurityData securityData, DataCache dataCache, SupplementalDatumUpdate update); + + boolean setSupplementaryDatum(String key, + Double datum, + OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, + SecurityData securityData, + DataCache dataCache, + SupplementalDatumUpdate update); + + /** + * @return live unmodifiable view of contract-level supplemental data + */ Map+ * Updates are non-transactional dirty sets (typically by event timestamp) and are safe + * for concurrent callers, but do not provide a consistent multi-field snapshot. + *
+ */ public interface SecurityData { + + /** + * @return equity ticker symbol for this cache entry + */ String getTickerSymbol(); + /** @return latest equities trade, or {@code null} */ intrinio.realtime.equities.Trade getLatestEquitiesTrade(); + + /** @return latest equities ask quote, or {@code null} */ intrinio.realtime.equities.Quote getLatestEquitiesAskQuote(); + + /** @return latest equities bid quote, or {@code null} */ intrinio.realtime.equities.Quote getLatestEquitiesBidQuote(); + /** + * @param key supplemental datum key + * @return value or {@code null} + */ Double getSupplementaryDatum(String key); + /** + * Atomically merge a security-level supplemental value. + * + * @param key datum key + * @param datum new value + * @param update merge function + * @return {@code true} if the stored value equals {@code datum} after merge + */ boolean setSupplementaryDatum(String key, Double datum, SupplementalDatumUpdate update); - boolean setSupplementaryDatum(String key, Double datum, OnSecuritySupplementalDatumUpdated onSecuritySupplementalDatumUpdated, DataCache dataCache, SupplementalDatumUpdate update); + /** + * Merge a security-level supplemental value and invoke a callback on success. + */ + boolean setSupplementaryDatum(String key, + Double datum, + OnSecuritySupplementalDatumUpdated onSecuritySupplementalDatumUpdated, + DataCache dataCache, + SupplementalDatumUpdate update); + + /** + * @return live unmodifiable view of security-level supplemental data + */ Map