@neabyte/candlestick-cli
Version:
Beautiful terminal candlestick charts with real-time trading data. Create stunning ASCII art charts directly in your terminal with support for live market data, custom colors, and professional trading visualization.
1 lines • 5.02 kB
JavaScript
import{writeFileSync,existsSync,mkdirSync}from"fs";import{extname,dirname}from"path";import{Chart}from"../chart/chart.js";import{ConfigurationError}from"../types/errors.js";import{CONSTANTS}from"../constants.js";const UNKNOWN_ERROR="Unknown error";export async function exportToText(e,t,a=!1){validateOutputPath(t),ensureDirectoryExists(t);const r=await e.render(),o=a?r:stripAnsiCodes(r);try{writeFileSync(t,o,"utf8")}catch(e){const t=e instanceof Error?e.message:UNKNOWN_ERROR;throw new ConfigurationError(`Failed to write text file: ${t}`,"outputPath")}}export async function exportToImage(e,t){validateExportOptions(t),ensureDirectoryExists(t.outputPath);const a=e.chartData.mainCandleSet.candles,r=new Chart(a,{title:e.infoBar.name||"Live Chart"});r.setBearColor(...e.renderer.bearishColor),r.setBullColor(...e.renderer.bullishColor),r.setVolumePaneEnabled(e.volumePane.enabled),r.setVolumePaneHeight(e.volumePane.height),e.volumePane.enabled&&(r.setVolBearColor(...e.volumePane.bearishColor),r.setVolBullColor(...e.volumePane.bullishColor)),r.setMargins(2,2,1,0),r.setScalingMode("fit"),r.fitToData(),await renderChartToCanvas(r,t)}function validateExportOptions(e){if(!e.outputPath)throw new ConfigurationError("Output path is required for export","outputPath")}function validateOutputPath(e){const t=extname(e).toLowerCase();if(![".txt",".png"].includes(t))throw new ConfigurationError(`Unsupported file extension: ${t}. Use .txt or .png`,"outputPath")}function ensureDirectoryExists(e){const t=dirname(e);if(!existsSync(t))try{mkdirSync(t,{recursive:!0})}catch(e){const t=e instanceof Error?e.message:UNKNOWN_ERROR;throw new ConfigurationError(`Failed to create directory: ${t}`,"outputPath")}}function stripAnsiCodes(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}async function renderChartToCanvas(e,t){const{createCanvas:a}=await import("canvas"),{writeFileSync:r}=await import("fs"),o=t.background||"dark",n="light"===o?"#ffffff":"#000000",i=t.scale||1,{candles:l}=e.chartData.visibleCandleSet;if(0===l.length)throw new ConfigurationError("No candles to render","chart");const{maxPrice:c,priceRange:s}=calculatePriceRange(l),{finalWidth:h,finalHeight:u,actualScale:f}=calculateCanvasDimensions(i,e),{chartAreaX:d,chartAreaY:m,chartAreaWidthScaled:p,chartAreaHeightScaled:C}=calculateChartArea(f,e),{candleWidth:g,candleSpacing:x}=calculateCandleDimensions(p,l.length),P=a(h,u),w=P.getContext("2d");if(w.fillStyle=n,w.fillRect(0,0,h,u),drawPriceLabels(w,c,s,m,C,f,o),drawCandles(w,l,e,d,m,C,g,x,c,s),e.volumePane.enabled){drawVolumePane(w,l,e,d,m+C+20,p,16*e.volumePane.height*i,g,x)}const b=P.toBuffer("image/png");r(t.outputPath,b)}function calculatePriceRange(e){const t=e.flatMap(({high:e,low:t})=>[e,t]),a=Math.min(...t),r=Math.max(...t);return{maxPrice:r,priceRange:r-a}}function calculateCanvasDimensions(e,t){const a=8*(t.chartData.width||120)*e+80+120,r=16*(t.chartData.height||30)*e+(t.volumePane.enabled?16*t.volumePane.height*e:0)+80+60,o=Math.min(16384,3e3);let n=a,i=r;if(a>o||r>o){const e=Math.min(o/a,o/r);n=Math.floor(a*e),i=Math.floor(r*e)}n=Math.max(n,400),i=Math.max(i,300);const l=n/a,c=i/r;return{finalWidth:n,finalHeight:i,actualScale:Math.min(l,c)}}function calculateChartArea(e,t){return{chartAreaX:160*e,chartAreaY:60*e,chartAreaWidthScaled:8*(t.chartData.width||120)*e,chartAreaHeightScaled:16*(t.chartData.height||30)*e}}function calculateCandleDimensions(e,t){return{candleWidth:Math.max(3,e/t*.95),candleSpacing:e/t}}function drawPriceLabels(e,t,a,r,o,n,i){const l=t-a,c=Math.round(o/(16*n));for(let s=1;s<=c;s++){let h;h=s===c?t:1===s?l:l+(s-1)*a/(c-1);const u=r+(t-h)/a*o;e.fillStyle="light"===i?"#666666":"#999999",e.font=16*n+"px monospace",e.fillText(h.toFixed(CONSTANTS.DEC_PRECISION),40*n,u-10*n)}}function drawCandles(e,t,a,r,o,n,i,l,c,s){t.forEach((t,h)=>{const u=r+h*l+(l-i)/2,f=o+(c-t.high)/s*n,d=o+(c-t.low)/s*n,m=o+(c-t.open)/s*n,p=o+(c-t.close)/s*n,C=t.close>t.open?`rgb(${a.renderer.bullishColor[0]}, ${a.renderer.bullishColor[1]}, ${a.renderer.bullishColor[2]})`:`rgb(${a.renderer.bearishColor[0]}, ${a.renderer.bearishColor[1]}, ${a.renderer.bearishColor[2]})`;e.strokeStyle=C,e.lineWidth=Math.max(1,1),e.beginPath(),e.moveTo(u+i/2,f),e.lineTo(u+i/2,d),e.stroke();const g=Math.min(m,p),x=Math.abs(p-m),P=Math.max(2,1);e.fillStyle=C,e.fillRect(u,g,i,Math.max(x,P))})}function drawVolumePane(e,t,a,r,o,n,i,l,c){const s=Math.max(...t.map(e=>e.volume||0));t.forEach((t,n)=>{if(!t.volume)return;const h=r+n*c+(c-l)/2,u=t.volume/s*i,f=o+i-u,d=t.close>t.open?`rgb(${a.volumePane.bullishColor[0]}, ${a.volumePane.bullishColor[1]}, ${a.volumePane.bullishColor[2]})`:`rgb(${a.volumePane.bearishColor[0]}, ${a.volumePane.bearishColor[1]}, ${a.volumePane.bearishColor[2]})`;e.fillStyle=d,e.fillRect(h,f,l,u)})}export async function exportChart(e,t,a={}){const r=extname(t).toLowerCase();if(".txt"===r)exportToText(e,t);else{if(".png"!==r)throw new ConfigurationError(`Unsupported file extension: ${r}. Use .txt or .png`,"outputPath");await exportToImage(e,{...a,outputPath:t})}}