← Back
function calcPCRatio(optionsData, expiry, underlying) {
  const filtered = optionsData.filter(o => {
    const matchExp = o.symbol.includes('-'+expiry+'-');
    const matchUnd = underlying
      ? o.symbol.startsWith(underlying) : true;
    return matchExp && matchUnd;
  });
  let cV=0,pV=0,cOi=0,pOi=0,cC=0,pC=0;
  for (const o of filtered) {
    const isCall = o.side==='CALL'
      || o.symbol.includes('-C');
    const vol = parseFloat(o.volume || 0);
    const oi = parseFloat(o.openInterest||o.oi||0);
    if (isCall) { cV+=vol; cOi+=oi; cC++; }
    else { pV+=vol; pOi+=oi; pC++; }
  }
  const volR = cV>0 ? pV/cV : null;
  const oiR = cOi>0 ? pOi/cOi : null;
  return { expiry, underlying: underlying||'ALL',
    volumeRatio: volR?parseFloat(volR.toFixed(3)):null,
    oiRatio: oiR?parseFloat(oiR.toFixed(3)):null,
    callVolume:cV, putVolume:pV,
    callOi:cOi, putOi:pOi,
    callCount:cC, putCount:pC };
}

function getSentiment(ratio) {
  if (ratio===null)
    return { sentiment:'UNKNOWN', sentimentStrength:0 };
  if (ratio > 1.5) return { sentiment:'STRONG_BEARISH',
    sentimentStrength: Math.min((ratio-1)*50, 100) };
  if (ratio > 1.0) return { sentiment:'BEARISH',
    sentimentStrength: (ratio-0.5)*50 };
  if (ratio > 0.5) return { sentiment:'BULLISH',
    sentimentStrength: (1-ratio)*50 };
  return { sentiment:'STRONG_BULLISH',
    sentimentStrength: Math.min((1-ratio)*100, 100) };
}

function analyzeAllPCRatios(optionsData, underlying) {
  const expiries = [...new Set(
    optionsData.filter(o => underlying
      ? o.symbol.startsWith(underlying) : true)
      .map(o => o.symbol.split('-')[1])
  )].sort();
  const results = expiries.map(exp => {
    const r = calcPCRatio(optionsData, exp, underlying);
    return { ...r, ...getSentiment(r.volumeRatio) };
  });
  const avgArr = results
    .filter(r => r.volumeRatio!==null)
    .map(r => r.volumeRatio);
  const avg = avgArr.length > 0
    ? avgArr.reduce((a,b)=>a+b,0)/avgArr.length : 0;
  for (const r of results) {
    r.isAnomaly = false; r.anomalyType = null;
    if (r.volumeRatio!==null && avg>0
      && Math.abs(r.volumeRatio-avg) > avg*0.5) {
      r.isAnomaly = true;
      r.anomalyType = r.volumeRatio > avg
        ? 'EXCESS_PUTS' : 'EXCESS_CALLS';
    }
  }
  return { underlying: underlying||'ALL',
    avgVolumeRatio: avg?parseFloat(avg.toFixed(3)):null,
    expiries: results,
    anomalies: results.filter(r => r.isAnomaly) };
}
module.exports = { analyzeAllPCRatios };

📜 Git History

163bb5dfeat: migrate to options-screener-v2 folder to isolate deployment4 months ago
Show last diff
Loading...