QuietSwing
  • Themes
  • Market Movers
  • Stocks
  • Ideas

Stock Analysis

QUIETSWING / SINGLE STOCK

Stock analysis

Inspect recent price acceptance and how volume at price migrates between sessions.

Volume profile
Profiles use 15-minute trading data. Opening and closing bars can be included.
viewof stockTicker = Inputs.text({label: "Ticker", value: stockLink.ticker ?? "AAPL", placeholder: "AAPL", required: true, width: 110})
viewof stockStartDate = Inputs.date({label: "Start date", min: stockDateBounds.min, max: stockDateBounds.max, value: stockDateDefaults.start, required: true})
viewof stockEndDate = Inputs.date({label: "End date", min: stockDateBounds.min, max: stockDateBounds.max, value: stockDateDefaults.end, required: true})
viewof stockLoadRequest = Inputs.button("Load profile", {
  value: null,
  reduce: () => ({
    ticker: stockTicker.trim().toUpperCase(),
    start: dateKey(stockStartDate),
    end: dateKey(stockEndDate)
  })
})
viewof includeEdgeBars = Inputs.toggle({label: "Include opening/closing edge bars", value: false})
function localDateKey(value) {
  const date = new Date(value)
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, "0")
  const day = String(date.getDate()).padStart(2, "0")
  return `${year}-${month}-${day}`
}

function shiftDate(value, days) {
  const date = new Date(`${value}T12:00:00`)
  date.setDate(date.getDate() + days)
  return localDateKey(date)
}

function dateKey(value) {
  if (value == null) return null
  if (typeof value === "string") return value.slice(0, 10)
  return value.toISOString().slice(0, 10)
}

stockToday = localDateKey(new Date())
// Links from Movers and Themes pass ?ticker=&start=&end= to open a profile directly.
stockLink = (() => {
  const params = new URLSearchParams(location.search)
  const isDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value ?? "")
  const end = isDate(params.get("end")) ? params.get("end") : stockToday
  const start = isDate(params.get("start")) ? params.get("start") : shiftDate(end, -7)
  // Profiles cover at most 60 calendar days; keep the latest part of longer periods.
  const earliest = shiftDate(end, -59)
  return {ticker: params.get("ticker")?.trim().toUpperCase() || null, start: start < earliest ? earliest : start, end}
})()
stockDateDefaults = ({start: stockLink.start, end: stockLink.end})
stockDateBounds = ({min: [shiftDate(stockToday, -20), stockLink.start].sort()[0], max: stockToday})
stockLinkRequest = stockLink.ticker ? stockLink : null

function formatPrice(value) {
  return `$${value.toFixed(2)}`
}

function formatPercent(value) {
  return `${value >= 0 ? "+" : ""}${value.toFixed(1)}%`
}

function formatVolume(value) {
  return new Intl.NumberFormat("en", {notation: "compact", maximumFractionDigits: 1}).format(value)
}

// Profiles are computed on the server; the page only renders them.
async function loadStockProfile(request, includeEdges) {
  if (!request) return null
  if (!request.ticker) return {error: "Enter a ticker."}
  if (!request.start || !request.end || request.start > request.end) {
    return {error: "Choose a start date on or before the end date."}
  }
  const query = new URLSearchParams({ticker: request.ticker, start: request.start, end: request.end, edges: includeEdges ? "1" : "0"})
  try {
    const response = await fetch(`/api/stock-profile?${query}`)
    const result = await response.json()
    return response.ok ? result : {error: result.error || "Could not load intraday data."}
  } catch {
    return {error: "Could not load intraday data. Please try again shortly."}
  }
}
stockProfile = await loadStockProfile(stockLoadRequest ?? stockLinkRequest, includeEdgeBars)
(() => {
  const result = stockProfile
  if (!stockLoadRequest && !stockLinkRequest) return html`<div class="stock-prompt">Choose a ticker and period, then load its recent profile.</div>`
  if (result?.error) return html`<div class="empty-state">${result.error}</div>`
  if (!result) return ""
  return html`
    <div class="period-line"><strong>${result.ticker}</strong> · ${result.actualStart} → ${result.actualEnd} · ${result.sessionSummaries.length} sessions · ${result.profileMode} profile · ${result.barCount} bars</div>
    ${result.splits.length ? html`<div class="stock-warning">The selected period includes a split event. Treat cross-split profile levels cautiously.</div>` : ""}
    <div class="stock-metric-grid">
      <div><span>Latest close</span><strong>${formatPrice(result.currentClose)}</strong></div>
      <div><span>Profile POC</span><strong>${formatPrice(result.levels.poc)}</strong></div>
      <div><span>Value area</span><strong>${formatPrice(result.levels.val)}–${formatPrice(result.levels.vah)}</strong></div>
      <div><span>Close vs POC</span><strong>${formatPercent(result.closeVsPoc)}</strong></div>
      <div><span>Profile volume</span><strong>${formatVolume(result.profileVolume)}</strong></div>
      <div><span>Edge volume share</span><strong>${result.edgeShare.toFixed(1)}%</strong></div>
    </div>
  `
})()
VOLUME AT PRICE

Selected-period profile

Selected profile mode · estimated from bar ranges
Volume at price POC (busiest price) Value area edges Latest close

Bars show where shares changed hands over the period. Comparing the latest close with the POC and value area describes whether price sits inside or away from the period’s busiest range.

(() => {
  const result = stockProfile
  if (!result || result.error) return html``
  return Plot.plot({
    height: 470,
    marginLeft: 68,
    marginRight: 24,
    marginBottom: 48,
    style: {background: "transparent", fontSize: "11px"},
    x: {grid: true, label: "Selected-profile volume share (%)"},
    y: {grid: true, label: "Price ($)"},
    marks: [
      Plot.rectX(result.profile, {
        x1: 0,
        x2: "share",
        y1: "lower",
        y2: "upper",
        fill: "#7fa6d8",
        title: bin => `${formatPrice(bin.lower)}–${formatPrice(bin.upper)}\n${bin.share.toFixed(2)}% · ${formatVolume(bin.volume)}`
      }),
      Plot.ruleY([result.levels.val, result.levels.vah], {stroke: "#16835f", strokeDasharray: "4,4"}),
      Plot.ruleY([result.levels.poc], {stroke: "#3564b7", strokeWidth: 2}),
      Plot.ruleY([result.currentClose], {stroke: "#18212f", strokeWidth: 1.5})
    ]
  })
})()
MIGRATION

Session profiles

Period-comparable volume · value area, close and POC overlaid
Volume at price (darker = more) Daily value area Daily POC Daily close Close above value Inside Below

Reading left to right shows whether each day’s busiest prices and value area moved higher, lower or stayed in place, and where each day closed relative to its own range.

(() => {
  const result = stockProfile
  if (!result || result.error) return html``
  const maxPeriodShare = Math.max(...result.sessionProfiles.map(row => row.periodShare))
  const sessionCount = result.sessionSummaries.length
  const closeTitle = row => `${row.sessionDate}\nClose ${formatPrice(row.close)} · ${row.closeState}\nValue area ${formatPrice(row.val)}–${formatPrice(row.vah)}`
  return Plot.plot({
    height: 470,
    marginLeft: 68,
    marginRight: 24,
    marginBottom: 48,
    style: {background: "transparent", fontSize: "11px"},
    x: {
      type: "linear",
      domain: [-0.5, sessionCount - 0.5],
      label: null,
      ticks: Math.min(sessionCount, 8),
      tickFormat: value => {
        const index = Math.round(value)
        return Math.abs(value - index) < 1e-6 && result.sessionSummaries[index]
          ? result.sessionSummaries[index].sessionDate.slice(5)
          : ""
      }
    },
    y: {grid: true, label: "Price ($)"},
    color: {type: "sqrt", scheme: "Blues", domain: [0, maxPeriodShare], legend: true, label: "Selected-period volume (%)"},
    marks: [
      Plot.rect(result.sessionProfiles, {
        x1: "x1",
        x2: "x2",
        y1: "lower",
        y2: "upper",
        fill: "periodShare",
        title: row => `${row.sessionDate}\n${formatPrice(row.lower)}–${formatPrice(row.upper)}\nDaily share ${row.share.toFixed(2)}% · Period contribution ${row.periodShare.toFixed(2)}%`
      }),
      Plot.rect(result.sessionSummaries, {
        x1: row => row.sessionIndex - 0.43,
        x2: row => row.sessionIndex + 0.43,
        y1: "val",
        y2: "vah",
        fill: "none",
        stroke: "#16835f",
        strokeWidth: 1.4,
        title: row => `${row.sessionDate}\nValue area ${formatPrice(row.val)}–${formatPrice(row.vah)}`
      }),
      Plot.line(result.sessionSummaries, {x: "sessionIndex", y: "close", stroke: "#18212f", strokeWidth: 2}),
      Plot.dot(result.sessionSummaries.filter(row => row.closeState === "Inside value"), {x: "sessionIndex", y: "close", fill: "#18212f", r: 3.5, title: closeTitle}),
      Plot.dot(result.sessionSummaries.filter(row => row.closeState === "Above value"), {x: "sessionIndex", y: "close", fill: "#16835f", r: 4, title: closeTitle}),
      Plot.dot(result.sessionSummaries.filter(row => row.closeState === "Below value"), {x: "sessionIndex", y: "close", fill: "#c64f5e", r: 4, title: closeTitle}),
      Plot.line(result.sessionSummaries, {x: "sessionIndex", y: "poc", stroke: "#3564b7", strokeWidth: 2, strokeDasharray: "4,3"}),
      Plot.dot(result.sessionSummaries, {x: "sessionIndex", y: "poc", fill: "#3564b7", r: 2.5, title: row => `${row.sessionDate}\nPOC ${formatPrice(row.poc)}`})
    ]
  })
})()
Volume profiles show how much trading happened at each price level. POC (point of control) is the busiest price, and the value area is the range where most trading occurred. Session profiles show how that activity shifts from day to day; dots mark each day’s close: green above the value area, dark inside it, red below it. Core mode leaves out the most volatile opening and closing bars of each session.
 

For research and education only; not financial advice.