From de54d07f54a42cdaacdab21d955db11671544db0 Mon Sep 17 00:00:00 2001 From: Shawn Snyder Date: Tue, 4 Aug 2026 13:36:45 -0500 Subject: [PATCH 1/2] WIP on master --- README.md | 4 + src/SampleApp/GreekSampleApp.java | 251 +++++ src/SampleApp/SampleApp.java | 1 + .../BlackScholesGreekCalculator.java | 255 ++++- .../realtime/composite/CalculateNewGreek.java | 17 + .../realtime/composite/CurrentDataCache.java | 195 +++- .../composite/CurrentOptionsContractData.java | 109 +- .../composite/CurrentSecurityData.java | 198 ++-- .../realtime/composite/DataCache.java | 225 ++++- .../realtime/composite/DataCacheFactory.java | 18 +- src/intrinio/realtime/composite/Greek.java | 58 +- .../realtime/composite/GreekClient.java | 937 ++++++++++++++++++ .../realtime/composite/GreekDataUpdate.java | 10 +- .../composite/GreekUpdateFrequency.java | 34 +- .../composite/OnEquitiesQuoteUpdated.java | 9 + .../composite/OnEquitiesTradeUpdated.java | 9 + .../OnOptionsContractGreekDataUpdated.java | 17 +- ...tionsContractSupplementalDatumUpdated.java | 17 +- .../composite/OnOptionsQuoteUpdated.java | 15 +- .../composite/OnOptionsRefreshUpdated.java | 15 +- .../composite/OnOptionsTradeUpdated.java | 15 +- .../OnOptionsUnusualActivityUpdated.java | 15 +- .../OnSecuritySupplementalDatumUpdated.java | 10 + .../composite/OnSupplementalDatumUpdated.java | 9 + .../composite/OptionsContractData.java | 91 +- .../realtime/composite/SecurityData.java | 119 ++- .../composite/SupplementalDatumUpdate.java | 10 +- src/intrinio/realtime/options/Config.java | 13 +- 28 files changed, 2406 insertions(+), 270 deletions(-) create mode 100644 src/SampleApp/GreekSampleApp.java create mode 100644 src/intrinio/realtime/composite/GreekClient.java diff --git a/README.md b/README.md index 6294f2f..f3bfad3 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,10 @@ For a sample Java project see: [intrinio-realtime-java-sdk](https://github.com/i ## Options and Equities concurrently Example Usage * See [Composite Sample Websocket](https://github.com/intrinio/intrinio-realtime-java-sdk/blob/master/src/SampleApp/CompositeSampleApp.java) and [Sample Websocket](https://github.com/intrinio/intrinio-realtime-java-sdk/blob/master/src/SampleApp/SampleApp.java) +## Realtime Greeks Example Usage +* See [Greek Sample App](https://github.com/intrinio/intrinio-realtime-java-sdk/blob/master/src/SampleApp/GreekSampleApp.java) and [Sample Websocket](https://github.com/intrinio/intrinio-realtime-java-sdk/blob/master/src/SampleApp/SampleApp.java) +* Uses `intrinio.realtime.composite.DataCache` + `GreekClient` with Black–Scholes calculation (mirrors the C# composite Greek client). Wire equities and options trades/quotes into the shared cache and `GreekClient` handlers; enable via `GreekSampleApp.run(args)` in `SampleApp.java`. + ## Handling Events There are thousands of securities and millions of options contracts, each with their own feed of activity. We highly encourage you to make your trade and quote handlers has short as possible and follow a queue pattern so your app can handle the volume of activity. diff --git a/src/SampleApp/GreekSampleApp.java b/src/SampleApp/GreekSampleApp.java new file mode 100644 index 0000000..fa2466e --- /dev/null +++ b/src/SampleApp/GreekSampleApp.java @@ -0,0 +1,251 @@ +package SampleApp; + +import intrinio.realtime.composite.DataCache; +import intrinio.realtime.composite.DataCacheFactory; +import intrinio.realtime.composite.Greek; +import intrinio.realtime.composite.GreekClient; +import intrinio.realtime.composite.GreekUpdateFrequency; +import intrinio.realtime.composite.OptionsContractData; +import intrinio.realtime.composite.SecurityData; + +import java.time.LocalDateTime; +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.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Sample application that mirrors the C# {@code GreekSampleApp}: + * streams equities and options into a shared {@link DataCache}, runs {@link GreekClient} + * for Black–Scholes Greeks, and periodically logs socket / Greek statistics. + *

+ * 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 seenGreekTickers = new ConcurrentHashMap<>(); + + private static intrinio.realtime.options.Client optionsClient; + private static intrinio.realtime.options.Config optionsConfig; + private static final AtomicLong optionsTradeEventCount = new AtomicLong(0L); + private static final AtomicLong optionsQuoteEventCount = new AtomicLong(0L); + private static final AtomicLong greekUpdatedEventCount = new AtomicLong(0L); + + private static intrinio.realtime.equities.Client equitiesClient; + private static intrinio.realtime.equities.Config equitiesConfig; + private static final AtomicLong equitiesTradeEventCount = new AtomicLong(0L); + private static final AtomicLong equitiesQuoteEventCount = new AtomicLong(0L); + private static final AtomicBoolean stopped = new AtomicBoolean(false); + + private static void onOptionsQuote(intrinio.realtime.options.Quote quote) { + optionsQuoteEventCount.incrementAndGet(); + } + + private static void onOptionsTrade(intrinio.realtime.options.Trade trade) { + optionsTradeEventCount.incrementAndGet(); + } + + private static void onEquitiesQuote(intrinio.realtime.equities.Quote quote) { + equitiesQuoteEventCount.incrementAndGet(); + } + + private static void onEquitiesTrade(intrinio.realtime.equities.Trade trade) { + equitiesTradeEventCount.incrementAndGet(); + } + + /** + * Invoked by {@link GreekClient} / the data cache when a contract's Greek value is updated. + */ + private static void onGreek(String key, + Greek datum, + OptionsContractData optionsContractData, + SecurityData securityData, + DataCache cache) { + greekUpdatedEventCount.incrementAndGet(); + // Log("Greek: " + optionsContractData.getContract() + "\t\t" + key + "\t\t" + (datum != null ? datum.toString() : "")); + if (securityData != null && optionsContractData != null) { + seenGreekTickers.putIfAbsent(securityData.getTickerSymbol(), optionsContractData.getContract()); + } + } + + private static void timerCallback() { + try { + if (optionsClient != null) { + Log("Options Socket Stats - " + optionsClient.getStats() + + ", App trades: " + optionsTradeEventCount.get() + + ", App quotes: " + optionsQuoteEventCount.get()); + } + if (equitiesClient != null) { + Log("Equities Socket Stats - " + equitiesClient.getStats() + + ", App trades: " + equitiesTradeEventCount.get() + + ", App quotes: " + equitiesQuoteEventCount.get()); + } + + Log("Greek updates: " + greekUpdatedEventCount.get()); + Log("Data Cache Security Count: " + dataCache.getAllSecurityData().size()); + + long dividendYieldCount = 0L; + for (Map.Entry entry : dataCache.getAllSecurityData().entrySet()) { + if (entry.getValue() != null + && entry.getValue().getSupplementaryDatum(GreekClient.DIVIDEND_YIELD_KEY_NAME) != null) { + dividendYieldCount++; + } + } + Log("Dividend Yield Count: " + dividendYieldCount); + Log("Unique Securities with Greeks Count: " + seenGreekTickers.size()); + } catch (Exception e) { + Log("Error in timer callback: " + e.getMessage()); + } + } + + private static void shutdown() { + Log("Stopping sample app"); + try { + if (timer != null) { + timer.cancel(); + } + } catch (Exception ignored) { + } + try { + if (optionsClient != null) { + optionsClient.leave(); + optionsClient.stop(); + } + } catch (Exception ignored) { + } + try { + if (equitiesClient != null) { + equitiesClient.leave(); + equitiesClient.stop(); + } + } catch (Exception ignored) { + } + try { + if (greekClient != null) { + greekClient.stop(); + } + } catch (Exception ignored) { + } + stopped.set(true); + } + + private static void Log(String message) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + System.out.println(dtf.format(LocalDateTime.now()) + " " + message); + } + + /** + * Entry point used by {@link SampleApp}. + * + * @param args unused + */ + public static void run(String[] args) { + Log("Starting Greek sample app"); + + dataCache = DataCacheFactory.create(); + + EnumSet updateFrequency = EnumSet.of( + GreekUpdateFrequency.EVERY_DIVIDEND_YIELD_UPDATE, + GreekUpdateFrequency.EVERY_RISK_FREE_INTEREST_RATE_UPDATE, + GreekUpdateFrequency.EVERY_OPTIONS_TRADE_UPDATE, + GreekUpdateFrequency.EVERY_EQUITY_TRADE_UPDATE); + + // You can either automatically load config.json by doing nothing, or you can specify your own config and pass it in. + // optionsConfig = new intrinio.realtime.options.Config("API_KEY_HERE", intrinio.realtime.options.Provider.OPRA, null, new String[]{}, 8, false); + optionsConfig = intrinio.realtime.options.Config.load(); + if (optionsConfig == null) { + Log("Failed to load options config from intrinio/config.json"); + return; + } + + greekClient = new GreekClient(updateFrequency, GreekSampleApp::onGreek, optionsConfig.getOptionsApiKey(), dataCache); + greekClient.addBlackScholes(optionsConfig.getOptionsProvider()); + // greekClient.tryAddOrUpdateGreekCalculation("MyGreekCalculation", MyCalculateNewGreekDelegate); + // Hint: Use dataCache.setOptionSupplementalDatum inside your delegate to save values. + greekClient.start(); + + optionsClient = new intrinio.realtime.options.Client(optionsConfig); + // Fan-out: app counters, data cache (drives Greek callbacks), and GreekClient ticker tracking + optionsClient.setOnTrade(trade -> { + onOptionsTrade(trade); + dataCache.setOptionsTrade(trade); + greekClient.onOptionsTrade(trade); + }); + optionsClient.setOnQuote(quote -> { + onOptionsQuote(quote); + dataCache.setOptionsQuote(quote); + greekClient.onOptionsQuote(quote); + }); + + try { + optionsClient.start(); + optionsClient.join(); + // optionsClient.joinLobby(); // Firehose + // optionsClient.join(new String[] { "AAPL", "GOOG", "MSFT" }); // Specify symbols at runtime + } catch (Exception e) { + Log("Error starting options client: " + e.getMessage()); + e.printStackTrace(); + return; + } + + // equitiesConfig = new intrinio.realtime.equities.Config("API_KEY_HERE", intrinio.realtime.equities.Provider.NASDAQ_BASIC, null, new String[]{}, false, 4, false); + equitiesConfig = intrinio.realtime.equities.Config.load(); + if (equitiesConfig == null) { + Log("Failed to load equities config from intrinio/config.json"); + return; + } + + equitiesClient = new intrinio.realtime.equities.Client( + trade -> { + onEquitiesTrade(trade); + dataCache.setEquityTrade(trade); + greekClient.onEquityTrade(trade); + }, + quote -> { + onEquitiesQuote(quote); + dataCache.setEquityQuote(quote); + greekClient.onEquityQuote(quote); + }, + equitiesConfig); + + try { + equitiesClient.start(); + equitiesClient.join(); + // equitiesClient.joinLobby(); // Firehose + // equitiesClient.join(new String[] { "AAPL", "GOOG", "MSFT" }); // Specify symbols at runtime + } catch (Exception e) { + Log("Error starting equities client: " + e.getMessage()); + e.printStackTrace(); + return; + } + + Runtime.getRuntime().addShutdownHook(new Thread(GreekSampleApp::shutdown)); + + timer = new Timer("GreekSampleApp-stats", true); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + timerCallback(); + } + }, 60_000L, 60_000L); + + // Keep the main thread alive until shutdown + while (!stopped.get()) { + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } +} diff --git a/src/SampleApp/SampleApp.java b/src/SampleApp/SampleApp.java index 3d3db68..3aa78c8 100644 --- a/src/SampleApp/SampleApp.java +++ b/src/SampleApp/SampleApp.java @@ -6,5 +6,6 @@ public static void main(String[] args) EquitiesSampleApp.run(args); //OptionsSampleApp.run(args); //CompositeSampleApp.run(args); + //GreekSampleApp.run(args); } } diff --git a/src/intrinio/realtime/composite/BlackScholesGreekCalculator.java b/src/intrinio/realtime/composite/BlackScholesGreekCalculator.java index 726a8ca..37a790a 100644 --- a/src/intrinio/realtime/composite/BlackScholesGreekCalculator.java +++ b/src/intrinio/realtime/composite/BlackScholesGreekCalculator.java @@ -1,27 +1,85 @@ package intrinio.realtime.composite; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.Date; -public class BlackScholesGreekCalculator { +/** + * Static Black–Scholes–Merton Greek calculator used by {@link GreekClient}. + *

+ * 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 securities = new ConcurrentHashMap<>(); + + /** Unmodifiable live view of {@link #securities}. */ private final Map readonlySecurities = Collections.unmodifiableMap(securities); + + /** Concurrent map of top-level supplemental numeric data. */ private final ConcurrentHashMap supplementaryData = new ConcurrentHashMap<>(); + + /** Unmodifiable live view of {@link #supplementaryData}. */ private final Map readonlySupplementaryData = Collections.unmodifiableMap(supplementaryData); - private OnSupplementalDatumUpdated supplementalDatumUpdatedCallback; - private OnSecuritySupplementalDatumUpdated securitySupplementalDatumUpdatedCallback; - private OnOptionsContractSupplementalDatumUpdated optionsContractSupplementalDatumUpdatedCallback; + /** Optional callback for top-level supplemental updates. */ + private volatile OnSupplementalDatumUpdated supplementalDatumUpdatedCallback; + + /** Optional callback for security-level supplemental updates. */ + private volatile OnSecuritySupplementalDatumUpdated securitySupplementalDatumUpdatedCallback; + + /** Optional callback for option-contract supplemental updates. */ + private volatile OnOptionsContractSupplementalDatumUpdated optionsContractSupplementalDatumUpdatedCallback; - private OnOptionsContractGreekDataUpdated optionsContractGreekDataUpdatedCallback; + /** Optional callback for option Greek updates. */ + private volatile OnOptionsContractGreekDataUpdated optionsContractGreekDataUpdatedCallback; - private OnEquitiesTradeUpdated equitiesTradeUpdatedCallback; - private OnEquitiesQuoteUpdated equitiesQuoteUpdatedCallback; + /** Optional callback for equities trade updates. */ + private volatile OnEquitiesTradeUpdated equitiesTradeUpdatedCallback; - private OnOptionsTradeUpdated optionsTradeUpdatedCallback; - private OnOptionsQuoteUpdated optionsQuoteUpdatedCallback; - private OnOptionsRefreshUpdated optionsRefreshUpdatedCallback; - private OnOptionsUnusualActivityUpdated optionsUnusualActivityUpdatedCallback; + /** Optional callback for equities quote updates. */ + private volatile OnEquitiesQuoteUpdated equitiesQuoteUpdatedCallback; - public CurrentDataCache() { + /** Optional callback for options trade updates. */ + private volatile OnOptionsTradeUpdated optionsTradeUpdatedCallback; + + /** Optional callback for options quote updates. */ + private volatile OnOptionsQuoteUpdated optionsQuoteUpdatedCallback; + + /** Optional callback for options refresh updates. */ + private volatile OnOptionsRefreshUpdated optionsRefreshUpdatedCallback; + + /** Optional callback for options unusual-activity updates. */ + private volatile OnOptionsUnusualActivityUpdated optionsUnusualActivityUpdatedCallback; + + CurrentDataCache() { } + //region Supplementary Data + + @Override public Double getSupplementaryDatum(String key) { return supplementaryData.getOrDefault(key, null); } + @Override public boolean setSupplementaryDatum(String key, Double datum, SupplementalDatumUpdate update) { + // compute is atomic per key; returning null from update removes the mapping Double newValue = supplementaryData.compute(key, (k, oldValue) -> update.supplementalDatumUpdate(k, oldValue, datum)); boolean result = java.util.Objects.equals(datum, newValue); if (result && supplementalDatumUpdatedCallback != null) { try { supplementalDatumUpdatedCallback.onSupplementalDatumUpdated(key, datum, this); } catch (Exception e) { - Log("Error in OnSupplementalDatumUpdated Callback: " + e.getMessage()); + log("Error in OnSupplementalDatumUpdated Callback: " + e.getMessage()); } } return result; } + @Override public Map getAllSupplementaryData() { return readonlySupplementaryData; } + @Override public Double getSecuritySupplementalDatum(String tickerSymbol, String key) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getSupplementaryDatum(key) : null; } + @Override public boolean setSecuritySupplementalDatum(String tickerSymbol, String key, Double datum, SupplementalDatumUpdate update) { if (tickerSymbol != null && !tickerSymbol.trim().isEmpty()) { - SecurityData securityData = securities.computeIfAbsent(tickerSymbol, k -> new CurrentSecurityData(tickerSymbol, null, null, null)); + // Get-or-create security without locking the whole map + SecurityData securityData = securities.computeIfAbsent( + tickerSymbol, k -> new CurrentSecurityData(tickerSymbol, null, null, null)); return securityData.setSupplementaryDatum(key, datum, securitySupplementalDatumUpdatedCallback, this, update); } return false; } + @Override public Double getOptionsContractSupplementalDatum(String tickerSymbol, String contract, String key) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getOptionsContractSupplementalDatum(contract, key) : null; } + @Override public boolean setOptionSupplementalDatum(String tickerSymbol, String contract, String key, Double datum, SupplementalDatumUpdate update) { if (tickerSymbol != null && !tickerSymbol.trim().isEmpty()) { - SecurityData securityData = securities.computeIfAbsent(tickerSymbol, k -> new CurrentSecurityData(tickerSymbol, null, null, null)); - return securityData.setOptionsContractSupplementalDatum(contract, key, datum, optionsContractSupplementalDatumUpdatedCallback, this, update); + SecurityData securityData = securities.computeIfAbsent( + tickerSymbol, k -> new CurrentSecurityData(tickerSymbol, null, null, null)); + return securityData.setOptionsContractSupplementalDatum( + contract, key, datum, optionsContractSupplementalDatumUpdatedCallback, this, update); } return false; } + //endregion Supplementary Data + + //region Greeks + + @Override public Greek getOptionsContractGreekData(String tickerSymbol, String contract, String key) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getOptionsContractGreekData(contract, key) : null; } + @Override public boolean setOptionGreekData(String tickerSymbol, String contract, String key, Greek data, GreekDataUpdate update) { if (tickerSymbol != null && !tickerSymbol.trim().isEmpty()) { - SecurityData securityData = securities.computeIfAbsent(tickerSymbol, k -> new CurrentSecurityData(tickerSymbol, null, null, null)); - return securityData.setOptionsContractGreekData(contract, key, data, optionsContractGreekDataUpdatedCallback, this, update); + SecurityData securityData = securities.computeIfAbsent( + tickerSymbol, k -> new CurrentSecurityData(tickerSymbol, null, null, null)); + return securityData.setOptionsContractGreekData( + contract, key, data, optionsContractGreekDataUpdatedCallback, this, update); } return false; } + //endregion Greeks + + //region Sub-caches + + @Override public SecurityData getSecurityData(String tickerSymbol) { return securities.get(tickerSymbol); } + @Override public Map getAllSecurityData() { return readonlySecurities; } + @Override public OptionsContractData getOptionsContractData(String tickerSymbol, String contract) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getOptionsContractData(contract) : null; } + @Override public Map getAllOptionsContractData(String tickerSymbol) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getAllOptionsContractData() : Collections.emptyMap(); } + //endregion Sub-caches + + //region Equities + + @Override public intrinio.realtime.equities.Trade getLatestEquityTrade(String tickerSymbol) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getLatestEquitiesTrade() : null; } + @Override public boolean setEquityTrade(intrinio.realtime.equities.Trade trade) { if (trade != null) { String symbol = trade.symbol(); - SecurityData securityData = securities.computeIfAbsent(symbol, k -> new CurrentSecurityData(symbol, trade, null, null)); + SecurityData securityData = securities.computeIfAbsent( + symbol, k -> new CurrentSecurityData(symbol, trade, null, null)); return securityData.setEquitiesTrade(trade, equitiesTradeUpdatedCallback, this); } return false; } + /** + * Convenience handler for equities trade callbacks / plug-in style wiring. + */ public void onTrade(intrinio.realtime.equities.Trade trade) { setEquityTrade(trade); } + @Override public intrinio.realtime.equities.Quote getLatestEquityAskQuote(String tickerSymbol) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getLatestEquitiesAskQuote() : null; } + @Override public intrinio.realtime.equities.Quote getLatestEquityBidQuote(String tickerSymbol) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getLatestEquitiesBidQuote() : null; } + @Override public boolean setEquityQuote(intrinio.realtime.equities.Quote quote) { if (quote != null) { String symbol = quote.symbol(); - SecurityData securityData = securities.computeIfAbsent(symbol, k -> new CurrentSecurityData(symbol, null, quote.type() == intrinio.realtime.equities.QuoteType.ASK ? quote : null, quote.type() == intrinio.realtime.equities.QuoteType.BID ? quote : null)); + SecurityData securityData = securities.computeIfAbsent( + symbol, + k -> new CurrentSecurityData( + symbol, + null, + quote.type() == intrinio.realtime.equities.QuoteType.ASK ? quote : null, + quote.type() == intrinio.realtime.equities.QuoteType.BID ? quote : null)); return securityData.setEquitiesQuote(quote, equitiesQuoteUpdatedCallback, this); } return false; } + /** + * Convenience handler for equities quote callbacks / plug-in style wiring. + */ public void onQuote(intrinio.realtime.equities.Quote quote) { setEquityQuote(quote); } + //endregion Equities + + //region Options + + @Override public intrinio.realtime.options.Trade getLatestOptionsTrade(String tickerSymbol, String contract) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getOptionsContractTrade(contract) : null; } + @Override public boolean setOptionsTrade(intrinio.realtime.options.Trade trade) { if (trade != null) { String underlyingSymbol = trade.getUnderlyingSymbol(); - SecurityData securityData = securities.computeIfAbsent(underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); + SecurityData securityData = securities.computeIfAbsent( + underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); return securityData.setOptionsContractTrade(trade, optionsTradeUpdatedCallback, this); } return false; } + /** + * Convenience handler for options trade callbacks / plug-in style wiring. + */ public void onTrade(intrinio.realtime.options.Trade trade) { setOptionsTrade(trade); } + @Override public intrinio.realtime.options.Quote getLatestOptionsQuote(String tickerSymbol, String contract) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getOptionsContractQuote(contract) : null; } + @Override public boolean setOptionsQuote(intrinio.realtime.options.Quote quote) { if (quote != null) { String underlyingSymbol = quote.getUnderlyingSymbol(); - SecurityData securityData = securities.computeIfAbsent(underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); + SecurityData securityData = securities.computeIfAbsent( + underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); return securityData.setOptionsContractQuote(quote, optionsQuoteUpdatedCallback, this); } return false; } + /** + * Convenience handler for options quote callbacks / plug-in style wiring. + */ public void onQuote(intrinio.realtime.options.Quote quote) { setOptionsQuote(quote); } + @Override public intrinio.realtime.options.Refresh getLatestOptionsRefresh(String tickerSymbol, String contract) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getOptionsContractRefresh(contract) : null; } + @Override public boolean setOptionsRefresh(intrinio.realtime.options.Refresh refresh) { if (refresh != null) { String underlyingSymbol = refresh.getUnderlyingSymbol(); - SecurityData securityData = securities.computeIfAbsent(underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); + SecurityData securityData = securities.computeIfAbsent( + underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); return securityData.setOptionsContractRefresh(refresh, optionsRefreshUpdatedCallback, this); } return false; } + /** + * Convenience handler for options refresh callbacks / plug-in style wiring. + */ public void onRefresh(intrinio.realtime.options.Refresh refresh) { setOptionsRefresh(refresh); } + @Override public intrinio.realtime.options.UnusualActivity getLatestOptionsUnusualActivity(String tickerSymbol, String contract) { SecurityData securityData = securities.get(tickerSymbol); return securityData != null ? securityData.getOptionsContractUnusualActivity(contract) : null; } + @Override public boolean setOptionsUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity) { if (unusualActivity != null) { String underlyingSymbol = unusualActivity.getUnderlyingSymbol(); - SecurityData securityData = securities.computeIfAbsent(underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); - return securityData.setOptionsContractUnusualActivity(unusualActivity, optionsUnusualActivityUpdatedCallback, this); + SecurityData securityData = securities.computeIfAbsent( + underlyingSymbol, k -> new CurrentSecurityData(underlyingSymbol, null, null, null)); + return securityData.setOptionsContractUnusualActivity( + unusualActivity, optionsUnusualActivityUpdatedCallback, this); } return false; } + /** + * Convenience handler for options unusual-activity callbacks / plug-in style wiring. + */ public void onUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity) { setOptionsUnusualActivity(unusualActivity); } + //endregion Options + + //region Callback accessors + + @Override public OnSupplementalDatumUpdated getSupplementalDatumUpdatedCallback() { return supplementalDatumUpdatedCallback; } + @Override public void setSupplementalDatumUpdatedCallback(OnSupplementalDatumUpdated supplementalDatumUpdatedCallback) { this.supplementalDatumUpdatedCallback = supplementalDatumUpdatedCallback; } + @Override public OnSecuritySupplementalDatumUpdated getSecuritySupplementalDatumUpdatedCallback() { return securitySupplementalDatumUpdatedCallback; } + @Override public void setSecuritySupplementalDatumUpdatedCallback(OnSecuritySupplementalDatumUpdated securitySupplementalDatumUpdatedCallback) { this.securitySupplementalDatumUpdatedCallback = securitySupplementalDatumUpdatedCallback; } + @Override public OnOptionsContractSupplementalDatumUpdated getOptionsContractSupplementalDatumUpdatedCallback() { return optionsContractSupplementalDatumUpdatedCallback; } + @Override public void setOptionsContractSupplementalDatumUpdatedCallback(OnOptionsContractSupplementalDatumUpdated optionsContractSupplementalDatumUpdatedCallback) { this.optionsContractSupplementalDatumUpdatedCallback = optionsContractSupplementalDatumUpdatedCallback; } + @Override public OnOptionsContractGreekDataUpdated getOptionsContractGreekDataUpdatedCallback() { return optionsContractGreekDataUpdatedCallback; } + @Override public void setOptionsContractGreekDataUpdatedCallback(OnOptionsContractGreekDataUpdated optionsContractGreekDataUpdatedCallback) { this.optionsContractGreekDataUpdatedCallback = optionsContractGreekDataUpdatedCallback; } + @Override public OnEquitiesTradeUpdated getEquitiesTradeUpdatedCallback() { return equitiesTradeUpdatedCallback; } + @Override public void setEquitiesTradeUpdatedCallback(OnEquitiesTradeUpdated equitiesTradeUpdatedCallback) { this.equitiesTradeUpdatedCallback = equitiesTradeUpdatedCallback; } + @Override public OnEquitiesQuoteUpdated getEquitiesQuoteUpdatedCallback() { return equitiesQuoteUpdatedCallback; } + @Override public void setEquitiesQuoteUpdatedCallback(OnEquitiesQuoteUpdated equitiesQuoteUpdatedCallback) { this.equitiesQuoteUpdatedCallback = equitiesQuoteUpdatedCallback; } + @Override public OnOptionsTradeUpdated getOptionsTradeUpdatedCallback() { return optionsTradeUpdatedCallback; } + @Override public void setOptionsTradeUpdatedCallback(OnOptionsTradeUpdated optionsTradeUpdatedCallback) { this.optionsTradeUpdatedCallback = optionsTradeUpdatedCallback; } + @Override public OnOptionsQuoteUpdated getOptionsQuoteUpdatedCallback() { return optionsQuoteUpdatedCallback; } + @Override public void setOptionsQuoteUpdatedCallback(OnOptionsQuoteUpdated optionsQuoteUpdatedCallback) { this.optionsQuoteUpdatedCallback = optionsQuoteUpdatedCallback; } + @Override public OnOptionsRefreshUpdated getOptionsRefreshUpdatedCallback() { return optionsRefreshUpdatedCallback; } + @Override public void setOptionsRefreshUpdatedCallback(OnOptionsRefreshUpdated optionsRefreshUpdatedCallback) { this.optionsRefreshUpdatedCallback = optionsRefreshUpdatedCallback; } + @Override public OnOptionsUnusualActivityUpdated getOptionsUnusualActivityUpdatedCallback() { return optionsUnusualActivityUpdatedCallback; } + @Override public void setOptionsUnusualActivityUpdatedCallback(OnOptionsUnusualActivityUpdated optionsUnusualActivityUpdatedCallback) { this.optionsUnusualActivityUpdatedCallback = optionsUnusualActivityUpdatedCallback; } - - private void Log(String message){ + + //endregion Callback accessors + + private void log(String message) { System.out.println(message); } -} \ No newline at end of file +} diff --git a/src/intrinio/realtime/composite/CurrentOptionsContractData.java b/src/intrinio/realtime/composite/CurrentOptionsContractData.java index bb93d2b..78f8a2e 100644 --- a/src/intrinio/realtime/composite/CurrentOptionsContractData.java +++ b/src/intrinio/realtime/composite/CurrentOptionsContractData.java @@ -1,33 +1,56 @@ package intrinio.realtime.composite; -import intrinio.realtime.options.Trade; import intrinio.realtime.options.Quote; import intrinio.realtime.options.Refresh; +import intrinio.realtime.options.Trade; import intrinio.realtime.options.UnusualActivity; -import intrinio.realtime.options.QuoteType; -import java.util.concurrent.ConcurrentHashMap; + import java.util.Collections; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** - * Not for Use yet. Subject to change. + * Default {@link OptionsContractData} implementation for a single option contract. + *

+ * 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 supplementaryData = new ConcurrentHashMap<>(); + + /** Unmodifiable live view of {@link #supplementaryData}. */ private final Map readonlySupplementaryData = Collections.unmodifiableMap(supplementaryData); + + /** Concurrent map of Greek series by name. */ private final ConcurrentHashMap greekData = new ConcurrentHashMap<>(); + + /** Unmodifiable live view of {@link #greekData}. */ private final Map readonlyGreekData = Collections.unmodifiableMap(greekData); - public CurrentOptionsContractData(String contract, - Trade latestTrade, - Quote latestQuote, - Refresh latestRefresh, - UnusualActivity latestUnusualActivity) { + CurrentOptionsContractData(String contract, + Trade latestTrade, + Quote latestQuote, + Refresh latestRefresh, + UnusualActivity latestUnusualActivity) { this.contract = contract; this.latestTrade = latestTrade; this.latestQuote = latestQuote; @@ -62,7 +85,7 @@ public UnusualActivity getLatestUnusualActivity() { @Override public boolean setTrade(Trade trade) { - //dirty set + // dirty set if (this.latestTrade == null || (trade != null && trade.timestamp() > this.latestTrade.timestamp())) { this.latestTrade = trade; return true; @@ -71,13 +94,16 @@ public boolean setTrade(Trade trade) { } @Override - public boolean setTrade(Trade trade, OnOptionsTradeUpdated onOptionsTradeUpdated, SecurityData securityData, DataCache dataCache) { + public boolean setTrade(Trade trade, + OnOptionsTradeUpdated onOptionsTradeUpdated, + SecurityData securityData, + DataCache dataCache) { boolean isSet = setTrade(trade); if (isSet && onOptionsTradeUpdated != null) { try { onOptionsTradeUpdated.onOptionsTradeUpdated(this, dataCache, securityData, trade); } catch (Exception e) { - Log("Error in OnOptionsTradeUpdated Callback: " + e.getMessage()); + log("Error in OnOptionsTradeUpdated Callback: " + e.getMessage()); } } return isSet; @@ -85,7 +111,7 @@ public boolean setTrade(Trade trade, OnOptionsTradeUpdated onOptionsTradeUpdated @Override public boolean setQuote(Quote quote) { - //dirty set + // dirty set if (this.latestQuote == null || (quote != null && quote.timestamp() > this.latestQuote.timestamp())) { this.latestQuote = quote; return true; @@ -94,13 +120,16 @@ public boolean setQuote(Quote quote) { } @Override - public boolean setQuote(Quote quote, OnOptionsQuoteUpdated onOptionsQuoteUpdated, SecurityData securityData, DataCache dataCache) { + public boolean setQuote(Quote quote, + OnOptionsQuoteUpdated onOptionsQuoteUpdated, + SecurityData securityData, + DataCache dataCache) { boolean isSet = this.setQuote(quote); if (isSet && onOptionsQuoteUpdated != null) { try { onOptionsQuoteUpdated.onOptionsQuoteUpdated(this, dataCache, securityData, quote); } catch (Exception e) { - Log("Error in onOptionsQuoteUpdated Callback: " + e.getMessage()); + log("Error in onOptionsQuoteUpdated Callback: " + e.getMessage()); } } return isSet; @@ -113,13 +142,16 @@ public boolean setRefresh(Refresh refresh) { } @Override - public boolean setRefresh(Refresh refresh, OnOptionsRefreshUpdated onOptionsRefreshUpdated, SecurityData securityData, DataCache dataCache) { + public boolean setRefresh(Refresh refresh, + OnOptionsRefreshUpdated onOptionsRefreshUpdated, + SecurityData securityData, + DataCache dataCache) { boolean isSet = this.setRefresh(refresh); if (isSet && onOptionsRefreshUpdated != null) { try { onOptionsRefreshUpdated.onOptionsRefreshUpdated(this, dataCache, securityData, refresh); } catch (Exception e) { - Log("Error in onOptionsRefreshUpdated Callback: " + e.getMessage()); + log("Error in onOptionsRefreshUpdated Callback: " + e.getMessage()); } } return isSet; @@ -132,13 +164,16 @@ public boolean setUnusualActivity(UnusualActivity unusualActivity) { } @Override - public boolean setUnusualActivity(UnusualActivity unusualActivity, OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, SecurityData securityData, DataCache dataCache) { + public boolean setUnusualActivity(UnusualActivity unusualActivity, + OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, + SecurityData securityData, + DataCache dataCache) { boolean isSet = this.setUnusualActivity(unusualActivity); if (isSet && onOptionsUnusualActivityUpdated != null) { try { onOptionsUnusualActivityUpdated.onOptionsUnusualActivityUpdated(this, dataCache, securityData, unusualActivity); } catch (Exception e) { - Log("Error in onOptionsUnusualActivityUpdated Callback: " + e.getMessage()); + log("Error in onOptionsUnusualActivityUpdated Callback: " + e.getMessage()); } } return isSet; @@ -156,13 +191,19 @@ public boolean setSupplementaryDatum(String key, Double datum, SupplementalDatum } @Override - public boolean setSupplementaryDatum(String key, Double datum, OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, SecurityData securityData, DataCache dataCache, SupplementalDatumUpdate update) { + public boolean setSupplementaryDatum(String key, + Double datum, + OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, + SecurityData securityData, + DataCache dataCache, + SupplementalDatumUpdate update) { boolean result = setSupplementaryDatum(key, datum, update); if (result && onOptionsContractSupplementalDatumUpdated != null) { try { - onOptionsContractSupplementalDatumUpdated.onOptionsContractSupplementalDatumUpdated(key, datum, this, securityData, dataCache); + onOptionsContractSupplementalDatumUpdated.onOptionsContractSupplementalDatumUpdated( + key, datum, this, securityData, dataCache); } catch (Exception e) { - Log("Error in onOptionsContractSupplementalDatumUpdated Callback: " + e.getMessage()); + log("Error in onOptionsContractSupplementalDatumUpdated Callback: " + e.getMessage()); } } return result; @@ -186,13 +227,19 @@ public boolean setGreekData(String key, Greek datum, GreekDataUpdate update) { } @Override - public boolean setGreekData(String key, Greek datum, OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, SecurityData securityData, DataCache dataCache, GreekDataUpdate update) { + public boolean setGreekData(String key, + Greek datum, + OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, + SecurityData securityData, + DataCache dataCache, + GreekDataUpdate update) { boolean result = setGreekData(key, datum, update); if (result && onOptionsContractGreekDataUpdated != null) { try { - onOptionsContractGreekDataUpdated.onOptionsContractGreekDataUpdated(key, datum, this, securityData, dataCache); + onOptionsContractGreekDataUpdated.onOptionsContractGreekDataUpdated( + key, datum, this, securityData, dataCache); } catch (Exception e) { - Log("Error in onOptionsContractGreekDataUpdated Callback: " + e.getMessage()); + log("Error in onOptionsContractGreekDataUpdated Callback: " + e.getMessage()); } } return result; @@ -203,7 +250,7 @@ public Map getAllGreekData() { return readonlyGreekData; } - private void Log(String message){ + private void log(String message) { System.out.println(message); } -} \ No newline at end of file +} diff --git a/src/intrinio/realtime/composite/CurrentSecurityData.java b/src/intrinio/realtime/composite/CurrentSecurityData.java index 687f141..30e7f8f 100644 --- a/src/intrinio/realtime/composite/CurrentSecurityData.java +++ b/src/intrinio/realtime/composite/CurrentSecurityData.java @@ -1,23 +1,46 @@ package intrinio.realtime.composite; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +/** + * Default {@link SecurityData} implementation for a single equity ticker. + *

+ * 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 contracts = new ConcurrentHashMap<>(); + + /** Unmodifiable live view of {@link #contracts}. */ private final Map readonlyContracts = Collections.unmodifiableMap(contracts); + + /** Concurrent map of security-level supplemental numerics. */ private final ConcurrentHashMap supplementaryData = new ConcurrentHashMap<>(); + + /** Unmodifiable live view of {@link #supplementaryData}. */ private final Map readonlySupplementaryData = Collections.unmodifiableMap(supplementaryData); - public CurrentSecurityData(String tickerSymbol, + CurrentSecurityData(String tickerSymbol, intrinio.realtime.equities.Trade latestTrade, intrinio.realtime.equities.Quote latestAskQuote, intrinio.realtime.equities.Quote latestBidQuote) { @@ -59,13 +82,17 @@ public boolean setSupplementaryDatum(String key, Double datum, SupplementalDatum } @Override - public boolean setSupplementaryDatum(String key, Double datum, OnSecuritySupplementalDatumUpdated onSecuritySupplementalDatumUpdated, DataCache dataCache, SupplementalDatumUpdate update) { + public boolean setSupplementaryDatum(String key, + Double datum, + OnSecuritySupplementalDatumUpdated onSecuritySupplementalDatumUpdated, + DataCache dataCache, + SupplementalDatumUpdate update) { boolean result = setSupplementaryDatum(key, datum, update); if (result && onSecuritySupplementalDatumUpdated != null) { try { onSecuritySupplementalDatumUpdated.onSecuritySupplementalDatumUpdated(key, datum, this, dataCache); } catch (Exception e) { - Log("Error in onSecuritySupplementalDatumUpdated Callback: " + e.getMessage()); + log("Error in onSecuritySupplementalDatumUpdated Callback: " + e.getMessage()); } } return result; @@ -78,7 +105,7 @@ public Map getAllSupplementaryData() { @Override public boolean setEquitiesTrade(intrinio.realtime.equities.Trade trade) { - //dirty set + // dirty set: accept only if no trade yet or incoming timestamp is strictly newer if (this.latestTrade == null || (trade != null && trade.timestamp() > this.latestTrade.timestamp())) { this.latestTrade = trade; return true; @@ -87,13 +114,15 @@ public boolean setEquitiesTrade(intrinio.realtime.equities.Trade trade) { } @Override - public boolean setEquitiesTrade(intrinio.realtime.equities.Trade trade, OnEquitiesTradeUpdated onEquitiesTradeUpdated, DataCache dataCache) { + public boolean setEquitiesTrade(intrinio.realtime.equities.Trade trade, + OnEquitiesTradeUpdated onEquitiesTradeUpdated, + DataCache dataCache) { boolean isSet = setEquitiesTrade(trade); if (isSet && onEquitiesTradeUpdated != null) { try { onEquitiesTradeUpdated.onEquitiesTradeUpdated(this, dataCache, trade); } catch (Exception e) { - Log("Error in onEquitiesTradeUpdated Callback: " + e.getMessage()); + log("Error in onEquitiesTradeUpdated Callback: " + e.getMessage()); } } return isSet; @@ -120,13 +149,15 @@ public boolean setEquitiesQuote(intrinio.realtime.equities.Quote quote) { } @Override - public boolean setEquitiesQuote(intrinio.realtime.equities.Quote quote, OnEquitiesQuoteUpdated onEquitiesQuoteUpdated, DataCache dataCache) { + public boolean setEquitiesQuote(intrinio.realtime.equities.Quote quote, + OnEquitiesQuoteUpdated onEquitiesQuoteUpdated, + DataCache dataCache) { boolean isSet = this.setEquitiesQuote(quote); if (isSet && onEquitiesQuoteUpdated != null) { try { onEquitiesQuoteUpdated.onEquitiesQuoteUpdated(this, dataCache, quote); } catch (Exception e) { - Log("Error in onEquitiesQuoteUpdated Callback: " + e.getMessage()); + log("Error in onEquitiesQuoteUpdated Callback: " + e.getMessage()); } } return isSet; @@ -142,6 +173,7 @@ public Map getAllOptionsContractData() { return readonlyContracts; } + @Override public List getContractNames() { return contracts.values().stream().map(OptionsContractData::getContract).collect(Collectors.toList()); } @@ -156,26 +188,22 @@ public intrinio.realtime.options.Trade getOptionsContractTrade(String contract) public boolean setOptionsContractTrade(intrinio.realtime.options.Trade trade) { if (trade != null) { String contract = trade.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - CurrentOptionsContractData newDatum = new CurrentOptionsContractData(contract, trade, null, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setTrade(trade); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, trade, null, null, null)); + return current.setTrade(trade); } return false; } @Override - public boolean setOptionsContractTrade(intrinio.realtime.options.Trade trade, OnOptionsTradeUpdated onOptionsTradeUpdated, DataCache dataCache) { + public boolean setOptionsContractTrade(intrinio.realtime.options.Trade trade, + OnOptionsTradeUpdated onOptionsTradeUpdated, + DataCache dataCache) { if (trade != null) { String contract = trade.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, trade, null, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setTrade(trade, onOptionsTradeUpdated, this, dataCache); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, trade, null, null, null)); + return current.setTrade(trade, onOptionsTradeUpdated, this, dataCache); } return false; } @@ -190,26 +218,22 @@ public intrinio.realtime.options.Quote getOptionsContractQuote(String contract) public boolean setOptionsContractQuote(intrinio.realtime.options.Quote quote) { if (quote != null) { String contract = quote.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, quote, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setQuote(quote); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, quote, null, null)); + return current.setQuote(quote); } return false; } @Override - public boolean setOptionsContractQuote(intrinio.realtime.options.Quote quote, OnOptionsQuoteUpdated onOptionsQuoteUpdated, DataCache dataCache) { + public boolean setOptionsContractQuote(intrinio.realtime.options.Quote quote, + OnOptionsQuoteUpdated onOptionsQuoteUpdated, + DataCache dataCache) { if (quote != null) { String contract = quote.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, quote, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setQuote(quote, onOptionsQuoteUpdated, this, dataCache); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, quote, null, null)); + return current.setQuote(quote, onOptionsQuoteUpdated, this, dataCache); } return false; } @@ -224,26 +248,22 @@ public intrinio.realtime.options.Refresh getOptionsContractRefresh(String contra public boolean setOptionsContractRefresh(intrinio.realtime.options.Refresh refresh) { if (refresh != null) { String contract = refresh.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, refresh, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setRefresh(refresh); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, refresh, null)); + return current.setRefresh(refresh); } return false; } @Override - public boolean setOptionsContractRefresh(intrinio.realtime.options.Refresh refresh, OnOptionsRefreshUpdated onOptionsRefreshUpdated, DataCache dataCache) { + public boolean setOptionsContractRefresh(intrinio.realtime.options.Refresh refresh, + OnOptionsRefreshUpdated onOptionsRefreshUpdated, + DataCache dataCache) { if (refresh != null) { String contract = refresh.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, refresh, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setRefresh(refresh, onOptionsRefreshUpdated, this, dataCache); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, refresh, null)); + return current.setRefresh(refresh, onOptionsRefreshUpdated, this, dataCache); } return false; } @@ -258,26 +278,22 @@ public intrinio.realtime.options.UnusualActivity getOptionsContractUnusualActivi public boolean setOptionsContractUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity) { if (unusualActivity != null) { String contract = unusualActivity.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, null, unusualActivity); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setUnusualActivity(unusualActivity); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, null, unusualActivity)); + return current.setUnusualActivity(unusualActivity); } return false; } @Override - public boolean setOptionsContractUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity, OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, DataCache dataCache) { + public boolean setOptionsContractUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity, + OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, + DataCache dataCache) { if (unusualActivity != null) { String contract = unusualActivity.contract(); - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, null, unusualActivity); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setUnusualActivity(unusualActivity, onOptionsUnusualActivityUpdated, this, dataCache); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, null, unusualActivity)); + return current.setUnusualActivity(unusualActivity, onOptionsUnusualActivityUpdated, this, dataCache); } return false; } @@ -291,25 +307,24 @@ public Double getOptionsContractSupplementalDatum(String contract, String key) { @Override public boolean setOptionsContractSupplementalDatum(String contract, String key, Double datum, SupplementalDatumUpdate update) { if (contract != null && !contract.trim().isEmpty()) { - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setSupplementaryDatum(key, datum, update); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, null, null)); + return current.setSupplementaryDatum(key, datum, update); } return false; } @Override - public boolean setOptionsContractSupplementalDatum(String contract, String key, Double datum, OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, DataCache dataCache, SupplementalDatumUpdate update) { + public boolean setOptionsContractSupplementalDatum(String contract, + String key, + Double datum, + OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, + DataCache dataCache, + SupplementalDatumUpdate update) { if (contract != null && !contract.trim().isEmpty()) { - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setSupplementaryDatum(key, datum, onOptionsContractSupplementalDatumUpdated, this, dataCache, update); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, null, null)); + return current.setSupplementaryDatum(key, datum, onOptionsContractSupplementalDatumUpdated, this, dataCache, update); } return false; } @@ -323,30 +338,29 @@ public Greek getOptionsContractGreekData(String contract, String key) { @Override public boolean setOptionsContractGreekData(String contract, String key, Greek data, GreekDataUpdate update) { if (contract != null && !contract.trim().isEmpty()) { - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setGreekData(key, data, update); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, null, null)); + return current.setGreekData(key, data, update); } return false; } @Override - public boolean setOptionsContractGreekData(String contract, String key, Greek data, OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, DataCache dataCache, GreekDataUpdate update) { + public boolean setOptionsContractGreekData(String contract, + String key, + Greek data, + OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, + DataCache dataCache, + GreekDataUpdate update) { if (contract != null && !contract.trim().isEmpty()) { - OptionsContractData currentOptionsContractData = contracts.get(contract); - if (currentOptionsContractData == null) { - OptionsContractData newDatum = new CurrentOptionsContractData(contract, null, null, null, null); - currentOptionsContractData = contracts.computeIfAbsent(contract, k -> newDatum); - } - return currentOptionsContractData.setGreekData(key, data, onOptionsContractGreekDataUpdated, this, dataCache, update); + OptionsContractData current = contracts.computeIfAbsent( + contract, k -> new CurrentOptionsContractData(contract, null, null, null, null)); + return current.setGreekData(key, data, onOptionsContractGreekDataUpdated, this, dataCache, update); } return false; } - private void Log(String message){ + private void log(String message) { System.out.println(message); } -} \ No newline at end of file +} diff --git a/src/intrinio/realtime/composite/DataCache.java b/src/intrinio/realtime/composite/DataCache.java index 37fccaf..ab59ad2 100644 --- a/src/intrinio/realtime/composite/DataCache.java +++ b/src/intrinio/realtime/composite/DataCache.java @@ -3,194 +3,339 @@ import java.util.Map; /** - * A non-transactional, thread-safe, volatile local cache for storing the latest data from a websocket. + * A non-transactional, thread-safe, volatile local cache for storing the latest data from + * equities and options WebSocket feeds, plus optional supplemental and Greek values. + *

+ * 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 getAllSupplementaryData(); /** * Get a supplemental data point stored in a specific security's cache. + * + * @param tickerSymbol equity ticker + * @param key datum key + * @return the value, or {@code null} if absent */ Double getSecuritySupplementalDatum(String tickerSymbol, String key); /** * Set a supplemental data point stored in a specific security's cache. + * Creates the security sub-cache if needed. + * + * @param tickerSymbol equity ticker + * @param key datum key + * @param datum new value + * @param update merge function + * @return {@code true} if the value was accepted by the merge */ boolean setSecuritySupplementalDatum(String tickerSymbol, String key, Double datum, SupplementalDatumUpdate update); /** * Get a supplemental data point stored in a specific option contract's cache. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @param key datum key + * @return the value, or {@code null} if absent */ Double getOptionsContractSupplementalDatum(String tickerSymbol, String contract, String key); /** * Set a supplemental data point stored in a specific option contract's cache. + * Creates security and contract sub-caches if needed. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @param key datum key + * @param datum new value + * @param update merge function + * @return {@code true} if the value was accepted by the merge */ boolean setOptionSupplementalDatum(String tickerSymbol, String contract, String key, Double datum, SupplementalDatumUpdate update); + //endregion Supplementary Data + + //region Greek Data + /** - * Get a supplemental data point stored in a specific option contract's cache. + * Get Greek data stored for a specific option contract. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @param key Greek series key (e.g. {@link GreekClient#BLACK_SCHOLES_KEY_NAME}) + * @return the Greek value, or {@code null} if absent */ Greek getOptionsContractGreekData(String tickerSymbol, String contract, String key); /** - * Set a supplemental data point stored in a specific option contract's cache. + * Set Greek data for a specific option contract. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @param key Greek series key + * @param data new Greek value + * @param update merge function + * @return {@code true} if the value was accepted by the merge */ boolean setOptionGreekData(String tickerSymbol, String contract, String key, Greek data, GreekDataUpdate update); + //endregion Greek Data + + //region Sub-caches + /** - * Get the cache for a specific security + * Get the cache for a specific security. + * + * @param tickerSymbol equity ticker + * @return security data, or {@code null} if never created */ SecurityData getSecurityData(String tickerSymbol); /** * Get all security caches. + * The returned map is a live, unmodifiable view of concurrent storage. + * + * @return unmodifiable view of all securities */ Map getAllSecurityData(); /** * Get a specific option contract's cache. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @return contract data, or {@code null} if absent */ OptionsContractData getOptionsContractData(String tickerSymbol, String contract); /** * Get all option contract caches for a security. + * + * @param tickerSymbol underlying ticker + * @return unmodifiable view of contracts, or an empty map if the security is unknown */ Map getAllOptionsContractData(String tickerSymbol); + //endregion Sub-caches + + //region Equities + /** * Get the latest trade for a security. + * + * @param tickerSymbol equity ticker + * @return latest trade, or {@code null} */ intrinio.realtime.equities.Trade getLatestEquityTrade(String tickerSymbol); /** - * Set the latest trade for a security. + * Set the latest trade for a security (dirty set by timestamp). + * Creates the security sub-cache if needed and may invoke {@link OnEquitiesTradeUpdated}. + * + * @param trade equities trade + * @return {@code true} if the trade was stored as the new latest */ boolean setEquityTrade(intrinio.realtime.equities.Trade trade); /** * Get the latest ask quote for a security. + * + * @param tickerSymbol equity ticker + * @return latest ask quote, or {@code null} */ intrinio.realtime.equities.Quote getLatestEquityAskQuote(String tickerSymbol); /** - * Set the latest bid quote for a security. + * Get the latest bid quote for a security. + * + * @param tickerSymbol equity ticker + * @return latest bid quote, or {@code null} */ intrinio.realtime.equities.Quote getLatestEquityBidQuote(String tickerSymbol); /** - * Set the latest quote for a security. + * Set the latest quote for a security (ask or bid by quote type; dirty set by timestamp). + * + * @param quote equities quote + * @return {@code true} if the quote was stored as the new latest for its side */ boolean setEquityQuote(intrinio.realtime.equities.Quote quote); + //endregion Equities + + //region Options + /** * Get the latest option contract trade. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @return latest trade, or {@code null} */ intrinio.realtime.options.Trade getLatestOptionsTrade(String tickerSymbol, String contract); /** - * Set the latest option contract trade. + * Set the latest option contract trade (dirty set by timestamp). + * + * @param trade options trade + * @return {@code true} if the trade was stored as the new latest */ boolean setOptionsTrade(intrinio.realtime.options.Trade trade); /** * Get the latest option contract quote. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @return latest quote, or {@code null} */ intrinio.realtime.options.Quote getLatestOptionsQuote(String tickerSymbol, String contract); /** - * Set the latest option contract quote. + * Set the latest option contract quote (dirty set by timestamp). + * + * @param quote options quote + * @return {@code true} if the quote was stored as the new latest */ boolean setOptionsQuote(intrinio.realtime.options.Quote quote); /** * Get the latest option contract refresh. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @return latest refresh, or {@code null} */ intrinio.realtime.options.Refresh getLatestOptionsRefresh(String tickerSymbol, String contract); /** - * Set the latest option contract refresh. + * Set the latest option contract refresh (always overwrites). + * + * @param refresh options refresh + * @return {@code true} if stored */ boolean setOptionsRefresh(intrinio.realtime.options.Refresh refresh); /** * Get the latest option contract unusual activity. + * + * @param tickerSymbol underlying ticker + * @param contract option contract id + * @return latest unusual activity, or {@code null} */ intrinio.realtime.options.UnusualActivity getLatestOptionsUnusualActivity(String tickerSymbol, String contract); /** - * Set the latest option contract unusual activity. + * Set the latest option contract unusual activity (always overwrites). + * + * @param unusualActivity unusual activity event + * @return {@code true} if stored */ boolean setOptionsUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity); - /** - * Set the callback when the top level supplemental data is updated. - */ + //endregion Options + + //region Callbacks + + /** @return callback for top-level supplemental updates, or {@code null} */ OnSupplementalDatumUpdated getSupplementalDatumUpdatedCallback(); - void setSupplementalDatumUpdatedCallback(OnSupplementalDatumUpdated callback); /** - * Set the callback when a security's supplemental data is updated. + * Set the callback when the top-level supplemental data is updated. + * Replaces any previous callback (compose/chain externally if multiple listeners are needed). */ + void setSupplementalDatumUpdatedCallback(OnSupplementalDatumUpdated callback); + + /** @return callback for security-level supplemental updates, or {@code null} */ OnSecuritySupplementalDatumUpdated getSecuritySupplementalDatumUpdatedCallback(); + + /** Set the callback when a security's supplemental data is updated. */ void setSecuritySupplementalDatumUpdatedCallback(OnSecuritySupplementalDatumUpdated callback); - /** - * Set the callback when an option contract's supplemental data is updated. - */ + /** @return callback for option-contract supplemental updates, or {@code null} */ OnOptionsContractSupplementalDatumUpdated getOptionsContractSupplementalDatumUpdatedCallback(); + + /** Set the callback when an option contract's supplemental data is updated. */ void setOptionsContractSupplementalDatumUpdatedCallback(OnOptionsContractSupplementalDatumUpdated callback); - /** - * Set the callback for when the latest equity trade is updated. - */ + /** @return callback for equities trade updates, or {@code null} */ OnEquitiesTradeUpdated getEquitiesTradeUpdatedCallback(); + + /** Set the callback for when the latest equity trade is updated. */ void setEquitiesTradeUpdatedCallback(OnEquitiesTradeUpdated callback); - /** - * Set the callback for when the latest equity quote is updated. - */ + /** @return callback for equities quote updates, or {@code null} */ OnEquitiesQuoteUpdated getEquitiesQuoteUpdatedCallback(); + + /** Set the callback for when the latest equity quote is updated. */ void setEquitiesQuoteUpdatedCallback(OnEquitiesQuoteUpdated callback); - /** - * Set the callback for when the latest option trade is updated. - */ + /** @return callback for options trade updates, or {@code null} */ OnOptionsTradeUpdated getOptionsTradeUpdatedCallback(); + + /** Set the callback for when the latest option trade is updated. */ void setOptionsTradeUpdatedCallback(OnOptionsTradeUpdated callback); - /** - * Set the callback for when the latest option quote is updated. - */ + /** @return callback for options quote updates, or {@code null} */ OnOptionsQuoteUpdated getOptionsQuoteUpdatedCallback(); + + /** Set the callback for when the latest option quote is updated. */ void setOptionsQuoteUpdatedCallback(OnOptionsQuoteUpdated callback); - /** - * Set the callback for when the latest option refresh is updated. - */ + /** @return callback for options refresh updates, or {@code null} */ OnOptionsRefreshUpdated getOptionsRefreshUpdatedCallback(); + + /** Set the callback for when the latest option refresh is updated. */ void setOptionsRefreshUpdatedCallback(OnOptionsRefreshUpdated callback); - /** - * Set the callback for when the latest option unusual activity is updated. - */ + /** @return callback for options unusual-activity updates, or {@code null} */ OnOptionsUnusualActivityUpdated getOptionsUnusualActivityUpdatedCallback(); + + /** Set the callback for when the latest option unusual activity is updated. */ void setOptionsUnusualActivityUpdatedCallback(OnOptionsUnusualActivityUpdated callback); + /** @return callback for option Greek updates, or {@code null} */ OnOptionsContractGreekDataUpdated getOptionsContractGreekDataUpdatedCallback(); + + /** Set the callback for when option contract Greek data is updated. */ void setOptionsContractGreekDataUpdatedCallback(OnOptionsContractGreekDataUpdated callback); -} \ No newline at end of file + + //endregion Callbacks +} diff --git a/src/intrinio/realtime/composite/DataCacheFactory.java b/src/intrinio/realtime/composite/DataCacheFactory.java index 39b9d51..b1495cc 100644 --- a/src/intrinio/realtime/composite/DataCacheFactory.java +++ b/src/intrinio/realtime/composite/DataCacheFactory.java @@ -1,7 +1,21 @@ package intrinio.realtime.composite; -public class DataCacheFactory { +/** + * Factory for {@link DataCache} instances. + *

+ * 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 calcLookup; + + /** Replace-style Greek cache update: always keep the newly computed value. */ + private final GreekDataUpdate updateFuncGreek = + (String key, Greek oldValue, Greek newValue) -> newValue; + + /** Replace-style numeric supplemental update: always keep the new value. */ + private final SupplementalDatumUpdate updateFuncNumber = + (String key, Double oldValue, Double newValue) -> newValue; + + /** Tracks tickers observed on the wire or from REST, and last dividend-refresh time. */ + private final ConcurrentHashMap seenTickers; + + /** Periodic risk-free rate fetch. */ + private Timer riskFreeInterestRateFetchTimer; + + /** Periodic dividend-yield refresh. */ + private Timer dividendFetchTimer; + + /** Background work for bulk REST seeding. */ + private final ExecutorService startupExecutor; + + /** Intrinio API key used for REST dividend / rate / universe calls. */ + private final String apiKey; + + /** Hours between dividend-yield refreshes for a given ticker. */ + private volatile int dividendYieldUpdatePeriodHours = 4; + + /** Minimum spacing between REST calls to avoid rate limiting (milliseconds). */ + private volatile int apiCallSpacerMilliseconds = 1100; + + /** Guards concurrent dividend bulk/refresh work. */ + private final AtomicBoolean dividendYieldWorking = new AtomicBoolean(false); + + /** + * {@code true} when this client created {@link #cache} itself and therefore must + * write equities/options events into the cache from {@code on*} handlers. + */ + private final boolean selfCache; + + /** Optional user callback invoked when Greek data is written to the cache. */ + private volatile OnOptionsContractGreekDataUpdated onGreekValueUpdated; + + //endregion Data Members + + //region Constructors + + /** + * Creates a {@code GreekClient} that calculates realtime Greeks from equities and options streams. + * + * @param greekUpdateFrequency flags controlling when Greeks are recalculated + * @param onGreekValueUpdated callback invoked when a contract's Greek data is updated; may be {@code null} + * @param apiKey Intrinio API key for REST fetches (rates, dividends, tickers) + * @param cache optional external {@link DataCache}; if {@code null}, an internal cache is created + */ + public GreekClient(EnumSet greekUpdateFrequency, + OnOptionsContractGreekDataUpdated onGreekValueUpdated, + String apiKey, + DataCache cache) { + if (apiKey == null || apiKey.isBlank()) { + throw new IllegalArgumentException("apiKey must be provided"); + } + if (greekUpdateFrequency == null || greekUpdateFrequency.isEmpty()) { + throw new IllegalArgumentException("greekUpdateFrequency must contain at least one flag"); + } + + this.apiKey = apiKey; + this.apiCallSpacerMilliseconds = 1100; + this.dividendYieldUpdatePeriodHours = 4; + this.selfCache = (cache == null); + this.cache = (cache != null) ? cache : DataCacheFactory.create(); + this.seenTickers = new ConcurrentHashMap<>(); + this.calcLookup = new ConcurrentHashMap<>(); + this.startupExecutor = Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "GreekClient-startup"); + t.setDaemon(true); + return t; + }); + + setOnGreekValueUpdated(onGreekValueUpdated); + registerUpdateFrequencyCallbacks(greekUpdateFrequency); + } + + /** + * Creates a {@code GreekClient} with an internally owned {@link DataCache}. + * + * @param greekUpdateFrequency flags controlling when Greeks are recalculated + * @param onGreekValueUpdated callback invoked when a contract's Greek data is updated + * @param apiKey Intrinio API key + */ + public GreekClient(EnumSet greekUpdateFrequency, + OnOptionsContractGreekDataUpdated onGreekValueUpdated, + String apiKey) { + this(greekUpdateFrequency, onGreekValueUpdated, apiKey, null); + } + + //endregion Constructors + + //region Public Properties + + /** + * @return the cache used by this client (shared or self-owned) + */ + public DataCache getCache() { + return cache; + } + + /** + * @return hours between dividend-yield refreshes per ticker + */ + public int getDividendYieldUpdatePeriodHours() { + return dividendYieldUpdatePeriodHours; + } + + /** + * @param dividendYieldUpdatePeriodHours hours between dividend-yield refreshes per ticker + */ + public void setDividendYieldUpdatePeriodHours(int dividendYieldUpdatePeriodHours) { + this.dividendYieldUpdatePeriodHours = dividendYieldUpdatePeriodHours; + } + + /** + * @return milliseconds to sleep between REST calls + */ + public int getApiCallSpacerMilliseconds() { + return apiCallSpacerMilliseconds; + } + + /** + * @param apiCallSpacerMilliseconds milliseconds to sleep between REST calls + */ + public void setApiCallSpacerMilliseconds(int apiCallSpacerMilliseconds) { + this.apiCallSpacerMilliseconds = apiCallSpacerMilliseconds; + } + + /** + * Registers (or chains) a callback for Greek cache updates on the underlying {@link DataCache}. + * + * @param onGreekValueUpdated callback to invoke when option Greek data is updated; may be {@code null} + */ + public void setOnGreekValueUpdated(OnOptionsContractGreekDataUpdated onGreekValueUpdated) { + this.onGreekValueUpdated = onGreekValueUpdated; + if (onGreekValueUpdated == null) { + return; + } + OnOptionsContractGreekDataUpdated existing = cache.getOptionsContractGreekDataUpdatedCallback(); + if (existing == null) { + cache.setOptionsContractGreekDataUpdatedCallback(onGreekValueUpdated); + } else if (existing != onGreekValueUpdated) { + cache.setOptionsContractGreekDataUpdatedCallback( + (key, datum, optionsContractData, securityData, dataCache) -> { + existing.onOptionsContractGreekDataUpdated(key, datum, optionsContractData, securityData, dataCache); + onGreekValueUpdated.onOptionsContractGreekDataUpdated(key, datum, optionsContractData, securityData, dataCache); + }); + } + } + + //endregion Public Properties + + //region Public Methods + + /** + * Starts background REST seeding (optionable tickers, securities, historical dividend metrics) + * and periodic timers for risk-free rate and dividend-yield refresh. + */ + public void start() { + startupExecutor.execute(() -> { + log("Fetching company daily metrics in bulk"); + for (int i = 365; i >= 0; i--) { + fetchInitialCompanyDividends(i); + } + }); + startupExecutor.execute(() -> { + log("Fetching list of tickers with options associated"); + cacheListOfOptionableTickers(); + }); + startupExecutor.execute(() -> { + log("Fetching list of all securities."); + cacheAllSecurities(); + }); + + log("Fetching risk free interest rate and periodically additional new dividend yields"); + riskFreeInterestRateFetchTimer = new Timer("GreekClient-riskFreeRate", true); + riskFreeInterestRateFetchTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + fetchRiskFreeInterestRate(); + } + }, 0L, 11L * 60L * 60L * 1000L); + + dividendFetchTimer = new Timer("GreekClient-dividends", true); + dividendFetchTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + refreshDividendYields(); + } + }, 60_000L, 30_000L); + } + + /** + * Stops periodic timers and shuts down startup background work. + */ + public void stop() { + if (riskFreeInterestRateFetchTimer != null) { + try { + riskFreeInterestRateFetchTimer.cancel(); + } catch (Exception ignored) { + } + riskFreeInterestRateFetchTimer = null; + } + if (dividendFetchTimer != null) { + try { + dividendFetchTimer.cancel(); + } catch (Exception ignored) { + } + dividendFetchTimer = null; + } + startupExecutor.shutdownNow(); + } + + /** + * Equities trade handler. Tracks the ticker and, in self-cache mode, writes the trade into the cache. + * + * @param trade equities trade event + */ + public void onEquityTrade(intrinio.realtime.equities.Trade trade) { + try { + if (trade == null) { + return; + } + seenTickers.putIfAbsent(trade.symbol().intern(), Instant.EPOCH); + if (selfCache) { + cache.setEquityTrade(trade); + } + } catch (Exception e) { + log("Error on handling equity trade in GreekClient: " + e.getMessage()); + } + } + + /** + * Equities quote handler. Tracks the ticker and, in self-cache mode, writes the quote into the cache. + * + * @param quote equities quote event + */ + public void onEquityQuote(intrinio.realtime.equities.Quote quote) { + try { + if (quote == null) { + return; + } + seenTickers.putIfAbsent(quote.symbol().intern(), Instant.EPOCH); + if (selfCache) { + cache.setEquityQuote(quote); + } + } catch (Exception e) { + log("Error on handling equity quote in GreekClient: " + e.getMessage()); + } + } + + /** + * Options trade handler. Tracks the underlying and, in self-cache mode, writes the trade into the cache. + * + * @param trade options trade event + */ + public void onOptionsTrade(intrinio.realtime.options.Trade trade) { + try { + if (trade == null) { + return; + } + seenTickers.putIfAbsent(trade.getUnderlyingSymbol().intern(), Instant.EPOCH); + if (selfCache) { + cache.setOptionsTrade(trade); + } + } catch (Exception e) { + log("Error on handling option trade in GreekClient: " + e.getMessage()); + } + } + + /** + * Options quote handler. Tracks the underlying and, in self-cache mode, writes the quote into the cache. + * + * @param quote options quote event + */ + public void onOptionsQuote(intrinio.realtime.options.Quote quote) { + try { + if (quote == null) { + return; + } + seenTickers.putIfAbsent(quote.getUnderlyingSymbol().intern(), Instant.EPOCH); + if (selfCache) { + cache.setOptionsQuote(quote); + } + } catch (Exception e) { + log("Error on handling option quote in GreekClient: " + e.getMessage()); + } + } + + /** + * Options refresh handler (no-op; provided for symmetry with the C# plug-in interface). + * + * @param refresh options refresh event + */ + public void onOptionsRefresh(intrinio.realtime.options.Refresh refresh) { + // intentionally empty + } + + /** + * Options unusual-activity handler (no-op; provided for symmetry with the C# plug-in interface). + * + * @param unusualActivity unusual activity event + */ + public void onOptionsUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity) { + // intentionally empty + } + + /** + * Registers or replaces a named Greek calculation strategy. + * + * @param name calculator name (also used as the Greek cache key by built-in strategies) + * @param calc calculation delegate; must not be {@code null} + * @return {@code true} if the calculator was stored + */ + public boolean tryAddOrUpdateGreekCalculation(String name, CalculateNewGreek calc) { + if (name == null || name.isBlank() || calc == null) { + return false; + } + calcLookup.put(name, calc); + return true; + } + + /** + * Registers the built-in Black–Scholes calculator appropriate for the given options provider. + *
    + *
  • {@link intrinio.realtime.options.Provider#OPTIONS_EDGE} — uses option trade mid (trade price)
  • + *
  • All other providers (including OPRA) — use option quote mid and ask/bid IVs
  • + *
+ * + * @param provider options provider; {@code null} defaults to OPRA-style quote-based calculation + */ + public void addBlackScholes(intrinio.realtime.options.Provider provider) { + if (provider == null) { + provider = intrinio.realtime.options.Provider.OPRA; + } + switch (provider) { + case OPTIONS_EDGE: + tryAddOrUpdateGreekCalculation(BLACK_SCHOLES_KEY_NAME, this::blackScholesCalcOptionsEdge); + break; + case OPRA: + case MANUAL: + case NONE: + default: + tryAddOrUpdateGreekCalculation(BLACK_SCHOLES_KEY_NAME, this::blackScholesCalc); + break; + } + } + + /** + * Registers the built-in Black–Scholes calculator using OPRA-style quote-based inputs. + */ + public void addBlackScholes() { + addBlackScholes(intrinio.realtime.options.Provider.OPRA); + } + + //endregion Public Methods + + //region Private REST / Startup + + /** + * Fetches the universe of optionable tickers into {@link #seenTickers}. + */ + private void cacheListOfOptionableTickers() { + try { + String body = httpGet("/options/tickers"); + if (body == null) { + return; + } + JsonObject root = JsonParser.parseString(body).getAsJsonObject(); + JsonArray tickers = root.has("tickers") && root.get("tickers").isJsonArray() + ? root.getAsJsonArray("tickers") + : null; + if (tickers == null) { + return; + } + int count = 0; + for (JsonElement el : tickers) { + if (el != null && el.isJsonPrimitive()) { + String ticker = el.getAsString(); + if (ticker != null && !ticker.isBlank()) { + seenTickers.putIfAbsent(ticker.intern(), Instant.EPOCH); + count++; + } + } + } + log("Found " + count + " optionable tickers."); + } catch (Exception e) { + log("Error in cacheListOfOptionableTickers - " + e + ", " + e.getMessage()); + } + } + + /** + * Pages through active primary US listings and records tickers for dividend refresh. + */ + private void cacheAllSecurities() { + try { + String nextPage = null; + do { + try { + StringBuilder path = new StringBuilder("/securities?active=true&delisted=false&primary_listing=true&composite_mic=USCOMP&page_size=9999"); + if (nextPage != null && !nextPage.isBlank()) { + path.append("&next_page=").append(urlEncode(nextPage)); + } + String body = httpGet(path.toString()); + if (body == null) { + break; + } + JsonObject root = JsonParser.parseString(body).getAsJsonObject(); + JsonArray securities = root.has("securities") && root.get("securities").isJsonArray() + ? root.getAsJsonArray("securities") + : null; + if (securities != null) { + for (JsonElement el : securities) { + if (el == null || !el.isJsonObject()) { + continue; + } + JsonObject sec = el.getAsJsonObject(); + if (sec.has("ticker") && !sec.get("ticker").isJsonNull()) { + String ticker = sec.get("ticker").getAsString(); + if (ticker != null && !ticker.isBlank()) { + seenTickers.putIfAbsent(ticker.intern(), Instant.EPOCH); + } + } + } + } + nextPage = (root.has("next_page") && !root.get("next_page").isJsonNull()) + ? root.get("next_page").getAsString() + : null; + sleepQuietly(apiCallSpacerMilliseconds); + } catch (Exception e) { + log("Error: " + e + "; " + e.getMessage()); + sleepQuietly(apiCallSpacerMilliseconds); + break; + } + } while (nextPage != null && !nextPage.isBlank()); + } catch (Exception e) { + log("Error: " + e + "; " + e.getMessage()); + } + } + + /** + * Loads company daily metrics for a historical day offset and seeds dividend yields. + * + * @param daysAgo number of days before today + */ + private void fetchInitialCompanyDividends(int daysAgo) { + if (!dividendYieldWorking.compareAndSet(false, true)) { + return; + } + try { + String nextPage = null; + LocalDate date = LocalDate.now(ZoneOffset.UTC).minusDays(daysAgo); + String dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE); + do { + StringBuilder path = new StringBuilder("/companies/daily_metrics?on_date=") + .append(urlEncode(dateStr)) + .append("&page_size=1000"); + if (nextPage != null && !nextPage.isBlank()) { + path.append("&next_page=").append(urlEncode(nextPage)); + } + String body = httpGet(path.toString()); + if (body == null) { + break; + } + JsonObject root = JsonParser.parseString(body).getAsJsonObject(); + JsonArray dailyMetrics = root.has("daily_metrics") && root.get("daily_metrics").isJsonArray() + ? root.getAsJsonArray("daily_metrics") + : null; + if (dailyMetrics != null) { + for (JsonElement el : dailyMetrics) { + if (el == null || !el.isJsonObject()) { + continue; + } + JsonObject metric = el.getAsJsonObject(); + JsonObject company = (metric.has("company") && metric.get("company").isJsonObject()) + ? metric.getAsJsonObject("company") + : null; + if (company == null || !company.has("ticker") || company.get("ticker").isJsonNull()) { + continue; + } + String ticker = company.get("ticker").getAsString(); + if (ticker == null || ticker.isBlank()) { + continue; + } + if (metric.has("dividend_yield") && !metric.get("dividend_yield").isJsonNull()) { + double yield = metric.get("dividend_yield").getAsDouble(); + cache.setSecuritySupplementalDatum(ticker, DIVIDEND_YIELD_KEY_NAME, yield, updateFuncNumber); + seenTickers.put(ticker.intern(), Instant.now()); + } + } + } + nextPage = (root.has("next_page") && !root.get("next_page").isJsonNull()) + ? root.get("next_page").getAsString() + : null; + sleepQuietly(apiCallSpacerMilliseconds); + } while (nextPage != null && !nextPage.isBlank()); + } catch (Exception e) { + log("Error: " + e + "; " + e.getMessage()); + } finally { + dividendYieldWorking.set(false); + } + } + + /** + * Refreshes trailing dividend yield for a single ticker via security data-point REST calls. + * + * @param ticker equity ticker symbol + */ + private void refreshDividendYield(String ticker) { + final String dividendYieldTag = "trailing_dividend_yield"; + try { + String securityBody = httpGet("/securities/" + urlEncode(ticker + ":US")); + sleepQuietly(apiCallSpacerMilliseconds); + if (securityBody == null) { + throw new IllegalStateException("No security body for " + ticker); + } + JsonObject security = JsonParser.parseString(securityBody).getAsJsonObject(); + if (!security.has("id") || security.get("id").isJsonNull()) { + throw new IllegalStateException("No security id for " + ticker); + } + String securityId = security.get("id").getAsString(); + String yieldBody = httpGet("/securities/" + urlEncode(securityId) + + "/data_point/" + urlEncode(dividendYieldTag) + "/number"); + double yield = 0.0D; + if (yieldBody != null && !yieldBody.isBlank()) { + // Endpoint returns a bare JSON number + yield = JsonParser.parseString(yieldBody.trim()).getAsDouble(); + } + cache.setSecuritySupplementalDatum(ticker, DIVIDEND_YIELD_KEY_NAME, yield, updateFuncNumber); + seenTickers.put(ticker.intern(), Instant.now()); + sleepQuietly(apiCallSpacerMilliseconds); + } catch (Exception e) { + cache.setSecuritySupplementalDatum(ticker, DIVIDEND_YIELD_KEY_NAME, 0.0D, updateFuncNumber); + seenTickers.put(ticker.intern(), Instant.now()); + sleepQuietly(apiCallSpacerMilliseconds); + } + } + + /** + * Refreshes dividend yields for major index underlyings and any seen ticker older than the refresh period. + */ + private void refreshDividendYields() { + if (!dividendYieldWorking.compareAndSet(false, true)) { + return; + } + try { + refreshDividendYield("SPY"); + refreshDividendYield("SPX"); + refreshDividendYield("SPXW"); + refreshDividendYield("RUT"); + refreshDividendYield("VIX"); + log("Refreshing dividend yields for " + seenTickers.size() + " tickers..."); + Instant cutoff = Instant.now().minusSeconds(dividendYieldUpdatePeriodHours * 3600L); + for (Map.Entry entry : seenTickers.entrySet()) { + Instant last = entry.getValue(); + if (last == null || last.isBefore(cutoff)) { + refreshDividendYield(entry.getKey()); + } + } + } catch (Exception e) { + log("Error: " + e + "; " + e.getMessage()); + } finally { + dividendYieldWorking.set(false); + } + } + + /** + * Fetches the 3-month Treasury bill rate ({@code $DTB3}) and stores it as a top-level supplemental datum. + */ + private void fetchRiskFreeInterestRate() { + boolean success = false; + int tryCount = 0; + do { + tryCount++; + try { + String body = httpGet("/indices/economic/" + urlEncode("$DTB3") + "/data_point/level/number"); + if (body != null && !body.isBlank()) { + double level = JsonParser.parseString(body.trim()).getAsDouble(); + cache.setSupplementaryDatum(RISK_FREE_INTEREST_RATE_KEY_NAME, level / 100.0D, updateFuncNumber); + success = true; + } + if (!success) { + sleepQuietly(10_000); + } + } catch (Exception e) { + log("Error: " + e + "; " + e.getMessage()); + } + } while (!success && tryCount < 10); + } + + /** + * Performs a GET against Intrinio API v2 with the configured API key. + * + * @param pathAndQuery path beginning with {@code /}, optionally including query string + * @return response body, or {@code null} on non-success / error + */ + private String httpGet(String pathAndQuery) { + HttpURLConnection connection = null; + try { + String separator = pathAndQuery.contains("?") ? "&" : "?"; + String full = API_BASE + pathAndQuery + separator + "api_key=" + urlEncode(apiKey); + URL url = URI.create(full).toURL(); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept", "application/json"); + connection.setConnectTimeout(30_000); + connection.setReadTimeout(60_000); + int code = connection.getResponseCode(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + code >= 200 && code < 300 ? connection.getInputStream() : connection.getErrorStream(), + StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + reader.close(); + if (code < 200 || code >= 300) { + log("HTTP " + code + " for " + pathAndQuery + ": " + sb); + return null; + } + return sb.toString(); + } catch (Exception e) { + log("HTTP error for " + pathAndQuery + ": " + e.getMessage()); + return null; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private static String urlEncode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + private static void sleepQuietly(long millis) { + try { + Thread.sleep(Math.max(0L, millis)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void log(String message) { + System.out.println(message); + } + + //endregion Private REST / Startup + + //region Private Greek Updates + + /** + * Chains this client's Greek recalculation onto the data-cache callbacks selected by frequency flags. + * Existing user callbacks (if any) are preserved and invoked first. + */ + private void registerUpdateFrequencyCallbacks(EnumSet frequencies) { + if (frequencies.contains(GreekUpdateFrequency.EVERY_OPTIONS_TRADE_UPDATE)) { + OnOptionsTradeUpdated previous = cache.getOptionsTradeUpdatedCallback(); + cache.setOptionsTradeUpdatedCallback((optionsContractData, dataCache, securityData, trade) -> { + if (previous != null) { + previous.onOptionsTradeUpdated(optionsContractData, dataCache, securityData, trade); + } + updateGreeks(optionsContractData, dataCache, securityData); + }); + } + + if (frequencies.contains(GreekUpdateFrequency.EVERY_OPTIONS_QUOTE_UPDATE)) { + OnOptionsQuoteUpdated previous = cache.getOptionsQuoteUpdatedCallback(); + cache.setOptionsQuoteUpdatedCallback((optionsContractData, dataCache, securityData, quote) -> { + if (previous != null) { + previous.onOptionsQuoteUpdated(optionsContractData, dataCache, securityData, quote); + } + updateGreeks(optionsContractData, dataCache, securityData); + }); + } + + if (frequencies.contains(GreekUpdateFrequency.EVERY_DIVIDEND_YIELD_UPDATE)) { + OnSecuritySupplementalDatumUpdated previous = cache.getSecuritySupplementalDatumUpdatedCallback(); + cache.setSecuritySupplementalDatumUpdatedCallback((key, datum, securityData, dataCache) -> { + if (previous != null) { + previous.onSecuritySupplementalDatumUpdated(key, datum, securityData, dataCache); + } + updateGreeksFromSecuritySupplemental(key, datum, securityData, dataCache); + }); + } + + if (frequencies.contains(GreekUpdateFrequency.EVERY_RISK_FREE_INTEREST_RATE_UPDATE)) { + OnSupplementalDatumUpdated previous = cache.getSupplementalDatumUpdatedCallback(); + cache.setSupplementalDatumUpdatedCallback((key, datum, dataCache) -> { + if (previous != null) { + previous.onSupplementalDatumUpdated(key, datum, dataCache); + } + updateGreeksFromTopLevelSupplemental(key, datum, dataCache); + }); + } + + if (frequencies.contains(GreekUpdateFrequency.EVERY_EQUITY_TRADE_UPDATE)) { + OnEquitiesTradeUpdated previous = cache.getEquitiesTradeUpdatedCallback(); + cache.setEquitiesTradeUpdatedCallback((securityData, dataCache, trade) -> { + if (previous != null) { + previous.onEquitiesTradeUpdated(securityData, dataCache, trade); + } + updateGreeks(securityData, dataCache); + }); + } + + if (frequencies.contains(GreekUpdateFrequency.EVERY_EQUITY_QUOTE_UPDATE)) { + OnEquitiesQuoteUpdated previous = cache.getEquitiesQuoteUpdatedCallback(); + cache.setEquitiesQuoteUpdatedCallback((securityData, dataCache, quote) -> { + if (previous != null) { + previous.onEquitiesQuoteUpdated(securityData, dataCache, quote); + } + updateGreeks(securityData, dataCache); + }); + } + } + + /** + * Recalculates Greeks for every cached contract when the risk-free rate changes. + */ + private void updateGreeksFromTopLevelSupplemental(String key, Double datum, DataCache dataCache) { + if (RISK_FREE_INTEREST_RATE_KEY_NAME.equals(key)) { + for (SecurityData securityData : dataCache.getAllSecurityData().values()) { + for (OptionsContractData optionsContractData : securityData.getAllOptionsContractData().values()) { + updateGreeks(optionsContractData, dataCache, securityData); + } + } + } + } + + /** + * Recalculates Greeks for every contract under a security when its dividend yield changes. + */ + private void updateGreeksFromSecuritySupplemental(String key, Double datum, SecurityData securityData, DataCache dataCache) { + if (DIVIDEND_YIELD_KEY_NAME.equals(key) && securityData != null) { + for (OptionsContractData optionsContractData : securityData.getAllOptionsContractData().values()) { + updateGreeks(optionsContractData, dataCache, securityData); + } + } + } + + /** + * Recalculates Greeks for every contract under the given security. + */ + private void updateGreeks(SecurityData securityData, DataCache dataCache) { + if (securityData == null) { + return; + } + for (OptionsContractData optionsContractData : securityData.getAllOptionsContractData().values()) { + updateGreeks(optionsContractData, dataCache, securityData); + } + } + + /** + * Invokes every registered {@link CalculateNewGreek} strategy for a single contract. + *

+ * 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 set) { int combined = 0; + if (set == null) { + return 0; + } for (GreekUpdateFrequency freq : set) { combined |= freq.getValue(); } return combined; } + /** + * Reconstructs an {@link EnumSet} from a bitmask integer. + * + * @param value bitmask + * @return set of matching frequencies + */ public static EnumSet fromValue(int value) { EnumSet set = EnumSet.noneOf(GreekUpdateFrequency.class); for (GreekUpdateFrequency freq : values()) { @@ -37,4 +69,4 @@ public static EnumSet fromValue(int value) { } return set; } -} \ No newline at end of file +} diff --git a/src/intrinio/realtime/composite/OnEquitiesQuoteUpdated.java b/src/intrinio/realtime/composite/OnEquitiesQuoteUpdated.java index 8f3e83c..12f265b 100644 --- a/src/intrinio/realtime/composite/OnEquitiesQuoteUpdated.java +++ b/src/intrinio/realtime/composite/OnEquitiesQuoteUpdated.java @@ -1,6 +1,15 @@ package intrinio.realtime.composite; +/** + * Callback invoked when a security's latest equities quote (ask or bid) is updated in the cache. + */ @FunctionalInterface public interface OnEquitiesQuoteUpdated { + + /** + * @param securityData security cache slice after the update + * @param dataCache the owning cache + * @param quote quote that was applied + */ void onEquitiesQuoteUpdated(SecurityData securityData, DataCache dataCache, intrinio.realtime.equities.Quote quote); } diff --git a/src/intrinio/realtime/composite/OnEquitiesTradeUpdated.java b/src/intrinio/realtime/composite/OnEquitiesTradeUpdated.java index 8b3ac13..89a4a87 100644 --- a/src/intrinio/realtime/composite/OnEquitiesTradeUpdated.java +++ b/src/intrinio/realtime/composite/OnEquitiesTradeUpdated.java @@ -1,6 +1,15 @@ package intrinio.realtime.composite; +/** + * Callback invoked when a security's latest equities trade is updated in the cache. + */ @FunctionalInterface public interface OnEquitiesTradeUpdated { + + /** + * @param securityData security cache slice after the update + * @param dataCache the owning cache + * @param trade trade that was applied + */ void onEquitiesTradeUpdated(SecurityData securityData, DataCache dataCache, intrinio.realtime.equities.Trade trade); } diff --git a/src/intrinio/realtime/composite/OnOptionsContractGreekDataUpdated.java b/src/intrinio/realtime/composite/OnOptionsContractGreekDataUpdated.java index 1090bb1..4b96229 100644 --- a/src/intrinio/realtime/composite/OnOptionsContractGreekDataUpdated.java +++ b/src/intrinio/realtime/composite/OnOptionsContractGreekDataUpdated.java @@ -1,6 +1,21 @@ package intrinio.realtime.composite; +/** + * Callback invoked when an option contract's Greek series value is updated in the cache. + */ @FunctionalInterface public interface OnOptionsContractGreekDataUpdated { - void onOptionsContractGreekDataUpdated(String key, Greek datum, OptionsContractData optionsContractData, SecurityData securityData, DataCache dataCache); + + /** + * @param key Greek series key (e.g. {@link GreekClient#BLACK_SCHOLES_KEY_NAME}) + * @param datum new Greek value that was stored + * @param optionsContractData contract cache slice + * @param securityData underlying security cache slice + * @param dataCache the owning cache + */ + void onOptionsContractGreekDataUpdated(String key, + Greek datum, + OptionsContractData optionsContractData, + SecurityData securityData, + DataCache dataCache); } diff --git a/src/intrinio/realtime/composite/OnOptionsContractSupplementalDatumUpdated.java b/src/intrinio/realtime/composite/OnOptionsContractSupplementalDatumUpdated.java index 706c890..8f3791e 100644 --- a/src/intrinio/realtime/composite/OnOptionsContractSupplementalDatumUpdated.java +++ b/src/intrinio/realtime/composite/OnOptionsContractSupplementalDatumUpdated.java @@ -1,6 +1,21 @@ package intrinio.realtime.composite; +/** + * Callback invoked when an option-contract supplemental datum is updated. + */ @FunctionalInterface public interface OnOptionsContractSupplementalDatumUpdated { - void onOptionsContractSupplementalDatumUpdated(String key, Double datum, OptionsContractData optionsContractData, SecurityData securityData, DataCache dataCache); + + /** + * @param key supplemental datum key + * @param datum new value that was stored + * @param optionsContractData contract cache slice + * @param securityData underlying security cache slice + * @param dataCache the owning cache + */ + void onOptionsContractSupplementalDatumUpdated(String key, + Double datum, + OptionsContractData optionsContractData, + SecurityData securityData, + DataCache dataCache); } diff --git a/src/intrinio/realtime/composite/OnOptionsQuoteUpdated.java b/src/intrinio/realtime/composite/OnOptionsQuoteUpdated.java index 9879778..e7643d3 100644 --- a/src/intrinio/realtime/composite/OnOptionsQuoteUpdated.java +++ b/src/intrinio/realtime/composite/OnOptionsQuoteUpdated.java @@ -1,6 +1,19 @@ package intrinio.realtime.composite; +/** + * Callback invoked when an option contract's latest quote is updated in the cache. + */ @FunctionalInterface public interface OnOptionsQuoteUpdated { - void onOptionsQuoteUpdated(OptionsContractData optionsContractData, DataCache dataCache, SecurityData securityData, intrinio.realtime.options.Quote quote); + + /** + * @param optionsContractData contract cache slice after the update + * @param dataCache the owning cache + * @param securityData underlying security cache slice + * @param quote quote that was applied + */ + void onOptionsQuoteUpdated(OptionsContractData optionsContractData, + DataCache dataCache, + SecurityData securityData, + intrinio.realtime.options.Quote quote); } diff --git a/src/intrinio/realtime/composite/OnOptionsRefreshUpdated.java b/src/intrinio/realtime/composite/OnOptionsRefreshUpdated.java index 59ea966..c2c88b9 100644 --- a/src/intrinio/realtime/composite/OnOptionsRefreshUpdated.java +++ b/src/intrinio/realtime/composite/OnOptionsRefreshUpdated.java @@ -1,6 +1,19 @@ package intrinio.realtime.composite; +/** + * Callback invoked when an option contract's latest refresh is updated in the cache. + */ @FunctionalInterface public interface OnOptionsRefreshUpdated { - void onOptionsRefreshUpdated(OptionsContractData optionsContractData, DataCache dataCache, SecurityData securityData, intrinio.realtime.options.Refresh refresh); + + /** + * @param optionsContractData contract cache slice after the update + * @param dataCache the owning cache + * @param securityData underlying security cache slice + * @param refresh refresh that was applied + */ + void onOptionsRefreshUpdated(OptionsContractData optionsContractData, + DataCache dataCache, + SecurityData securityData, + intrinio.realtime.options.Refresh refresh); } diff --git a/src/intrinio/realtime/composite/OnOptionsTradeUpdated.java b/src/intrinio/realtime/composite/OnOptionsTradeUpdated.java index 1d80a89..5a8ec35 100644 --- a/src/intrinio/realtime/composite/OnOptionsTradeUpdated.java +++ b/src/intrinio/realtime/composite/OnOptionsTradeUpdated.java @@ -1,6 +1,19 @@ package intrinio.realtime.composite; +/** + * Callback invoked when an option contract's latest trade is updated in the cache. + */ @FunctionalInterface public interface OnOptionsTradeUpdated { - void onOptionsTradeUpdated(OptionsContractData optionsContractData, DataCache dataCache, SecurityData securityData, intrinio.realtime.options.Trade trade); + + /** + * @param optionsContractData contract cache slice after the update + * @param dataCache the owning cache + * @param securityData underlying security cache slice + * @param trade trade that was applied + */ + void onOptionsTradeUpdated(OptionsContractData optionsContractData, + DataCache dataCache, + SecurityData securityData, + intrinio.realtime.options.Trade trade); } diff --git a/src/intrinio/realtime/composite/OnOptionsUnusualActivityUpdated.java b/src/intrinio/realtime/composite/OnOptionsUnusualActivityUpdated.java index 1ee3f60..72f9caf 100644 --- a/src/intrinio/realtime/composite/OnOptionsUnusualActivityUpdated.java +++ b/src/intrinio/realtime/composite/OnOptionsUnusualActivityUpdated.java @@ -1,6 +1,19 @@ package intrinio.realtime.composite; +/** + * Callback invoked when an option contract's latest unusual activity is updated in the cache. + */ @FunctionalInterface public interface OnOptionsUnusualActivityUpdated { - void onOptionsUnusualActivityUpdated(OptionsContractData optionsContractData, DataCache dataCache, SecurityData securityData, intrinio.realtime.options.UnusualActivity unusualActivity); + + /** + * @param optionsContractData contract cache slice after the update + * @param dataCache the owning cache + * @param securityData underlying security cache slice + * @param unusualActivity unusual activity that was applied + */ + void onOptionsUnusualActivityUpdated(OptionsContractData optionsContractData, + DataCache dataCache, + SecurityData securityData, + intrinio.realtime.options.UnusualActivity unusualActivity); } diff --git a/src/intrinio/realtime/composite/OnSecuritySupplementalDatumUpdated.java b/src/intrinio/realtime/composite/OnSecuritySupplementalDatumUpdated.java index b7b569b..6f70bee 100644 --- a/src/intrinio/realtime/composite/OnSecuritySupplementalDatumUpdated.java +++ b/src/intrinio/realtime/composite/OnSecuritySupplementalDatumUpdated.java @@ -1,6 +1,16 @@ package intrinio.realtime.composite; +/** + * Callback invoked when a security-level supplemental datum is updated. + */ @FunctionalInterface public interface OnSecuritySupplementalDatumUpdated { + + /** + * @param key supplemental datum key + * @param datum new value that was stored + * @param securityData security cache slice + * @param dataCache the owning cache + */ void onSecuritySupplementalDatumUpdated(String key, Double datum, SecurityData securityData, DataCache dataCache); } diff --git a/src/intrinio/realtime/composite/OnSupplementalDatumUpdated.java b/src/intrinio/realtime/composite/OnSupplementalDatumUpdated.java index e956371..d78144f 100644 --- a/src/intrinio/realtime/composite/OnSupplementalDatumUpdated.java +++ b/src/intrinio/realtime/composite/OnSupplementalDatumUpdated.java @@ -1,6 +1,15 @@ package intrinio.realtime.composite; +/** + * Callback invoked when a top-level (cache-wide) supplemental datum is updated. + */ @FunctionalInterface public interface OnSupplementalDatumUpdated { + + /** + * @param key supplemental datum key + * @param datum new value that was stored + * @param dataCache the owning cache + */ void onSupplementalDatumUpdated(String key, Double datum, DataCache dataCache); } diff --git a/src/intrinio/realtime/composite/OptionsContractData.java b/src/intrinio/realtime/composite/OptionsContractData.java index 628260e..864b14f 100644 --- a/src/intrinio/realtime/composite/OptionsContractData.java +++ b/src/intrinio/realtime/composite/OptionsContractData.java @@ -1,39 +1,110 @@ package intrinio.realtime.composite; -import intrinio.realtime.options.Trade; import intrinio.realtime.options.Quote; import intrinio.realtime.options.Refresh; +import intrinio.realtime.options.Trade; import intrinio.realtime.options.UnusualActivity; -import intrinio.realtime.options.QuoteType; + import java.util.Map; /** - * Not for Use yet. Subject to change. + * Per-option-contract slice of the composite cache: latest trade, quote, refresh, + * unusual activity, plus supplemental numerics and Greek series. + *

+ * 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 getAllSupplementaryData(); Greek getGreekData(String key); + boolean setGreekData(String key, Greek datum, GreekDataUpdate update); - boolean setGreekData(String key, Greek datum, OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, SecurityData securityData, DataCache dataCache, GreekDataUpdate update); + + boolean setGreekData(String key, + Greek datum, + OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, + SecurityData securityData, + DataCache dataCache, + GreekDataUpdate update); + + /** + * @return live unmodifiable view of contract-level Greek series + */ Map getAllGreekData(); -} \ No newline at end of file +} diff --git a/src/intrinio/realtime/composite/SecurityData.java b/src/intrinio/realtime/composite/SecurityData.java index a25e83b..8980ccc 100644 --- a/src/intrinio/realtime/composite/SecurityData.java +++ b/src/intrinio/realtime/composite/SecurityData.java @@ -1,64 +1,157 @@ package intrinio.realtime.composite; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +/** + * Per-security slice of the composite cache: latest equities trade/quotes, + * security-level supplemental data, and nested option-contract caches. + *

+ * 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 getAllSupplementaryData(); + /** + * Dirty-set the latest equities trade when {@code trade} is newer. + */ boolean setEquitiesTrade(intrinio.realtime.equities.Trade trade); - boolean setEquitiesTrade(intrinio.realtime.equities.Trade trade, OnEquitiesTradeUpdated onEquitiesTradeUpdated, DataCache dataCache); + /** + * Dirty-set the latest equities trade and invoke callback on success. + */ + boolean setEquitiesTrade(intrinio.realtime.equities.Trade trade, + OnEquitiesTradeUpdated onEquitiesTradeUpdated, + DataCache dataCache); + + /** + * Dirty-set the latest equities ask or bid quote by quote type when newer. + */ boolean setEquitiesQuote(intrinio.realtime.equities.Quote quote); - boolean setEquitiesQuote(intrinio.realtime.equities.Quote quote, OnEquitiesQuoteUpdated onEquitiesQuoteUpdated, DataCache dataCache); + /** + * Dirty-set the latest equities quote and invoke callback on success. + */ + boolean setEquitiesQuote(intrinio.realtime.equities.Quote quote, + OnEquitiesQuoteUpdated onEquitiesQuoteUpdated, + DataCache dataCache); + + /** + * @param contract option contract id + * @return contract cache or {@code null} + */ OptionsContractData getOptionsContractData(String contract); + /** + * @return live unmodifiable view of all option contract caches for this security + */ Map getAllOptionsContractData(); + /** + * @return list of known contract ids under this security + */ List getContractNames(); + /** @return latest trade for the contract, or {@code null} */ intrinio.realtime.options.Trade getOptionsContractTrade(String contract); boolean setOptionsContractTrade(intrinio.realtime.options.Trade trade); - boolean setOptionsContractTrade(intrinio.realtime.options.Trade trade, OnOptionsTradeUpdated onOptionsTradeUpdated, DataCache dataCache); + boolean setOptionsContractTrade(intrinio.realtime.options.Trade trade, + OnOptionsTradeUpdated onOptionsTradeUpdated, + DataCache dataCache); + + /** @return latest quote for the contract, or {@code null} */ intrinio.realtime.options.Quote getOptionsContractQuote(String contract); boolean setOptionsContractQuote(intrinio.realtime.options.Quote quote); - boolean setOptionsContractQuote(intrinio.realtime.options.Quote quote, OnOptionsQuoteUpdated onOptionsQuoteUpdated, DataCache dataCache); + boolean setOptionsContractQuote(intrinio.realtime.options.Quote quote, + OnOptionsQuoteUpdated onOptionsQuoteUpdated, + DataCache dataCache); + + /** @return latest refresh for the contract, or {@code null} */ intrinio.realtime.options.Refresh getOptionsContractRefresh(String contract); boolean setOptionsContractRefresh(intrinio.realtime.options.Refresh refresh); - boolean setOptionsContractRefresh(intrinio.realtime.options.Refresh refresh, OnOptionsRefreshUpdated onOptionsRefreshUpdated, DataCache dataCache); + boolean setOptionsContractRefresh(intrinio.realtime.options.Refresh refresh, + OnOptionsRefreshUpdated onOptionsRefreshUpdated, + DataCache dataCache); + + /** @return latest unusual activity for the contract, or {@code null} */ intrinio.realtime.options.UnusualActivity getOptionsContractUnusualActivity(String contract); boolean setOptionsContractUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity); - boolean setOptionsContractUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity, OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, DataCache dataCache); + + boolean setOptionsContractUnusualActivity(intrinio.realtime.options.UnusualActivity unusualActivity, + OnOptionsUnusualActivityUpdated onOptionsUnusualActivityUpdated, + DataCache dataCache); Double getOptionsContractSupplementalDatum(String contract, String key); boolean setOptionsContractSupplementalDatum(String contract, String key, Double datum, SupplementalDatumUpdate update); - boolean setOptionsContractSupplementalDatum(String contract, String key, Double datum, OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, DataCache dataCache, SupplementalDatumUpdate update); + + boolean setOptionsContractSupplementalDatum(String contract, + String key, + Double datum, + OnOptionsContractSupplementalDatumUpdated onOptionsContractSupplementalDatumUpdated, + DataCache dataCache, + SupplementalDatumUpdate update); Greek getOptionsContractGreekData(String contract, String key); boolean setOptionsContractGreekData(String contract, String key, Greek data, GreekDataUpdate update); - boolean setOptionsContractGreekData(String contract, String key, Greek data, OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, DataCache dataCache, GreekDataUpdate update); -} \ No newline at end of file + + boolean setOptionsContractGreekData(String contract, + String key, + Greek data, + OnOptionsContractGreekDataUpdated onOptionsContractGreekDataUpdated, + DataCache dataCache, + GreekDataUpdate update); +} diff --git a/src/intrinio/realtime/composite/SupplementalDatumUpdate.java b/src/intrinio/realtime/composite/SupplementalDatumUpdate.java index e17647a..230dfe9 100644 --- a/src/intrinio/realtime/composite/SupplementalDatumUpdate.java +++ b/src/intrinio/realtime/composite/SupplementalDatumUpdate.java @@ -1,9 +1,17 @@ package intrinio.realtime.composite; /** - * The function used to update the Supplemental value in the cache. + * The function used to merge a supplemental numeric value into the cache for a given key. + * Invoked atomically per key by concurrent map update logic. */ @FunctionalInterface public interface SupplementalDatumUpdate { + + /** + * @param key supplemental datum 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 + */ Double supplementalDatumUpdate(String key, Double oldValue, Double newValue); } diff --git a/src/intrinio/realtime/options/Config.java b/src/intrinio/realtime/options/Config.java index 16422c9..fdab151 100644 --- a/src/intrinio/realtime/options/Config.java +++ b/src/intrinio/realtime/options/Config.java @@ -59,8 +59,9 @@ public int getOptionsNumThreads() { public boolean isDelayed() { return delayed; } public String toString() { + String maskedApiKey = maskApiKey(this.optionsApiKey); return String.format("apiKey = %s, provider = %s, ipAddress = %s, delayed = %s, symbols = %s, numThreads = %d", - this.optionsApiKey, + maskedApiKey, this.optionsProvider, this.optionsIpAddress, this.delayed, @@ -68,6 +69,16 @@ public String toString() { this.optionsNumThreads); } + private static String maskApiKey(String apiKey) { + if (apiKey == null || apiKey.isEmpty()) { + return "****"; + } + if (apiKey.length() <= 4) { + return "****" + apiKey; + } + return "****" + apiKey.substring(apiKey.length() - 4); + } + public static Config load() { System.out.println("Loading application configuration"); try { From 8e5f93f47b9682bd21ea8ecb43fe20c2cfea601c Mon Sep 17 00:00:00 2001 From: Shawn Snyder Date: Tue, 4 Aug 2026 13:39:16 -0500 Subject: [PATCH 2/2] version bump --- pom.xml | 2 +- src/intrinio/realtime/equities/Client.java | 2 +- src/intrinio/realtime/options/Client.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index c1a4dcf..9881bd5 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.intrinio IntrinioRealtimeJavaSDK - 8.2.0 + 9.0.0 jar IntrinioRealtimeJavaSDK diff --git a/src/intrinio/realtime/equities/Client.java b/src/intrinio/realtime/equities/Client.java index ef8596e..2daf794 100644 --- a/src/intrinio/realtime/equities/Client.java +++ b/src/intrinio/realtime/equities/Client.java @@ -52,7 +52,7 @@ public class Client implements WebSocket.Listener { private Thread[] processDataThreads; private boolean isCancellationRequested = false; private String HeaderClientInformationKey = "Client-Information"; - private String HeaderClientInformationValue = "IntrinioRealtimeJavaSDKv8.2"; + private String HeaderClientInformationValue = "IntrinioRealtimeJavaSDKv9.0"; private String HeaderMessageVersionKey = "UseNewEquitiesFormat"; private String HeaderMessageVersionValue = "v2"; //endregion Data Members diff --git a/src/intrinio/realtime/options/Client.java b/src/intrinio/realtime/options/Client.java index 214fa69..8db7172 100644 --- a/src/intrinio/realtime/options/Client.java +++ b/src/intrinio/realtime/options/Client.java @@ -35,7 +35,7 @@ public class Client implements WebSocket.Listener { private final Lock dataBucketLock = new ReentrantLock(); private final LinkedBlockingDeque> dataBucket = new LinkedBlockingDeque>(); private final WebSocketState wsState = new WebSocketState(); - private final String Version = "IntrinioRealtimeOptionsJavaSDKv8.2"; + private final String Version = "IntrinioRealtimeOptionsJavaSDKv9.0"; //endregion Final data members //region Data Members