← Back
const cache = {
  options: null,
  greeks: null,
  spotPrices: null,
  lastUpdate: null,
  contractUnits: null, // { BTC: 1, ETH: 1, XRP: 100, DOGE: 1000, ... }
};

/**
 * Get contract unit for an underlying (1 contract = N units of the asset).
 * XRP=100, DOGE=1000, others=1. Premium from API is per-contract.
 * pricePerUnit = lastPrice / getContractUnit(underlying)
 */
function getContractUnit(underlying) {
  return cache.contractUnits?.[underlying] || 1;
}

function parseSymbol(symbol) {
  const parts = symbol.split('-');
  if (parts.length !== 4) return null;
  return {
    underlying: parts[0],
    expiry: parts[1],
    strike: parts[2],
    type: parts[3] === 'C' ? 'CALL' : 'PUT',
  };
}

function enrichWithGreeks(options) {
  return options.map(o => {
    const g = cache.greeks ? cache.greeks[o.symbol] : null;
    return {
      ...o,
      greeks: g ? {
        delta: g.delta,
        gamma: g.gamma,
        theta: g.theta,
        vega: g.vega,
        markIV: g.markIV,
      } : null,
    };
  });
}

function applyFilters(options, query) {
  let result = options;

  if (query.underlying) {
    const u = query.underlying.toUpperCase();
    result = result.filter(o => o.symbol.startsWith(u + '-'));
  }

  if (query.expiry) {
    result = result.filter(o => o.symbol.includes('-' + query.expiry + '-'));
  }

  if (query.type) {
    const t = query.type.toUpperCase();
    const suffix = t === 'CALL' ? '-C' : '-P';
    result = result.filter(o => o.symbol.endsWith(suffix));
  }

  if (query.minStrike) {
    const min = parseFloat(query.minStrike);
    result = result.filter(o => {
      const p = parseSymbol(o.symbol);
      return p && parseFloat(p.strike) >= min;
    });
  }

  if (query.maxStrike) {
    const max = parseFloat(query.maxStrike);
    result = result.filter(o => {
      const p = parseSymbol(o.symbol);
      return p && parseFloat(p.strike) <= max;
    });
  }

  if (query.minVolume) {
    const min = parseFloat(query.minVolume);
    result = result.filter(o => parseFloat(o.volume || 0) >= min);
  }

  if (query.minDelta) {
    const min = parseFloat(query.minDelta);
    result = result.filter(o => {
      if (!cache.greeks) return false;
      const g = cache.greeks[o.symbol];
      return g && parseFloat(g.delta) >= min;
    });
  }

  if (query.maxDelta) {
    const max = parseFloat(query.maxDelta);
    result = result.filter(o => {
      if (!cache.greeks) return false;
      const g = cache.greeks[o.symbol];
      return g && parseFloat(g.delta) <= max;
    });
  }

  if (query.minGamma) {
    const min = parseFloat(query.minGamma);
    result = result.filter(o => {
      if (!cache.greeks) return false;
      const g = cache.greeks[o.symbol];
      return g && parseFloat(g.gamma) >= min;
    });
  }

  if (query.minVega) {
    const min = parseFloat(query.minVega);
    result = result.filter(o => {
      if (!cache.greeks) return false;
      const g = cache.greeks[o.symbol];
      return g && parseFloat(g.vega) >= min;
    });
  }

  if (query.minIV) {
    const min = parseFloat(query.minIV);
    result = result.filter(o => {
      if (!cache.greeks) return false;
      const g = cache.greeks[o.symbol];
      return g && parseFloat(g.markIV) >= min;
    });
  }

  if (query.maxIV) {
    const max = parseFloat(query.maxIV);
    result = result.filter(o => {
      if (!cache.greeks) return false;
      const g = cache.greeks[o.symbol];
      return g && parseFloat(g.markIV) <= max;
    });
  }

  return result;
}

module.exports = { cache, parseSymbol, enrichWithGreeks, applyFilters, getContractUnit };

📜 Git History

36ce19afix: normalize option premium by contract unit (XRP=100, DOGE=1000)3 months ago
04f5515feat: split scheduler into fast (30s) and slow (5min) cycles3 months ago
163bb5dfeat: migrate to options-screener-v2 folder to isolate deployment4 months ago
Show last diff
Loading...