I have some code.
Please remove the LOD optimization and simply use only the line chart optimization.
These optimizations are conditional on different zoom levels.
When zoomed in, the candle bodies should be shown, when zoomed out a medium amount just the candle wicks should be shown, when zoomed out a high amount just the line chart should be shown.
These optimizations are to deal with very high numbers of candlesticks

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Three.js Candlesticks with LOD Optimization</title>
  <style>
    body { margin: 0; }
    canvas { display: block; }
  </style>
</head>
<body>
  <script type="module">

import * as THREE from './three.module.js';

// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);

// Camera setup
const aspectRatio = window.innerWidth / window.innerHeight;
const cameraSize = 200;
const camera = new THREE.OrthographicCamera(
  -cameraSize * aspectRatio,
  cameraSize * aspectRatio,
  cameraSize,
  -cameraSize,
  1,
  1000
);
camera.position.set(0, 0, 500);

// Renderer setup
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Function to generate random OHLC data
function generateOHLCData(count) {
  const data = new Array(count);
  let open = 50; // Starting price
  for (let i = 0; i < count; i++) {
    const high = open + Math.random() * 10;
    const low = open - Math.random() * 10;
    const close = low + Math.random() * (high - low);
    data[i] = { open, high, low, close };
    open = close; // Next open starts where the last close was
  }
  return data;
}

// Convert RGB to normalized color values (0 to 1)
function rgbToNormalized(r, g, b) {
  return [r / 255, g / 255, b / 255];
}

// Define colors in normalized RGB
const bullishColor = rgbToNormalized(0, 0.7 * 255, 0); // Green
const bearishColor = rgbToNormalized(0.7 * 255, 0, 0); // Red

// Generate data (Adjust the count to 5 million)
const ohlcData = generateOHLCData(5000000);

// Variables for candlestick rendering
let instancedMesh, wickLineSegments, aggregatedLine;
let instanceMatrices, instanceColors, wickVertices, wickColors;
let geometryBar, wickGeometry, aggregatedGeometry;
const maxVisibleCandlesticks = 2000; // Maximum number of visible candlesticks to render
const barWidth = 1;
const candlestickSpacing = barWidth * 1.5;
const totalCandlesticks = ohlcData.length;
const halfCandlesticks = totalCandlesticks / 2;

// Create instanced candlesticks with dynamic visibility
function createInstancedCandlesticks() {
  // Materials
  const material = new THREE.MeshBasicMaterial({ vertexColors: true });

  // Geometries for the candlestick body
  geometryBar = new THREE.BoxGeometry(barWidth, 1, 0);
  
  // Create instanced mesh with capacity for maximum visible candlesticks
  instancedMesh = new THREE.InstancedMesh(geometryBar, material, maxVisibleCandlesticks);
  instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);

  // Prepare instance matrices and colors
  instanceMatrices = new Float32Array(maxVisibleCandlesticks * 16); // 4x4 matrices
  instanceColors = new Float32Array(maxVisibleCandlesticks * 3); // RGB per instance

  // Set the per-instance color attribute on the geometry
  geometryBar.setAttribute('instanceColor', new THREE.InstancedBufferAttribute(instanceColors, 3));
  geometryBar.attributes.instanceColor.setUsage(THREE.DynamicDrawUsage);

  // Prepare wick geometry
  wickGeometry = new THREE.BufferGeometry();
  wickVertices = new Float32Array(maxVisibleCandlesticks * 6); // 2 points per wick, 3 components per point
  wickColors = new Float32Array(maxVisibleCandlesticks * 6); // 2 points per wick, 3 components per point
  wickGeometry.setAttribute('position', new THREE.BufferAttribute(wickVertices, 3));
  wickGeometry.setAttribute('color', new THREE.BufferAttribute(wickColors, 3)); // Add color to wicks
  const wickMaterial = new THREE.LineBasicMaterial({ vertexColors: true }); // Enable vertex colors
  wickLineSegments = new THREE.LineSegments(wickGeometry, wickMaterial);
  scene.add(wickLineSegments);

  // Add the instanced mesh to the scene
  scene.add(instancedMesh);

  // Prepare aggregated line geometry and material
  aggregatedGeometry = new THREE.BufferGeometry();
  const aggregatedMaterial = new THREE.LineBasicMaterial({ color: 0xCCCCCC }); // Light gray color
  aggregatedLine = new THREE.Line(aggregatedGeometry, aggregatedMaterial);
  scene.add(aggregatedLine);
}

// Create the candlesticks
createInstancedCandlesticks();

// Zoom logic
const zoomSpeed = 0.8;
let visibilityUpdateTimeout; // Timeout variable for visibility updates

function onWheel(event) {
  event.preventDefault();

  const rect = renderer.domElement.getBoundingClientRect();
  const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;

  const mouseWorld = new THREE.Vector3(mouseX, mouseY, 0).unproject(camera);
  const zoomFactor = 1 - event.deltaY * 0.001 * zoomSpeed;

  const ctrlPressed = event.ctrlKey;
  const shiftPressed = event.shiftKey;

  if (ctrlPressed) {
    camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
  } else if (shiftPressed) {
    camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
    camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
  } else {
    camera.left = (camera.left - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.right = (camera.right - mouseWorld.x) * zoomFactor + mouseWorld.x;
    camera.top = (camera.top - mouseWorld.y) * zoomFactor + mouseWorld.y;
    camera.bottom = (camera.bottom - mouseWorld.y) * zoomFactor + mouseWorld.y;
  }

  camera.updateProjectionMatrix();

  // Clear previous timeout if it exists
  clearTimeout(visibilityUpdateTimeout);

  // Set a new timeout to update visibility after a short delay
  visibilityUpdateTimeout = setTimeout(updateCandlestickVisibility, 50); // Adjust the delay as needed
}

// Function to update candlestick visibility based on zoom level
function updateCandlestickVisibility() {
  const totalCandlesticks = ohlcData.length;

  // Compute visible index range, adjusting for centering
  const firstVisibleIndex = Math.max(0, Math.floor((camera.left / candlestickSpacing) + halfCandlesticks));
  const lastVisibleIndex = Math.min(totalCandlesticks - 1, Math.ceil((camera.right / candlestickSpacing) + halfCandlesticks));

  let visibleCandlestickCount = lastVisibleIndex - firstVisibleIndex + 1;

  // Adjust the threshold for visibility based on zoom level
  const bodyThreshold = 700;
  const wickThreshold = 5000; // New wick threshold
  const aggregateThreshold = 10000; // Threshold to switch to aggregated representation

  if (visibleCandlestickCount > aggregateThreshold) {
    // Hide individual candlesticks and wicks
    instancedMesh.visible = false;
    wickLineSegments.visible = false;

    // Show aggregated line
    aggregatedLine.visible = true;

    // Generate aggregated data
    const maxPoints = 2000; // Limit the number of points in the aggregated line
    const step = Math.ceil(visibleCandlestickCount / maxPoints);

    const positions = [];
    for (let i = firstVisibleIndex; i <= lastVisibleIndex; i += step) {
      const index = i;
      const d = ohlcData[index];
      const currentX = (index - halfCandlesticks) * candlestickSpacing;
      const y = (d.high + d.low) / 2; // You can choose to plot close or an average
      positions.push(currentX, y, 0);
    }

    // Update aggregated line geometry
    const positionAttribute = new THREE.Float32BufferAttribute(positions, 3);
    aggregatedGeometry.setAttribute('position', positionAttribute);
    aggregatedGeometry.computeBoundingSphere();

  } else if (visibleCandlestickCount > wickThreshold) {
    // Hide candlestick bodies
    instancedMesh.visible = false;
    // Show wicks
    wickLineSegments.visible = true;
    // Hide aggregated line
    aggregatedLine.visible = false;

    // If visibleCandlestickCount is greater than maxVisibleCandlesticks, sample the data
    let step = 1;
    if (visibleCandlestickCount > maxVisibleCandlesticks) {
      step = Math.ceil(visibleCandlestickCount / maxVisibleCandlesticks);
    }

    let renderCount = 0;

    for (let i = firstVisibleIndex; i <= lastVisibleIndex; i += step) {
      const index = i;
      const d = ohlcData[index];
      const isBullish = d.close >= d.open;
      const currentX = (index - halfCandlesticks) * candlestickSpacing;

      // Wick positions
      const wickIndex = renderCount * 6;
      wickVertices[wickIndex] = currentX;
      wickVertices[wickIndex + 1] = d.low;
      wickVertices[wickIndex + 2] = -0.5;
      wickVertices[wickIndex + 3] = currentX;
      wickVertices[wickIndex + 4] = d.high;
      wickVertices[wickIndex + 5] = -0.5;

      // Wick colors
      const color = isBullish ? bullishColor : bearishColor;
      wickColors.set([...color, ...color], wickIndex);

      renderCount++;
      if (renderCount >= maxVisibleCandlesticks) break;
    }

    // Update wick geometry
    wickGeometry.setDrawRange(0, renderCount * 2); // 2 vertices per line
    wickGeometry.attributes.position.array.set(wickVertices.subarray(0, renderCount * 6));
    wickGeometry.attributes.position.needsUpdate = true;
    wickGeometry.attributes.color.array.set(wickColors.subarray(0, renderCount * 6));
    wickGeometry.attributes.color.needsUpdate = true;
    wickGeometry.computeBoundingSphere();

  } else {
    // Show candlestick bodies and wicks
    instancedMesh.visible = true;
    wickLineSegments.visible = true;
    // Hide aggregated line
    aggregatedLine.visible = false;

    // If visibleCandlestickCount is greater than maxVisibleCandlesticks, sample the data
    let step = 1;
    if (visibleCandlestickCount > maxVisibleCandlesticks) {
      step = Math.ceil(visibleCandlestickCount / maxVisibleCandlesticks);
    }

    let renderCount = 0;
    const dummy = new THREE.Object3D();

    for (let i = firstVisibleIndex; i <= lastVisibleIndex; i += step) {
      const index = i;
      const d = ohlcData[index];
      const isBullish = d.close >= d.open;
      const height = Math.abs(d.close - d.open);
      const y = (d.close + d.open) / 2;
      const currentX = (index - halfCandlesticks) * candlestickSpacing;

      // Set the position and scale for the body
      dummy.position.set(currentX, y, 0);
      dummy.scale.set(1, height, 1); // Width is set to 1; barWidth is already accounted for in scaling
      dummy.updateMatrix();

      // Copy the matrix to the instanceMatrices array
      dummy.matrix.toArray(instanceMatrices, renderCount * 16);

      // Set color for each instance
      const color = isBullish ? bullishColor : bearishColor;
      instanceColors.set(color, renderCount * 3);

      // Wick positions
      const wickIndex = renderCount * 6;
      wickVertices[wickIndex] = currentX;
      wickVertices[wickIndex + 1] = d.low;
      wickVertices[wickIndex + 2] = -0.5;
      wickVertices[wickIndex + 3] = currentX;
      wickVertices[wickIndex + 4] = d.high;
      wickVertices[wickIndex + 5] = -0.5;

      // Wick colors
      wickColors.set([...color, ...color], wickIndex);

      renderCount++;
      if (renderCount >= maxVisibleCandlesticks) break;
    }

    // Update instancedMesh attributes
    instancedMesh.count = renderCount;

    // Update instance matrices and colors
    instancedMesh.instanceMatrix.array.set(instanceMatrices.subarray(0, renderCount * 16));
    instancedMesh.instanceMatrix.needsUpdate = true;

    geometryBar.attributes.instanceColor.array.set(instanceColors.subarray(0, renderCount * 3));
    geometryBar.attributes.instanceColor.needsUpdate = true;

    // Update wick geometry
    wickGeometry.setDrawRange(0, renderCount * 2); // 2 vertices per line
    wickGeometry.attributes.position.array.set(wickVertices.subarray(0, renderCount * 6));
    wickGeometry.attributes.position.needsUpdate = true;
    wickGeometry.attributes.color.array.set(wickColors.subarray(0, renderCount * 6));
    wickGeometry.attributes.color.needsUpdate = true;
    wickGeometry.computeBoundingSphere();
  }
}

// Initial visibility update
updateCandlestickVisibility();

// Prevent default browser behavior for wheel scrolling with Control or Shift key
window.addEventListener('wheel', onWheel, { passive: false });

// Prevent default context menu on right-click
window.addEventListener('contextmenu', (event) => {
  event.preventDefault();
});

// Render loop
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

// Handle window resize
function onWindowResize() {
  const aspect = window.innerWidth / window.innerHeight;
  camera.left = -cameraSize * aspect;
  camera.right = cameraSize * aspect;
  camera.top = cameraSize;
  camera.bottom = -cameraSize;
  camera.updateProjectionMatrix();
  updateCandlestickVisibility();
}
window.addEventListener('resize', onWindowResize);

// Start animation
animate();

// Panning functionality
let isPanning = false; // Track panning state
let isDraggingEndpoint = false; // Track endpoint dragging state
let selectedEndpointIndex = null; // Index of the selected endpoint (0 or 1)
const previousMousePosition = new THREE.Vector2(); // Store previous mouse position

// Raycaster for detecting line interaction
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// **Interactive line code**
// Define line geometry and material
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
  new THREE.Vector3(-50, 0, 0),
  new THREE.Vector3(50, 0, 0)
]);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.frustumCulled = false; // Prevent the line from being culled
scene.add(line);

// Set the distance for grabbing the line (in world units)
let distanceForGrab = 2; // You can adjust this value to make grabbing easier

// Set the raycaster's line precision to `distanceForGrab`
raycaster.params.Line.threshold = distanceForGrab; // For Three.js r127 and above

// Predefine vectors to avoid excessive allocations
const mouseWorld = new THREE.Vector3();
const deltaWorld = new THREE.Vector3();
const currentMouse = new THREE.Vector2();
const previousMouseWorld = new THREE.Vector3();

// Mouse down event
function onMouseDown(event) {
  if (event.button === 2) { // Right mouse button for panning
    isPanning = true;
    previousMousePosition.set(event.clientX, event.clientY);
  } else if (event.button === 0) { // Left mouse button for line interaction
    // Prepare raycaster
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = - ((event.clientY - rect.top) / rect.height) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    // Check intersection with the line
    const intersects = raycaster.intersectObject(line, true);

    if (intersects.length > 0) {
      // Clicked on the line
      // Determine which endpoint is closer to the mouse
      const positions = lineGeometry.attributes.position.array;
      const lineStart = new THREE.Vector3(positions[0], positions[1], positions[2]);
      const lineEnd = new THREE.Vector3(positions[3], positions[4], positions[5]);

      mouseWorld.copy(intersects[0].point);

      const distanceToStart = mouseWorld.distanceTo(lineStart);
      const distanceToEnd = mouseWorld.distanceTo(lineEnd);

      selectedEndpointIndex = (distanceToStart < distanceToEnd) ? 0 : 1;

      isDraggingEndpoint = true;
      // Set previous mouse position in NDC
      previousMousePosition.set(mouse.x, mouse.y);
    }
  }
}

// Mouse move event
function onMouseMove(event) {
  if (isPanning) {
    // Panning code
    const deltaX = event.clientX - previousMousePosition.x;
    const deltaY = event.clientY - previousMousePosition.y;

    const zoomFactorX = camera.right - camera.left;
    const zoomFactorY = camera.top - camera.bottom;

    const panX = (deltaX / window.innerWidth) * zoomFactorX;
    const panY = (deltaY / window.innerHeight) * zoomFactorY;

    camera.left -= panX;
    camera.right -= panX;
    camera.top += panY;
    camera.bottom += panY;
    camera.updateProjectionMatrix();

    previousMousePosition.set(event.clientX, event.clientY); // Update previous position

    // Update candlestick visibility after panning
    clearTimeout(visibilityUpdateTimeout);
    visibilityUpdateTimeout = setTimeout(updateCandlestickVisibility, 50);
} else if (isDraggingEndpoint) {
    // Get current mouse position in normalized device coordinates (NDC)
    const rect = renderer.domElement.getBoundingClientRect();
    const currentMouse = new THREE.Vector2(
      ((event.clientX - rect.left) / rect.width) * 2 - 1,
      - ((event.clientY - rect.top) / rect.height) * 2 + 1
    );

    // Convert previous and current mouse positions to world coordinates
    const previousMouseWorld = new THREE.Vector3(previousMousePosition.x, previousMousePosition.y, 0).unproject(camera);
    const currentMouseWorld = new THREE.Vector3(currentMouse.x, currentMouse.y, 0).unproject(camera);

    // Compute delta in world coordinates
    const deltaWorld = currentMouseWorld.clone().sub(previousMouseWorld);

    // Update the selected endpoint
    const positions = lineGeometry.attributes.position.array;
    const index = selectedEndpointIndex * 3; // 0 or 3

    positions[index] += deltaWorld.x;
    positions[index + 1] += deltaWorld.y;

    lineGeometry.attributes.position.needsUpdate = true;
    lineGeometry.computeBoundingSphere(); // Update bounding sphere for frustum culling and raycasting

    previousMousePosition.copy(currentMouse); // Update previous mouse position
  }
}

// Mouse up event
function onMouseUp(event) {
  if (event.button === 2) { // Right mouse button
    isPanning = false;
  } else if (event.button === 0) { // Left mouse button
    isDraggingEndpoint = false;
  }
}

// Add event listeners for mouse actions
window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);

  </script>
</body>
</html>