mirror of
https://github.com/prodigist/ReactiveNotes.git
synced 2026-07-22 12:30:26 +00:00
Remove unused files and dependencies
This commit is contained in:
parent
deab1a970c
commit
5e3a5b4914
37 changed files with 0 additions and 3227 deletions
24
lwc-plugin-custom-primitives/.gitignore
vendored
24
lwc-plugin-custom-primitives/.gitignore
vendored
|
|
@ -1,24 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
typings
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Custom Primitives - Lightweight Charts™ Plugin
|
||||
|
||||
Description of the Plugin.
|
||||
|
||||
- Developed for Lightweight Charts version: `v4.1.0`
|
||||
|
||||
## Running Locally
|
||||
|
||||
```shell
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit `localhost:5173` in the browser.
|
||||
|
||||
## Compiling
|
||||
|
||||
```shell
|
||||
npm run compile
|
||||
```
|
||||
|
||||
Check the output in the `dist` folder.
|
||||
|
||||
## Publishing To NPM
|
||||
|
||||
You can configure the contents of the package's `package.json` within the
|
||||
`compile.mjs` script.
|
||||
|
||||
Once you have compiled the plugin (see above section) then you can publish the
|
||||
package to NPM with these commands:
|
||||
|
||||
```shell
|
||||
cd dist
|
||||
npm publish
|
||||
```
|
||||
|
||||
Hint: append `--dry-run` to the end of the publish command to see the results of
|
||||
the publish command without actually uploading the package to NPM.
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
import { dirname, resolve } from 'node:path';
|
||||
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { build, defineConfig } from 'vite';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { generateDtsBundle } from 'dts-bundle-generator';
|
||||
|
||||
function buildPackageJson(packageName) {
|
||||
/*
|
||||
Define the contents of the package's package.json here.
|
||||
*/
|
||||
return {
|
||||
name: packageName,
|
||||
version: '1.0.0',
|
||||
keywords: ['lwc-plugin', 'lightweight-charts'],
|
||||
type: 'module',
|
||||
main: `./${packageName}.umd.cjs`,
|
||||
module: `./${packageName}.js`,
|
||||
types: `./${packageName}.d.ts`,
|
||||
exports: {
|
||||
import: {
|
||||
types: `./${packageName}.d.ts`,
|
||||
default: `./${packageName}.js`,
|
||||
},
|
||||
require: {
|
||||
types: `./${packageName}.d.cts`,
|
||||
default: `./${packageName}.umd.cjs`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const currentDir = dirname(__filename);
|
||||
|
||||
const pluginFileName = 'custom-primitives';
|
||||
const pluginFile = resolve(currentDir, 'src', `${pluginFileName}.ts`);
|
||||
|
||||
const pluginsToBuild = [
|
||||
{
|
||||
filepath: pluginFile,
|
||||
exportName: 'lwc-plugin-custom-primitives',
|
||||
name: 'CustomPrimitives',
|
||||
},
|
||||
];
|
||||
|
||||
const compiledFolder = resolve(currentDir, 'dist');
|
||||
if (!existsSync(compiledFolder)) {
|
||||
mkdirSync(compiledFolder);
|
||||
}
|
||||
|
||||
const buildConfig = ({
|
||||
filepath,
|
||||
name,
|
||||
exportName,
|
||||
formats = ['es', 'umd'],
|
||||
}) => {
|
||||
return defineConfig({
|
||||
publicDir: false,
|
||||
build: {
|
||||
outDir: `dist`,
|
||||
emptyOutDir: true,
|
||||
copyPublicDir: false,
|
||||
lib: {
|
||||
entry: filepath,
|
||||
name,
|
||||
formats,
|
||||
fileName: exportName,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['lightweight-charts', 'fancy-canvas'],
|
||||
output: {
|
||||
globals: {
|
||||
'lightweight-charts': 'LightweightCharts',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const startTime = Date.now().valueOf();
|
||||
console.log('⚡️ Starting');
|
||||
console.log('Bundling the plugin...');
|
||||
const promises = pluginsToBuild.map(file => {
|
||||
return build(buildConfig(file));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
console.log('Generating the package.json file...');
|
||||
pluginsToBuild.forEach(file => {
|
||||
const packagePath = resolve(compiledFolder, 'package.json');
|
||||
const content = JSON.stringify(
|
||||
buildPackageJson(file.exportName),
|
||||
undefined,
|
||||
4
|
||||
);
|
||||
writeFileSync(packagePath, content, { encoding: 'utf-8' });
|
||||
});
|
||||
console.log('Generating the typings files...');
|
||||
pluginsToBuild.forEach(file => {
|
||||
try {
|
||||
const esModuleTyping = generateDtsBundle([
|
||||
{
|
||||
filePath: `./typings/${pluginFileName}.d.ts`,
|
||||
},
|
||||
]);
|
||||
const typingFilePath = resolve(compiledFolder, `${file.exportName}.d.ts`);
|
||||
writeFileSync(typingFilePath, esModuleTyping.join('\n'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
copyFileSync(typingFilePath, resolve(compiledFolder, `${file.exportName}.d.cts`));
|
||||
} catch (e) {
|
||||
console.error('Error generating typings for: ', file.exportName);
|
||||
}
|
||||
});
|
||||
const endTime = Date.now().valueOf();
|
||||
console.log(`🎉 Done (${endTime - startTime}ms)`);
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- redirect to example page -->
|
||||
<meta http-equiv="refresh" content="0; URL=src/example/" />
|
||||
</head>
|
||||
</html>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"name": "lwc-plugin-custom-primitives",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --config src/vite.config.js",
|
||||
"compile": "tsc && node compile.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"dts-bundle-generator": "^8.0.1",
|
||||
"fancy-canvas": "^2.1.0",
|
||||
"lightweight-charts": "^4.1.0-rc2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { CanvasRenderingTarget2D } from 'fancy-canvas';
|
||||
import { ISeriesPrimitivePaneRenderer } from 'lightweight-charts';
|
||||
import { positionsBox } from './helpers/dimensions/positions';
|
||||
|
||||
export class CustomPrimitivesAxisPaneRenderer implements ISeriesPrimitivePaneRenderer {
|
||||
_p1: number | null;
|
||||
_p2: number | null;
|
||||
_fillColor: string;
|
||||
_vertical: boolean = false;
|
||||
|
||||
constructor(
|
||||
p1: number | null,
|
||||
p2: number | null,
|
||||
fillColor: string,
|
||||
vertical: boolean
|
||||
) {
|
||||
this._p1 = p1;
|
||||
this._p2 = p2;
|
||||
this._fillColor = fillColor;
|
||||
this._vertical = vertical;
|
||||
}
|
||||
|
||||
draw(target: CanvasRenderingTarget2D) {
|
||||
target.useBitmapCoordinateSpace(scope => {
|
||||
if (this._p1 === null || this._p2 === null) return;
|
||||
const ctx = scope.context;
|
||||
ctx.globalAlpha = 0.5;
|
||||
const positions = positionsBox(
|
||||
this._p1,
|
||||
this._p2,
|
||||
this._vertical ? scope.verticalPixelRatio : scope.horizontalPixelRatio
|
||||
);
|
||||
ctx.fillStyle = this._fillColor;
|
||||
if (this._vertical) {
|
||||
ctx.fillRect(0, positions.position, 15, positions.length);
|
||||
} else {
|
||||
ctx.fillRect(positions.position, 0, positions.length, 15);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import {
|
||||
Coordinate,
|
||||
ISeriesPrimitivePaneView,
|
||||
SeriesPrimitivePaneViewZOrder,
|
||||
} from 'lightweight-charts';
|
||||
import { CustomPrimitivesAxisPaneRenderer } from './axis-pane-renderer';
|
||||
import { CustomPrimitivesDataSource } from './data-source';
|
||||
|
||||
abstract class CustomPrimitivesAxisPaneView implements ISeriesPrimitivePaneView {
|
||||
_source: CustomPrimitivesDataSource;
|
||||
_p1: number | null = null;
|
||||
_p2: number | null = null;
|
||||
_vertical: boolean = false;
|
||||
|
||||
constructor(source: CustomPrimitivesDataSource, vertical: boolean) {
|
||||
this._source = source;
|
||||
this._vertical = vertical;
|
||||
}
|
||||
|
||||
abstract getPoints(): [Coordinate | null, Coordinate | null];
|
||||
|
||||
update() {
|
||||
[this._p1, this._p2] = this.getPoints();
|
||||
}
|
||||
|
||||
renderer() {
|
||||
return new CustomPrimitivesAxisPaneRenderer(
|
||||
this._p1,
|
||||
this._p2,
|
||||
this._source.options.fillColor,
|
||||
this._vertical
|
||||
);
|
||||
}
|
||||
zOrder(): SeriesPrimitivePaneViewZOrder {
|
||||
return 'bottom';
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomPrimitivesPriceAxisPaneView extends CustomPrimitivesAxisPaneView {
|
||||
getPoints(): [Coordinate | null, Coordinate | null] {
|
||||
const series = this._source.series;
|
||||
const y1 = series.priceToCoordinate(this._source.p1.price);
|
||||
const y2 = series.priceToCoordinate(this._source.p2.price);
|
||||
return [y1, y2];
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomPrimitivesTimeAxisPaneView extends CustomPrimitivesAxisPaneView {
|
||||
getPoints(): [Coordinate | null, Coordinate | null] {
|
||||
const timeScale = this._source.chart.timeScale();
|
||||
const x1 = timeScale.timeToCoordinate(this._source.p1.time);
|
||||
const x2 = timeScale.timeToCoordinate(this._source.p2.time);
|
||||
return [x1, x2];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { Coordinate, ISeriesPrimitiveAxisView } from 'lightweight-charts';
|
||||
import { Point, CustomPrimitivesDataSource } from './data-source';
|
||||
|
||||
abstract class CustomPrimitivesAxisView implements ISeriesPrimitiveAxisView {
|
||||
_source: CustomPrimitivesDataSource;
|
||||
_p: Point;
|
||||
_pos: Coordinate | null = null;
|
||||
constructor(source: CustomPrimitivesDataSource, p: Point) {
|
||||
this._source = source;
|
||||
this._p = p;
|
||||
}
|
||||
abstract update(): void;
|
||||
abstract text(): string;
|
||||
|
||||
coordinate() {
|
||||
return this._pos ?? -1;
|
||||
}
|
||||
|
||||
visible(): boolean {
|
||||
return this._source.options.showLabels;
|
||||
}
|
||||
|
||||
tickVisible(): boolean {
|
||||
return this._source.options.showLabels;
|
||||
}
|
||||
|
||||
textColor() {
|
||||
return this._source.options.labelTextColor;
|
||||
}
|
||||
backColor() {
|
||||
return this._source.options.labelColor;
|
||||
}
|
||||
movePoint(p: Point) {
|
||||
this._p = p;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomPrimitivesTimeAxisView extends CustomPrimitivesAxisView {
|
||||
update() {
|
||||
const timeScale = this._source.chart.timeScale();
|
||||
this._pos = timeScale.timeToCoordinate(this._p.time);
|
||||
}
|
||||
text() {
|
||||
return this._source.options.timeLabelFormatter(this._p.time);
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomPrimitivesPriceAxisView extends CustomPrimitivesAxisView {
|
||||
update() {
|
||||
const series = this._source.series;
|
||||
this._pos = series.priceToCoordinate(this._p.price);
|
||||
}
|
||||
text() {
|
||||
return this._source.options.priceLabelFormatter(this._p.price);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import { AutoscaleInfo, Logical, Time, DataChangedScope } from 'lightweight-charts';
|
||||
import {
|
||||
CustomPrimitivesPriceAxisPaneView,
|
||||
CustomPrimitivesTimeAxisPaneView,
|
||||
} from './axis-pane-view';
|
||||
import { CustomPrimitivesPriceAxisView, CustomPrimitivesTimeAxisView } from './axis-view';
|
||||
import { Point, CustomPrimitivesDataSource } from './data-source';
|
||||
import { CustomPrimitivesOptions, defaultOptions } from './options';
|
||||
import { CustomPrimitivesPaneView } from './pane-view';
|
||||
import { PluginBase } from './plugin-base';
|
||||
|
||||
export class CustomPrimitives
|
||||
extends PluginBase
|
||||
implements CustomPrimitivesDataSource
|
||||
{
|
||||
_options: CustomPrimitivesOptions;
|
||||
_p1: Point;
|
||||
_p2: Point;
|
||||
_paneViews: CustomPrimitivesPaneView[];
|
||||
_timeAxisViews: CustomPrimitivesTimeAxisView[];
|
||||
_priceAxisViews: CustomPrimitivesPriceAxisView[];
|
||||
_priceAxisPaneViews: CustomPrimitivesPriceAxisPaneView[];
|
||||
_timeAxisPaneViews: CustomPrimitivesTimeAxisPaneView[];
|
||||
|
||||
constructor(
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
options: Partial<CustomPrimitivesOptions> = {}
|
||||
) {
|
||||
super();
|
||||
this._p1 = p1;
|
||||
this._p2 = p2;
|
||||
this._options = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
};
|
||||
this._paneViews = [new CustomPrimitivesPaneView(this)];
|
||||
this._timeAxisViews = [
|
||||
new CustomPrimitivesTimeAxisView(this, p1),
|
||||
new CustomPrimitivesTimeAxisView(this, p2),
|
||||
];
|
||||
this._priceAxisViews = [
|
||||
new CustomPrimitivesPriceAxisView(this, p1),
|
||||
new CustomPrimitivesPriceAxisView(this, p2),
|
||||
];
|
||||
this._priceAxisPaneViews = [new CustomPrimitivesPriceAxisPaneView(this, true)];
|
||||
this._timeAxisPaneViews = [new CustomPrimitivesTimeAxisPaneView(this, false)];
|
||||
}
|
||||
|
||||
updateAllViews() {
|
||||
//* Use this method to update any data required by the
|
||||
//* views to draw.
|
||||
this._paneViews.forEach(pw => pw.update());
|
||||
this._timeAxisViews.forEach(pw => pw.update());
|
||||
this._priceAxisViews.forEach(pw => pw.update());
|
||||
this._priceAxisPaneViews.forEach(pw => pw.update());
|
||||
this._timeAxisPaneViews.forEach(pw => pw.update());
|
||||
}
|
||||
|
||||
priceAxisViews() {
|
||||
//* Labels rendered on the price scale
|
||||
return this._priceAxisViews;
|
||||
}
|
||||
|
||||
timeAxisViews() {
|
||||
//* labels rendered on the time scale
|
||||
return this._timeAxisViews;
|
||||
}
|
||||
|
||||
paneViews() {
|
||||
//* rendering on the main chart pane
|
||||
return this._paneViews;
|
||||
}
|
||||
|
||||
priceAxisPaneViews() {
|
||||
//* rendering on the price scale
|
||||
return this._priceAxisPaneViews;
|
||||
}
|
||||
|
||||
timeAxisPaneViews() {
|
||||
//* rendering on the time scale
|
||||
return this._timeAxisPaneViews;
|
||||
}
|
||||
|
||||
autoscaleInfo(
|
||||
startTimePoint: Logical,
|
||||
endTimePoint: Logical
|
||||
): AutoscaleInfo | null {
|
||||
//* Use this method to provide autoscale information if your primitive
|
||||
//* should have the ability to remain in view automatically.
|
||||
if (
|
||||
this._timeCurrentlyVisible(this.p1.time, startTimePoint, endTimePoint) ||
|
||||
this._timeCurrentlyVisible(this.p2.time, startTimePoint, endTimePoint)
|
||||
) {
|
||||
return {
|
||||
priceRange: {
|
||||
minValue: Math.min(this.p1.price, this.p2.price),
|
||||
maxValue: Math.max(this.p1.price, this.p2.price),
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dataUpdated(scope: DataChangedScope): void {
|
||||
//* This method will be called by PluginBase when the data on the
|
||||
//* series has changed.
|
||||
}
|
||||
|
||||
_timeCurrentlyVisible(
|
||||
time: Time,
|
||||
startTimePoint: Logical,
|
||||
endTimePoint: Logical
|
||||
): boolean {
|
||||
const ts = this.chart.timeScale();
|
||||
const coordinate = ts.timeToCoordinate(time);
|
||||
if (coordinate === null) return false;
|
||||
const logical = ts.coordinateToLogical(coordinate);
|
||||
if (logical === null) return false;
|
||||
return logical <= endTimePoint && logical >= startTimePoint;
|
||||
}
|
||||
|
||||
public get options(): CustomPrimitivesOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
applyOptions(options: Partial<CustomPrimitivesOptions>) {
|
||||
this._options = { ...this._options, ...options };
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public get p1(): Point {
|
||||
return this._p1;
|
||||
}
|
||||
|
||||
public get p2(): Point {
|
||||
return this._p2;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import {
|
||||
IChartApi,
|
||||
ISeriesApi,
|
||||
SeriesOptionsMap,
|
||||
Time,
|
||||
} from 'lightweight-charts';
|
||||
import { CustomPrimitivesOptions } from './options';
|
||||
|
||||
export interface Point {
|
||||
time: Time;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface CustomPrimitivesDataSource {
|
||||
chart: IChartApi;
|
||||
series: ISeriesApi<keyof SeriesOptionsMap>;
|
||||
options: CustomPrimitivesOptions;
|
||||
p1: Point;
|
||||
p2: Point;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { createChart } from 'lightweight-charts';
|
||||
import { generateLineData } from '../sample-data';
|
||||
import { CustomPrimitives } from '../custom-primitives';
|
||||
|
||||
const chart = ((window as unknown as any).chart = createChart('chart', {
|
||||
autoSize: true,
|
||||
}));
|
||||
|
||||
const lineSeries = chart.addLineSeries({
|
||||
color: '#000000',
|
||||
});
|
||||
const data = generateLineData();
|
||||
lineSeries.setData(data);
|
||||
|
||||
const time1 = data[data.length - 50].time;
|
||||
const time2 = data[data.length - 10].time;
|
||||
|
||||
const primitive = new CustomPrimitives(
|
||||
{ price: 100, time: time1 },
|
||||
{ price: 500, time: time2 }
|
||||
);
|
||||
|
||||
lineSeries.attachPrimitive(primitive);
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Template Drawing Primitive Plugin Example</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: rgba(248, 249, 253, 1);
|
||||
color: rgba(19, 23, 34, 1);
|
||||
}
|
||||
#chart {
|
||||
margin-inline: auto;
|
||||
max-width: 600px;
|
||||
height: 300px;
|
||||
background-color: rgba(240, 243, 250, 1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart"></div>
|
||||
<script type="module" src="./example.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* Ensures that value is defined.
|
||||
* Throws if the value is undefined, returns the original value otherwise.
|
||||
*
|
||||
* @param value - The value, or undefined.
|
||||
* @returns The passed value, if it is not undefined
|
||||
*/
|
||||
export function ensureDefined(value: undefined): never;
|
||||
export function ensureDefined<T>(value: T | undefined): T;
|
||||
export function ensureDefined<T>(value: T | undefined): T {
|
||||
if (value === undefined) {
|
||||
throw new Error('Value is undefined');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that value is not null.
|
||||
* Throws if the value is null, returns the original value otherwise.
|
||||
*
|
||||
* @param value - The value, or null.
|
||||
* @returns The passed value, if it is not null
|
||||
*/
|
||||
export function ensureNotNull(value: null): never;
|
||||
export function ensureNotNull<T>(value: T | null): T;
|
||||
export function ensureNotNull<T>(value: T | null): T {
|
||||
if (value === null) {
|
||||
throw new Error('Value is null');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export interface BitmapPositionLength {
|
||||
/** coordinate for use with a bitmap rendering scope */
|
||||
position: number;
|
||||
/** length for use with a bitmap rendering scope */
|
||||
length: number;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/**
|
||||
* Default grid / crosshair line width in Bitmap sizing
|
||||
* @param horizontalPixelRatio - horizontal pixel ratio
|
||||
* @returns default grid / crosshair line width in Bitmap sizing
|
||||
*/
|
||||
export function gridAndCrosshairBitmapWidth(
|
||||
horizontalPixelRatio: number
|
||||
): number {
|
||||
return Math.max(1, Math.floor(horizontalPixelRatio));
|
||||
}
|
||||
|
||||
/**
|
||||
* Default grid / crosshair line width in Media sizing
|
||||
* @param horizontalPixelRatio - horizontal pixel ratio
|
||||
* @returns default grid / crosshair line width in Media sizing
|
||||
*/
|
||||
export function gridAndCrosshairMediaWidth(
|
||||
horizontalPixelRatio: number
|
||||
): number {
|
||||
return (
|
||||
gridAndCrosshairBitmapWidth(horizontalPixelRatio) / horizontalPixelRatio
|
||||
);
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { BitmapPositionLength } from './common';
|
||||
|
||||
/**
|
||||
* Calculates the position and width which will completely full the space for the bar.
|
||||
* Useful if you want to draw something that will not have any gaps between surrounding bars.
|
||||
* @param xMedia - x coordinate of the bar defined in media sizing
|
||||
* @param halfBarSpacingMedia - half the width of the current barSpacing (un-rounded)
|
||||
* @param horizontalPixelRatio - horizontal pixel ratio
|
||||
* @returns position and width which will completely full the space for the bar
|
||||
*/
|
||||
export function fullBarWidth(
|
||||
xMedia: number,
|
||||
halfBarSpacingMedia: number,
|
||||
horizontalPixelRatio: number
|
||||
): BitmapPositionLength {
|
||||
const fullWidthLeftMedia = xMedia - halfBarSpacingMedia;
|
||||
const fullWidthRightMedia = xMedia + halfBarSpacingMedia;
|
||||
const fullWidthLeftBitmap = Math.round(
|
||||
fullWidthLeftMedia * horizontalPixelRatio
|
||||
);
|
||||
const fullWidthRightBitmap = Math.round(
|
||||
fullWidthRightMedia * horizontalPixelRatio
|
||||
);
|
||||
const fullWidthBitmap = fullWidthRightBitmap - fullWidthLeftBitmap;
|
||||
return {
|
||||
position: fullWidthLeftBitmap,
|
||||
length: fullWidthBitmap,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import { BitmapPositionLength } from './common';
|
||||
|
||||
function centreOffset(lineBitmapWidth: number): number {
|
||||
return Math.floor(lineBitmapWidth * 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the bitmap position for an item with a desired length (height or width), and centred according to
|
||||
* an position coordinate defined in media sizing.
|
||||
* @param positionMedia - position coordinate for the bar (in media coordinates)
|
||||
* @param pixelRatio - pixel ratio. Either horizontal for x positions, or vertical for y positions
|
||||
* @param desiredWidthMedia - desired width (in media coordinates)
|
||||
* @returns Position of of the start point and length dimension.
|
||||
*/
|
||||
export function positionsLine(
|
||||
positionMedia: number,
|
||||
pixelRatio: number,
|
||||
desiredWidthMedia: number = 1,
|
||||
widthIsBitmap?: boolean
|
||||
): BitmapPositionLength {
|
||||
const scaledPosition = Math.round(pixelRatio * positionMedia);
|
||||
const lineBitmapWidth = widthIsBitmap
|
||||
? desiredWidthMedia
|
||||
: Math.round(desiredWidthMedia * pixelRatio);
|
||||
const offset = centreOffset(lineBitmapWidth);
|
||||
const position = scaledPosition - offset;
|
||||
return { position, length: lineBitmapWidth };
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the bitmap position and length for a dimension of a shape to be drawn.
|
||||
* @param position1Media - media coordinate for the first point
|
||||
* @param position2Media - media coordinate for the second point
|
||||
* @param pixelRatio - pixel ratio for the corresponding axis (vertical or horizontal)
|
||||
* @returns Position of of the start point and length dimension.
|
||||
*/
|
||||
export function positionsBox(
|
||||
position1Media: number,
|
||||
position2Media: number,
|
||||
pixelRatio: number
|
||||
): BitmapPositionLength {
|
||||
const scaledPosition1 = Math.round(pixelRatio * position1Media);
|
||||
const scaledPosition2 = Math.round(pixelRatio * position2Media);
|
||||
return {
|
||||
position: Math.min(scaledPosition1, scaledPosition2),
|
||||
length: Math.abs(scaledPosition2 - scaledPosition1) + 1,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { Time, isUTCTimestamp, isBusinessDay } from 'lightweight-charts';
|
||||
|
||||
export function convertTime(t: Time): number {
|
||||
if (isUTCTimestamp(t)) return t * 1000;
|
||||
if (isBusinessDay(t)) return new Date(t.year, t.month, t.day).valueOf();
|
||||
const [year, month, day] = t.split('-').map(parseInt);
|
||||
return new Date(year, month, day).valueOf();
|
||||
}
|
||||
|
||||
export function displayTime(time: Time): string {
|
||||
if (typeof time == 'string') return time;
|
||||
const date = isBusinessDay(time)
|
||||
? new Date(time.year, time.month, time.day)
|
||||
: new Date(time * 1000);
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function formattedDateAndTime(timestamp: number | undefined): [string, string] {
|
||||
if (!timestamp) return ['', ''];
|
||||
const dateObj = new Date(timestamp);
|
||||
|
||||
// Format date string
|
||||
const year = dateObj.getFullYear();
|
||||
const month = dateObj.toLocaleString('default', { month: 'short' });
|
||||
const date = dateObj.getDate().toString().padStart(2, '0');
|
||||
const formattedDate = `${date} ${month} ${year}`;
|
||||
|
||||
// Format time string
|
||||
const hours = dateObj.getHours().toString().padStart(2, '0');
|
||||
const minutes = dateObj.getMinutes().toString().padStart(2, '0');
|
||||
const formattedTime = `${hours}:${minutes}`;
|
||||
|
||||
return [formattedDate, formattedTime];
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { Time, isBusinessDay } from 'lightweight-charts';
|
||||
|
||||
export interface CustomPrimitivesOptions {
|
||||
//* Define the options for the primitive.
|
||||
fillColor: string;
|
||||
labelColor: string;
|
||||
labelTextColor: string;
|
||||
showLabels: boolean;
|
||||
priceLabelFormatter: (price: number) => string;
|
||||
timeLabelFormatter: (time: Time) => string;
|
||||
}
|
||||
|
||||
export const defaultOptions: CustomPrimitivesOptions = {
|
||||
//* Define the default values for all the primitive options.
|
||||
fillColor: 'rgba(200, 50, 100, 0.75)',
|
||||
labelColor: 'rgba(200, 50, 100, 1)',
|
||||
labelTextColor: 'white',
|
||||
showLabels: true,
|
||||
priceLabelFormatter: (price: number) => price.toFixed(2),
|
||||
timeLabelFormatter: (time: Time) => {
|
||||
if (typeof time == 'string') return time;
|
||||
const date = isBusinessDay(time)
|
||||
? new Date(time.year, time.month, time.day)
|
||||
: new Date(time * 1000);
|
||||
return date.toLocaleDateString();
|
||||
},
|
||||
} as const;
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { CanvasRenderingTarget2D } from 'fancy-canvas';
|
||||
import { ISeriesPrimitivePaneRenderer } from 'lightweight-charts';
|
||||
import { ViewPoint } from './pane-view';
|
||||
import { positionsBox } from './helpers/dimensions/positions';
|
||||
|
||||
export class CustomPrimitivesPaneRenderer implements ISeriesPrimitivePaneRenderer {
|
||||
_p1: ViewPoint;
|
||||
_p2: ViewPoint;
|
||||
_fillColor: string;
|
||||
|
||||
constructor(p1: ViewPoint, p2: ViewPoint, fillColor: string) {
|
||||
this._p1 = p1;
|
||||
this._p2 = p2;
|
||||
this._fillColor = fillColor;
|
||||
}
|
||||
|
||||
draw(target: CanvasRenderingTarget2D) {
|
||||
target.useBitmapCoordinateSpace(scope => {
|
||||
if (
|
||||
this._p1.x === null ||
|
||||
this._p1.y === null ||
|
||||
this._p2.x === null ||
|
||||
this._p2.y === null
|
||||
)
|
||||
return;
|
||||
const ctx = scope.context;
|
||||
const horizontalPositions = positionsBox(
|
||||
this._p1.x,
|
||||
this._p2.x,
|
||||
scope.horizontalPixelRatio
|
||||
);
|
||||
const verticalPositions = positionsBox(
|
||||
this._p1.y,
|
||||
this._p2.y,
|
||||
scope.verticalPixelRatio
|
||||
);
|
||||
ctx.fillStyle = this._fillColor;
|
||||
ctx.fillRect(
|
||||
horizontalPositions.position,
|
||||
verticalPositions.position,
|
||||
horizontalPositions.length,
|
||||
verticalPositions.length
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { Coordinate, ISeriesPrimitivePaneView } from 'lightweight-charts';
|
||||
import { CustomPrimitivesPaneRenderer } from './pane-renderer';
|
||||
import { CustomPrimitivesDataSource } from './data-source';
|
||||
|
||||
export interface ViewPoint {
|
||||
x: Coordinate | null;
|
||||
y: Coordinate | null;
|
||||
}
|
||||
|
||||
export class CustomPrimitivesPaneView implements ISeriesPrimitivePaneView {
|
||||
_source: CustomPrimitivesDataSource;
|
||||
_p1: ViewPoint = { x: null, y: null };
|
||||
_p2: ViewPoint = { x: null, y: null };
|
||||
|
||||
constructor(source: CustomPrimitivesDataSource) {
|
||||
this._source = source;
|
||||
}
|
||||
|
||||
update() {
|
||||
const series = this._source.series;
|
||||
const y1 = series.priceToCoordinate(this._source.p1.price);
|
||||
const y2 = series.priceToCoordinate(this._source.p2.price);
|
||||
const timeScale = this._source.chart.timeScale();
|
||||
const x1 = timeScale.timeToCoordinate(this._source.p1.time);
|
||||
const x2 = timeScale.timeToCoordinate(this._source.p2.time);
|
||||
this._p1 = { x: x1, y: y1 };
|
||||
this._p2 = { x: x2, y: y2 };
|
||||
}
|
||||
|
||||
renderer() {
|
||||
return new CustomPrimitivesPaneRenderer(
|
||||
this._p1,
|
||||
this._p2,
|
||||
this._source.options.fillColor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import {
|
||||
DataChangedScope,
|
||||
IChartApi,
|
||||
ISeriesApi,
|
||||
ISeriesPrimitive,
|
||||
SeriesAttachedParameter,
|
||||
SeriesOptionsMap,
|
||||
Time,
|
||||
} from 'lightweight-charts';
|
||||
import { ensureDefined } from './helpers/assertions';
|
||||
|
||||
//* PluginBase is a useful base to build a plugin upon which
|
||||
//* already handles creating getters for the chart and series,
|
||||
//* and provides a requestUpdate method.
|
||||
export abstract class PluginBase implements ISeriesPrimitive<Time> {
|
||||
private _chart: IChartApi | undefined = undefined;
|
||||
private _series: ISeriesApi<keyof SeriesOptionsMap> | undefined = undefined;
|
||||
|
||||
protected dataUpdated?(scope: DataChangedScope): void;
|
||||
protected requestUpdate(): void {
|
||||
if (this._requestUpdate) this._requestUpdate();
|
||||
}
|
||||
private _requestUpdate?: () => void;
|
||||
|
||||
public attached({
|
||||
chart,
|
||||
series,
|
||||
requestUpdate,
|
||||
}: SeriesAttachedParameter<Time>) {
|
||||
this._chart = chart;
|
||||
this._series = series;
|
||||
this._series.subscribeDataChanged(this._fireDataUpdated);
|
||||
this._requestUpdate = requestUpdate;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public detached() {
|
||||
this._chart = undefined;
|
||||
this._series = undefined;
|
||||
this._requestUpdate = undefined;
|
||||
}
|
||||
|
||||
public get chart(): IChartApi {
|
||||
return ensureDefined(this._chart);
|
||||
}
|
||||
|
||||
public get series(): ISeriesApi<keyof SeriesOptionsMap> {
|
||||
return ensureDefined(this._series);
|
||||
}
|
||||
|
||||
private _fireDataUpdated(scope: DataChangedScope) {
|
||||
if (this.dataUpdated) {
|
||||
this.dataUpdated(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import type { Time } from 'lightweight-charts';
|
||||
|
||||
type LineData = {
|
||||
time: Time;
|
||||
value: number;
|
||||
};
|
||||
|
||||
let randomFactor = 25 + Math.random() * 25;
|
||||
const samplePoint = (i: number) =>
|
||||
i *
|
||||
(0.5 +
|
||||
Math.sin(i / 10) * 0.2 +
|
||||
Math.sin(i / 20) * 0.4 +
|
||||
Math.sin(i / randomFactor) * 0.8 +
|
||||
Math.sin(i / 500) * 0.5) +
|
||||
200;
|
||||
|
||||
export function generateLineData(numberOfPoints: number = 500): LineData[] {
|
||||
randomFactor = 25 + Math.random() * 25;
|
||||
const res = [];
|
||||
const date = new Date(Date.UTC(2023, 0, 1, 12, 0, 0, 0));
|
||||
for (let i = 0; i < numberOfPoints; ++i) {
|
||||
const time = (date.getTime() / 1000) as Time;
|
||||
const value = samplePoint(i);
|
||||
res.push({
|
||||
time,
|
||||
value,
|
||||
});
|
||||
|
||||
date.setUTCDate(date.getUTCDate() + 1);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { defineConfig } from 'vite';
|
||||
|
||||
const input = {
|
||||
main: './src/example/index.html',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationDir": "typings",
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
// src/components/ComponentRenderer.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { transform } from '@babel/standalone';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { App } from 'obsidian';
|
||||
import { useStorage } from '../hooks/useStorage';
|
||||
import { ComponentRegistry } from '../core/registry';
|
||||
import { StorageManager } from '../core/storage';
|
||||
|
||||
interface ComponentRendererProps {
|
||||
code: string;
|
||||
scopeId: string;
|
||||
inline?: boolean;
|
||||
storage: StorageManager; // Add storage prop
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TransformError {
|
||||
message: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
}
|
||||
|
||||
const ErrorDisplay: React.FC<{ error: TransformError }> = ({ error }) => {
|
||||
return (
|
||||
<div className="react-component-error">
|
||||
<p>Error in component:</p>
|
||||
<pre className="error-message">
|
||||
{error.message}
|
||||
{error.line && error.column &&
|
||||
`\nAt line ${error.line}, column ${error.column}`
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComponentRenderer: React.FC<ComponentRendererProps> = ({
|
||||
code,
|
||||
scopeId,
|
||||
storage, // Get storage from props
|
||||
inline = false
|
||||
}) => {
|
||||
const [Component, setComponent] = useState<React.ComponentType | null>(null);
|
||||
const [error, setError] = useState<TransformError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const createComponent = async () => {
|
||||
try {
|
||||
const transformedCode = transform(code, {
|
||||
presets: ['env','react'],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-jsx', // Add JSX syntax plugin explicitly
|
||||
'@babel/plugin-transform-react-jsx', // Transforms JSX into JS
|
||||
'@babel/plugin-proposal-class-properties', // Handles class properties
|
||||
'@babel/plugin-proposal-object-rest-spread', // Handles object spread syntax
|
||||
],
|
||||
filename: `component-${scopeId}`,
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
babelrc: false
|
||||
}).code;
|
||||
console.log('Transformed Code:', transformedCode);
|
||||
// Create storage hook bound to this storage instance
|
||||
const boundUseStorage = <T,>(key: string, defaultValue: T) =>
|
||||
useStorage(key, defaultValue, storage);
|
||||
|
||||
// Enhanced scope with registry and storage
|
||||
const scope = {
|
||||
React,
|
||||
useState: React.useState,
|
||||
useEffect: React.useEffect,
|
||||
useRef: React.useRef,
|
||||
useStorage: boundUseStorage, // Add bound storage hook
|
||||
...ComponentRegistry,
|
||||
};
|
||||
|
||||
const componentFn = new Function(
|
||||
...Object.keys(scope),
|
||||
`try {
|
||||
${transformedCode}
|
||||
return Component;
|
||||
} catch (err) {
|
||||
console.error('Error in component execution:', err);
|
||||
throw err;
|
||||
}`
|
||||
);
|
||||
|
||||
const ComponentType = componentFn(...Object.values(scope));
|
||||
setComponent(() => ComponentType);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Component transformation failed:', err);
|
||||
setError({
|
||||
message: err instanceof Error ? err.message : 'Unknown error',
|
||||
line: (err as any).loc?.line,
|
||||
column: (err as any).loc?.column
|
||||
});
|
||||
setComponent(null);
|
||||
}
|
||||
};
|
||||
|
||||
createComponent();
|
||||
}, [code, scopeId, storage]); // Add storage to dependencies
|
||||
|
||||
const Wrapper: React.FC<{ children: React.ReactNode }> =
|
||||
inline ? ({ children }) => (
|
||||
<span className="inline-component">{children}</span>
|
||||
) : ({ children }) => (
|
||||
<div className="block-component">{children}</div>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <ErrorDisplay error={error} />;
|
||||
}
|
||||
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div
|
||||
className="component-sandbox"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<Component />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// src/utils/orderBlockVisualization.ts
|
||||
import { IChartApi, ISeriesApi } from 'lightweight-charts';
|
||||
import { Rectangle } from '../plugins/RectanglePlugin';
|
||||
import { OrderBlock } from '../services/OrderBlockAnalysis';
|
||||
|
||||
export function visualizeOrderBlocks(
|
||||
chart: IChartApi,
|
||||
mainSeries: ISeriesApi<"Candlestick">,
|
||||
orderBlocks: OrderBlock[]
|
||||
) {
|
||||
orderBlocks.forEach(block => {
|
||||
const rectangle = new Rectangle(
|
||||
{
|
||||
time: block.startTime / 1000, // Convert to seconds for lightweight-charts
|
||||
price: block.highPrice
|
||||
},
|
||||
{
|
||||
time: block.endTime / 1000,
|
||||
price: block.lowPrice
|
||||
},
|
||||
{
|
||||
fillColor: block.type === 'bullish'
|
||||
? 'rgba(34, 197, 94, 0.2)' // Green
|
||||
: 'rgba(239, 68, 68, 0.2)', // Red
|
||||
borderColor: block.type === 'bullish'
|
||||
? 'rgba(34, 197, 94, 1)'
|
||||
: 'rgba(239, 68, 68, 1)',
|
||||
borderWidth: block.validationMetrics.impulseStrength > 1.5 ? 2 : 1,
|
||||
opacity: block.validationMetrics.gapQuality,
|
||||
extend: 'right' // Extend to the right of the chart
|
||||
}
|
||||
);
|
||||
|
||||
mainSeries.attachPrimitive(rectangle);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
// src/component/RectPlugin.ts
|
||||
interface Point {
|
||||
time: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface RectangleOptions {
|
||||
fillColor?: string;
|
||||
borderColor?: string;
|
||||
borderWidth?: number;
|
||||
opacity?: number;
|
||||
extend?: 'none' | 'right';
|
||||
}
|
||||
|
||||
export class Rectangle {
|
||||
private _point1: Point;
|
||||
private _point2: Point;
|
||||
private _options: Required<RectangleOptions>;
|
||||
|
||||
private static readonly defaultOptions: Required<RectangleOptions> = {
|
||||
fillColor: 'rgba(255, 255, 255, 0.2)',
|
||||
borderColor: 'rgba(255, 255, 255, 1)',
|
||||
borderWidth: 1,
|
||||
opacity: 0.2,
|
||||
extend: 'none'
|
||||
};
|
||||
|
||||
constructor(point1: Point, point2: Point, options: RectangleOptions = {}) {
|
||||
this._point1 = point1;
|
||||
this._point2 = point2;
|
||||
this._options = { ...Rectangle.defaultOptions, ...options };
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D, pixelRatio: number, model: any): void {
|
||||
const points = this._calculatePoints(model);
|
||||
if (!points) return;
|
||||
|
||||
const { x1, y1, x2, y2 } = points;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Set line style
|
||||
ctx.lineWidth = this._options.borderWidth * pixelRatio;
|
||||
ctx.strokeStyle = this._options.borderColor;
|
||||
ctx.fillStyle = this._options.fillColor;
|
||||
|
||||
// Draw rectangle
|
||||
ctx.beginPath();
|
||||
ctx.rect(
|
||||
Math.round(x1 * pixelRatio),
|
||||
Math.round(y1 * pixelRatio),
|
||||
Math.round((x2 - x1) * pixelRatio),
|
||||
Math.round((y2 - y1) * pixelRatio)
|
||||
);
|
||||
|
||||
// Fill with semi-transparency
|
||||
ctx.globalAlpha = this._options.opacity;
|
||||
ctx.fill();
|
||||
|
||||
// Draw border
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
hitTest(x: number, y: number): boolean {
|
||||
// Implement hit testing if needed
|
||||
return false;
|
||||
}
|
||||
|
||||
private _calculatePoints(model: any) {
|
||||
const point1X = model.timeScale().timeToCoordinate(this._point1.time);
|
||||
const point2X = this._options.extend === 'right'
|
||||
? model.timeScale().getWidth()
|
||||
: model.timeScale().timeToCoordinate(this._point2.time);
|
||||
|
||||
const point1Y = model.priceScale('right').priceToCoordinate(this._point1.price);
|
||||
const point2Y = model.priceScale('right').priceToCoordinate(this._point2.price);
|
||||
|
||||
if (!point1X || !point2X || !point1Y || !point2Y) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
x1: Math.min(point1X, point2X),
|
||||
y1: Math.min(point1Y, point2Y),
|
||||
x2: Math.max(point1X, point2X),
|
||||
y2: Math.max(point1Y, point2Y)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// components/styled.tsx
|
||||
import React from 'react';
|
||||
|
||||
interface BoxProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
mt?: number;
|
||||
mb?: number;
|
||||
p?: number;
|
||||
flex?: boolean;
|
||||
grid?: boolean;
|
||||
cols?: number;
|
||||
gap?: number;
|
||||
center?: boolean;
|
||||
}
|
||||
|
||||
export const Box = React.forwardRef<HTMLDivElement, BoxProps>(({
|
||||
mt,
|
||||
mb,
|
||||
p,
|
||||
flex,
|
||||
grid,
|
||||
cols,
|
||||
gap,
|
||||
center,
|
||||
style,
|
||||
...props
|
||||
}, ref) => {
|
||||
const computedStyle: React.CSSProperties = {
|
||||
...(mt && { marginTop: `${mt * 0.25}rem` }),
|
||||
...(mb && { marginBottom: `${mb * 0.25}rem` }),
|
||||
...(p && { padding: `${p * 0.25}rem` }),
|
||||
...(flex && { display: 'flex' }),
|
||||
...(grid && {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols || 1}, minmax(0, 1fr))`,
|
||||
gap: gap ? `${gap * 0.25}rem` : undefined
|
||||
}),
|
||||
...(center && {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}),
|
||||
...style
|
||||
};
|
||||
|
||||
return <div ref={ref} style={computedStyle} {...props} />;
|
||||
});
|
||||
|
||||
// Usage:
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<Box flex center p={4} mt={2}>
|
||||
<Box grid cols={2} gap={4}>
|
||||
<div>Column 1</div>
|
||||
<div>Column 2</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
// src/core/dynamic.tsx
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
ComponentType,
|
||||
FC,
|
||||
ComponentProps,
|
||||
ReactElement
|
||||
} from 'react';
|
||||
|
||||
interface DynamicOptions {
|
||||
loading?: ComponentType;
|
||||
error?: ComponentType<{ error: Error }>;
|
||||
}
|
||||
|
||||
type ImportFunction<T> = () => Promise<{ default: T }>;
|
||||
|
||||
const DefaultError: FC<{ error: Error }> = ({ error }): ReactElement => {
|
||||
return (
|
||||
<div className="dynamic-import-error">
|
||||
Error loading component: {error.message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultLoading: FC = (): ReactElement => {
|
||||
return <div className="dynamic-loading">Loading...</div>;
|
||||
};
|
||||
|
||||
export function dynamic<T extends ComponentType<any>>(
|
||||
importFn: ImportFunction<T>,
|
||||
options: DynamicOptions = {}
|
||||
): FC<ComponentProps<T>> {
|
||||
const LoadingComponent: ComponentType = options.loading || DefaultLoading;
|
||||
const ErrorComponent: ComponentType<{ error: Error }> = options.error || DefaultError;
|
||||
|
||||
const DynamicComponent: FC<ComponentProps<T>> = (props): ReactElement => {
|
||||
const [Component, setComponent] = useState<T | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const loadComponent = async (): Promise<void> => {
|
||||
try {
|
||||
const module = await importFn();
|
||||
if (mounted) {
|
||||
setComponent(() => module.default);
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to load component'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadComponent();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <ErrorComponent error={error} />;
|
||||
}
|
||||
|
||||
if (!Component) {
|
||||
return <LoadingComponent />;
|
||||
}
|
||||
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
return DynamicComponent;
|
||||
}
|
||||
|
||||
// Version for named exports
|
||||
export function dynamicNamed<T extends ComponentType<any>>(
|
||||
importFn: () => Promise<T>,
|
||||
options: DynamicOptions = {}
|
||||
): FC<ComponentProps<T>> {
|
||||
return dynamic(
|
||||
async () => ({ default: await importFn() }),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
// src/core/scope.ts
|
||||
import React from 'react';
|
||||
import { App } from 'obsidian';
|
||||
|
||||
interface ScopeOptions {
|
||||
app?: App;
|
||||
parentScope?: ComponentScope;
|
||||
isGlobal?: boolean;
|
||||
}
|
||||
|
||||
export class ComponentScope {
|
||||
private static instances = new Map<string, ComponentScope>();
|
||||
private static globalScope: ComponentScope | null = null;
|
||||
|
||||
private variables = new Map<string, any>();
|
||||
private children = new Set<ComponentScope>();
|
||||
private readonly defaultScope: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
private readonly options: ScopeOptions = {}
|
||||
) {
|
||||
// Initialize default React scope
|
||||
this.defaultScope = {
|
||||
React,
|
||||
useState: React.useState,
|
||||
useEffect: React.useEffect,
|
||||
useRef: React.useRef,
|
||||
useMemo: React.useMemo,
|
||||
useCallback: React.useCallback,
|
||||
useContext: React.useContext,
|
||||
// Add Obsidian app if available
|
||||
app: options.app,
|
||||
};
|
||||
|
||||
if (options.isGlobal) {
|
||||
ComponentScope.globalScope = this;
|
||||
} else if (options.parentScope) {
|
||||
options.parentScope.addChild(this);
|
||||
}
|
||||
|
||||
ComponentScope.instances.set(id, this);
|
||||
}
|
||||
|
||||
// Get a value from scope
|
||||
get(name: string): any {
|
||||
// Check local variables first
|
||||
if (this.variables.has(name)) {
|
||||
return this.variables.get(name);
|
||||
}
|
||||
|
||||
// Check default scope
|
||||
if (name in this.defaultScope) {
|
||||
return this.defaultScope[name];
|
||||
}
|
||||
|
||||
// Check parent scope
|
||||
if (this.options.parentScope) {
|
||||
return this.options.parentScope.get(name);
|
||||
}
|
||||
|
||||
// Check global scope as last resort
|
||||
if (!this.options.isGlobal && ComponentScope.globalScope) {
|
||||
return ComponentScope.globalScope.get(name);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Set a value in scope
|
||||
set(name: string, value: any): void {
|
||||
this.variables.set(name, value);
|
||||
}
|
||||
|
||||
// Remove a value from scope
|
||||
delete(name: string): boolean {
|
||||
return this.variables.delete(name);
|
||||
}
|
||||
|
||||
// Check if scope has a variable
|
||||
has(name: string): boolean {
|
||||
return (
|
||||
this.variables.has(name) ||
|
||||
name in this.defaultScope ||
|
||||
(this.options.parentScope?.has(name) ?? false) ||
|
||||
(!this.options.isGlobal && (ComponentScope.globalScope?.has(name) ?? false))
|
||||
);
|
||||
}
|
||||
|
||||
// Add a child scope
|
||||
private addChild(scope: ComponentScope): void {
|
||||
this.children.add(scope);
|
||||
}
|
||||
|
||||
// Remove a child scope
|
||||
private removeChild(scope: ComponentScope): void {
|
||||
this.children.delete(scope);
|
||||
}
|
||||
|
||||
// Clear all variables in this scope
|
||||
clear(): void {
|
||||
this.variables.clear();
|
||||
}
|
||||
|
||||
// Dispose of this scope
|
||||
dispose(): void {
|
||||
if (this.options.parentScope) {
|
||||
this.options.parentScope.removeChild(this);
|
||||
}
|
||||
ComponentScope.instances.delete(this.id);
|
||||
this.clear();
|
||||
|
||||
// Dispose of all children
|
||||
this.children.forEach(child => child.dispose());
|
||||
this.children.clear();
|
||||
}
|
||||
|
||||
// Get all variables in this scope (including inherited)
|
||||
getAllVariables(): Record<string, any> {
|
||||
const variables: Record<string, any> = {
|
||||
...this.defaultScope,
|
||||
...Object.fromEntries(this.variables)
|
||||
};
|
||||
|
||||
if (this.options.parentScope) {
|
||||
return {
|
||||
...this.options.parentScope.getAllVariables(),
|
||||
...variables
|
||||
};
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
// Static methods for scope management
|
||||
static getScope(id: string): ComponentScope | undefined {
|
||||
return this.instances.get(id);
|
||||
}
|
||||
|
||||
static createScope(id: string, options: ScopeOptions = {}): ComponentScope {
|
||||
if (this.instances.has(id)) {
|
||||
throw new Error(`Scope with id "${id}" already exists`);
|
||||
}
|
||||
return new ComponentScope(id, options);
|
||||
}
|
||||
|
||||
static getOrCreateScope(id: string, options: ScopeOptions = {}): ComponentScope {
|
||||
return this.instances.get(id) || new ComponentScope(id, options);
|
||||
}
|
||||
|
||||
static disposeScope(id: string): void {
|
||||
const scope = this.instances.get(id);
|
||||
if (scope) {
|
||||
scope.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a hook for React components to access scope
|
||||
static createScopeHook(scopeId: string) {
|
||||
return function useScopeValue(name: string) {
|
||||
const scope = ComponentScope.getScope(scopeId);
|
||||
if (!scope) {
|
||||
throw new Error(`Scope "${scopeId}" not found`);
|
||||
}
|
||||
return scope.get(name);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a new component scope
|
||||
export function createComponentScope(
|
||||
id: string,
|
||||
app?: App,
|
||||
parentScope?: ComponentScope
|
||||
): ComponentScope {
|
||||
return new ComponentScope(id, { app, parentScope });
|
||||
}
|
||||
|
||||
// React hook for accessing scope
|
||||
export function useComponentScope(scopeId: string) {
|
||||
return React.useMemo(() => {
|
||||
const scope = ComponentScope.getScope(scopeId);
|
||||
if (!scope) {
|
||||
throw new Error(`Scope "${scopeId}" not found`);
|
||||
}
|
||||
return scope;
|
||||
}, [scopeId]);
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
/*
|
||||
// Create a global scope
|
||||
const globalScope = new ComponentScope('global', { isGlobal: true });
|
||||
globalScope.set('sharedValue', 42);
|
||||
|
||||
// Create a component scope
|
||||
const componentScope = createComponentScope('component-1', app);
|
||||
componentScope.set('localValue', 'hello');
|
||||
|
||||
// In a React component:
|
||||
function MyComponent() {
|
||||
const scope = useComponentScope('component-1');
|
||||
const localValue = scope.get('localValue');
|
||||
const sharedValue = scope.get('sharedValue');
|
||||
|
||||
return (
|
||||
<div>
|
||||
Local: {localValue}, Shared: {sharedValue}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// src/core/tailwind.ts
|
||||
import postcss from 'postcss';
|
||||
import tailwindcss from 'tailwindcss';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
|
||||
export class TailwindProcessor {
|
||||
private config: any;
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
// Disable preflight to avoid conflicts with Obsidian
|
||||
corePlugins: {
|
||||
preflight: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Process Tailwind CSS
|
||||
async process(css: string): Promise<string> {
|
||||
const result = await postcss([
|
||||
tailwindcss(this.config),
|
||||
autoprefixer
|
||||
]).process(css, {
|
||||
from: undefined
|
||||
});
|
||||
|
||||
return result.css;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
// src/hooks/useMarketData.tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { TFile } from 'obsidian';
|
||||
import { StorageManager } from '../core/storage';
|
||||
import { MarketDataService } from 'src/services/marketDataService';
|
||||
import { MarketData, MarketDataCache } from '../services/marketDataService';
|
||||
|
||||
interface UseMarketDataResult {
|
||||
data: MarketData[];
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
export function useMarketData(
|
||||
symbol: string,
|
||||
interval: string,
|
||||
range: string,
|
||||
storage: StorageManager,
|
||||
noteFile: TFile
|
||||
) {
|
||||
const [data, setData] = useState<MarketData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const marketData = await storage.getMarketData(
|
||||
symbol,
|
||||
interval,
|
||||
range,
|
||||
noteFile
|
||||
);
|
||||
setData(marketData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Unknown error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [symbol, interval, range, noteFile, storage]);
|
||||
|
||||
return { data, loading, error };
|
||||
}
|
||||
export function createMarketDataHook(storage: StorageManager, noteFile: TFile) {
|
||||
return function useMarketData(
|
||||
symbol: string,
|
||||
interval: string,
|
||||
range: string
|
||||
): UseMarketDataResult {
|
||||
const [data, setData] = useState<MarketData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const cacheKey = `market-data-${symbol}-${interval}-${range}`;
|
||||
|
||||
// Try to get from storage first
|
||||
const cached = await storage.get<MarketDataCache | null>(
|
||||
cacheKey,
|
||||
null,
|
||||
noteFile
|
||||
);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < 24 * 60 * 60 * 1000) {
|
||||
setData(cached.data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
const {data:freshData} = await MarketDataService.fetchHistoricalData(symbol);
|
||||
|
||||
// Cache the new data
|
||||
await storage.set(
|
||||
cacheKey,
|
||||
{ data: freshData, timestamp: Date.now() },
|
||||
noteFile
|
||||
);
|
||||
|
||||
setData(freshData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Unknown error'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [symbol, interval, range]);
|
||||
|
||||
return { data, loading, error };
|
||||
};
|
||||
}
|
||||
|
|
@ -1,793 +0,0 @@
|
|||
// src/services/OrderBlockAnalysis.ts
|
||||
import { TFile } from 'obsidian';
|
||||
import { MarketDataService, MarketData } from './marketDataService';
|
||||
import { MarketDataStorage } from './marketDataStorage';
|
||||
|
||||
interface SwingPoint {
|
||||
time: number;
|
||||
price: number;
|
||||
type: 'high' | 'low';
|
||||
index: number;
|
||||
}
|
||||
interface TrendLeg {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
direction: 'up' | 'down' | 'sideways';
|
||||
strength: number;
|
||||
startPrice: number;
|
||||
endPrice: number;
|
||||
swingPoints: SwingPoint[];
|
||||
}
|
||||
interface OrderBlock {
|
||||
id: string;
|
||||
type: 'bullish' | 'bearish';
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
highPrice: number;
|
||||
lowPrice: number;
|
||||
volume: number;
|
||||
impulseMagnitude: number;
|
||||
priceValueGap: number;
|
||||
validationMetrics: {
|
||||
trendAlignment: boolean;
|
||||
hasBreakOfStructure: boolean;
|
||||
gapQuality: number; // 0-1 score
|
||||
impulseStrength: number; // Relative to average movement
|
||||
};
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
symbol: string;
|
||||
timeframe: string;
|
||||
analysisTime: number;
|
||||
marketHours: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
marketData: MarketData[]; // Add this
|
||||
trendLegs: TrendLeg[]; // Add this
|
||||
trend: {
|
||||
direction: 'up' | 'down' | 'none';
|
||||
strength: number;
|
||||
swingPoints: SwingPoint[];
|
||||
};
|
||||
orderBlocks: OrderBlock[];
|
||||
}
|
||||
|
||||
export class OrderBlockAnalysisService {
|
||||
private static readonly MARKET_HOURS = {
|
||||
start: { hour: 9, minute: 30 },
|
||||
end: { hour: 16, minute: 0 }
|
||||
};
|
||||
// Analysis specific params
|
||||
private static readonly ANALYSIS_PARAMS = {
|
||||
minSwingPoints: 4,
|
||||
lookbackPeriods: 20,
|
||||
minImpulseStrength: 1.5,
|
||||
minGapSize: 0.1, // 10% of average candle size
|
||||
trendStrengthThreshold: 0.6
|
||||
};
|
||||
private static readonly TREND_PARAMS = {
|
||||
minSwingPoints: 2, // Reduced minimum swing points
|
||||
trendThreshold: 0.4, // Lowered threshold
|
||||
minMovementSize: 0.1, // Minimum % move
|
||||
swingPointConfirmation: 1, // Candles to confirm swing
|
||||
momentumThreshold: 0.6, // New: Momentum strength threshold
|
||||
priceChangeThreshold: 0.001 // New: Minimum price change (0.1%)
|
||||
};
|
||||
constructor(
|
||||
private storage: MarketDataStorage,
|
||||
private marketDataService: MarketDataService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Main analysis function for a given symbol and timeframe
|
||||
*/
|
||||
async analyzeOrderBlocks(
|
||||
symbol: string,
|
||||
timeframe: string,
|
||||
noteFile: TFile
|
||||
): Promise<AnalysisResult> {
|
||||
try{
|
||||
const today = new Date();
|
||||
const startTime = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate()-1,
|
||||
OrderBlockAnalysisService.MARKET_HOURS.start.hour,
|
||||
OrderBlockAnalysisService.MARKET_HOURS.start.minute
|
||||
).getTime();
|
||||
|
||||
const endTime = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate()-1,
|
||||
OrderBlockAnalysisService.MARKET_HOURS.end.hour,
|
||||
OrderBlockAnalysisService.MARKET_HOURS.end.minute
|
||||
).getTime();
|
||||
|
||||
const config = {
|
||||
interval: timeframe as any,
|
||||
startTime,
|
||||
endTime
|
||||
};
|
||||
|
||||
// Use the marketDataService instance for fetching
|
||||
const data = await MarketDataService.getMarketData(symbol, config, this.storage);
|
||||
|
||||
// Apply market hours filter
|
||||
const marketHoursData = this.filterMarketHours(data);
|
||||
|
||||
const trend = this.analyzeTrend(marketHoursData);
|
||||
const orderBlocks = this.findOrderBlocks(marketHoursData, trend);
|
||||
|
||||
return {
|
||||
symbol,
|
||||
timeframe,
|
||||
analysisTime: Date.now(),
|
||||
marketHours: {
|
||||
start: startTime,
|
||||
end: endTime
|
||||
},
|
||||
marketData: marketHoursData,
|
||||
trendLegs: this.analyzeTrendLegs(marketHoursData),
|
||||
trend,
|
||||
orderBlocks
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error analyzing order blocks:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter data for market hours only
|
||||
*/
|
||||
private filterMarketHours(data: MarketData[]): MarketData[] {
|
||||
return data.filter(candle => {
|
||||
const date = new Date(candle.time);
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
if (hours < OrderBlockAnalysisService.MARKET_HOURS.start.hour ||
|
||||
hours > OrderBlockAnalysisService.MARKET_HOURS.end.hour) return false;
|
||||
|
||||
if (hours === OrderBlockAnalysisService.MARKET_HOURS.start.hour &&
|
||||
minutes < OrderBlockAnalysisService.MARKET_HOURS.start.minute) return false;
|
||||
|
||||
if (hours === OrderBlockAnalysisService.MARKET_HOURS.end.hour &&
|
||||
minutes >= OrderBlockAnalysisService.MARKET_HOURS.end.minute) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced trend analysis that considers both structure and momentum
|
||||
*/
|
||||
private analyzeTrend(data: MarketData[]): {
|
||||
direction: 'up' | 'down' | 'none';
|
||||
strength: number;
|
||||
swingPoints: SwingPoint[];
|
||||
} {
|
||||
if (data.length < 10) return { direction: 'none', strength: 0, swingPoints: [] };
|
||||
|
||||
const atr = this.calculateATR(data, 14);
|
||||
const swingPoints = this.findSignificantSwings(data, atr);
|
||||
|
||||
// Calculate overall momentum
|
||||
const momentum = this.calculateMomentum(data);
|
||||
|
||||
// Calculate price movement
|
||||
const priceChange = (data[data.length - 1].close - data[0].close) / data[0].close;
|
||||
const absolutePriceChange = Math.abs(priceChange);
|
||||
|
||||
// Only proceed with detailed analysis if we have significant price movement
|
||||
if (absolutePriceChange < OrderBlockAnalysisService.TREND_PARAMS.priceChangeThreshold) {
|
||||
return { direction: 'none', strength: 0, swingPoints };
|
||||
}
|
||||
|
||||
// Get structural analysis
|
||||
const structure = this.analyzeTrendStructure(swingPoints, atr);
|
||||
|
||||
// Combine structural and momentum analysis
|
||||
const direction = this.determineOverallTrend(structure, momentum, priceChange);
|
||||
const strength = this.calculateOverallStrength(structure.strength, momentum.strength);
|
||||
|
||||
return {
|
||||
direction,
|
||||
strength,
|
||||
swingPoints
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate Average True Range
|
||||
*/
|
||||
private calculateATR(data: MarketData[], period: number): number {
|
||||
if (data.length < period) return 0;
|
||||
|
||||
let tr = [];
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const high = data[i].high;
|
||||
const low = data[i].low;
|
||||
const prevClose = data[i-1].close;
|
||||
|
||||
tr.push(Math.max(
|
||||
high - low,
|
||||
Math.abs(high - prevClose),
|
||||
Math.abs(low - prevClose)
|
||||
));
|
||||
}
|
||||
|
||||
// Calculate simple moving average of TR
|
||||
const atr = tr.slice(-period).reduce((sum, val) => sum + val, 0) / period;
|
||||
return atr;
|
||||
}
|
||||
/**
|
||||
* Calculate price momentum
|
||||
*/
|
||||
private calculateMomentum(data: MarketData[]): { direction: 'up' | 'down'; strength: number } {
|
||||
const closes = data.map(d => d.close);
|
||||
let upMoves = 0;
|
||||
let downMoves = 0;
|
||||
|
||||
// Count consecutive moves
|
||||
for (let i = 1; i < closes.length; i++) {
|
||||
if (closes[i] > closes[i - 1]) upMoves++;
|
||||
if (closes[i] < closes[i - 1]) downMoves++;
|
||||
}
|
||||
|
||||
const totalMoves = upMoves + downMoves;
|
||||
const upStrength = upMoves / totalMoves;
|
||||
const downStrength = downMoves / totalMoves;
|
||||
|
||||
return {
|
||||
direction: upStrength > downStrength ? 'up' : 'down',
|
||||
strength: Math.max(upStrength, downStrength)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine overall trend combining structure and momentum
|
||||
*/
|
||||
private determineOverallTrend(
|
||||
structure: { direction: 'up' | 'down' | 'none'; strength: number },
|
||||
momentum: { direction: 'up' | 'down'; strength: number },
|
||||
priceChange: number
|
||||
): 'up' | 'down' | 'none' {
|
||||
// Strong momentum overrides structure
|
||||
if (momentum.strength > OrderBlockAnalysisService.TREND_PARAMS.momentumThreshold) {
|
||||
return momentum.direction;
|
||||
}
|
||||
|
||||
// Strong structure with confirming price change
|
||||
if (structure.direction !== 'none' &&
|
||||
Math.sign(priceChange) === (structure.direction === 'up' ? 1 : -1)) {
|
||||
return structure.direction;
|
||||
}
|
||||
|
||||
// Default to momentum direction if price change confirms it
|
||||
if (Math.sign(priceChange) === (momentum.direction === 'up' ? 1 : -1)) {
|
||||
return momentum.direction;
|
||||
}
|
||||
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overall trend strength
|
||||
*/
|
||||
private calculateOverallStrength(structureStrength: number, momentumStrength: number): number {
|
||||
return (structureStrength * 0.6 + momentumStrength * 0.4);
|
||||
}
|
||||
/**
|
||||
* Find significant swing points using ATR for context
|
||||
*/
|
||||
private findSignificantSwings(data: MarketData[], atr: number): SwingPoint[] {
|
||||
const swingPoints: SwingPoint[] = [];
|
||||
const minMove = atr * 0.3; // Reduced from 0.5 to catch more potential swings
|
||||
|
||||
for (let i = 2; i < data.length - 2; i++) {
|
||||
const current = data[i];
|
||||
const before = data.slice(i - 2, i);
|
||||
const after = data.slice(i + 1, i + 3);
|
||||
|
||||
// Check for swing high with relaxed conditions
|
||||
if (before.every(c => c.high <= current.high) &&
|
||||
after[0].high < current.high) {
|
||||
swingPoints.push({
|
||||
type: 'high',
|
||||
price: current.high,
|
||||
time: current.time,
|
||||
index: i
|
||||
});
|
||||
}
|
||||
|
||||
// Check for swing low with relaxed conditions
|
||||
if (before.every(c => c.low >= current.low) &&
|
||||
after[0].low > current.low) {
|
||||
swingPoints.push({
|
||||
type: 'low',
|
||||
price: current.low,
|
||||
time: current.time,
|
||||
index: i
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return swingPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze trend structure using swing points
|
||||
*/
|
||||
private analyzeTrendStructure(
|
||||
swingPoints: SwingPoint[],
|
||||
atr: number
|
||||
): { direction: 'up' | 'down' | 'none'; strength: number; } {
|
||||
const highs = swingPoints.filter(p => p.type === 'high')
|
||||
.sort((a, b) => a.time - b.time);
|
||||
const lows = swingPoints.filter(p => p.type === 'low')
|
||||
.sort((a, b) => a.time - b.time);
|
||||
|
||||
if (highs.length < 2 || lows.length < 2) {
|
||||
return { direction: 'none', strength: 0 };
|
||||
}
|
||||
|
||||
// Calculate trend metrics
|
||||
const hhSequence = this.calculateSequenceStrength(highs, 'up', atr);
|
||||
const lhSequence = this.calculateSequenceStrength(highs, 'down', atr);
|
||||
const hlSequence = this.calculateSequenceStrength(lows, 'up', atr);
|
||||
const llSequence = this.calculateSequenceStrength(lows, 'down', atr);
|
||||
|
||||
// Determine trend
|
||||
const upStrength = (hhSequence + hlSequence) / 2;
|
||||
const downStrength = (lhSequence + llSequence) / 2;
|
||||
|
||||
if (upStrength > OrderBlockAnalysisService.TREND_PARAMS.trendThreshold &&
|
||||
upStrength > downStrength) {
|
||||
return {
|
||||
direction: 'up',
|
||||
strength: upStrength
|
||||
};
|
||||
}
|
||||
|
||||
if (downStrength > OrderBlockAnalysisService.TREND_PARAMS.trendThreshold &&
|
||||
downStrength > upStrength) {
|
||||
return {
|
||||
direction: 'down',
|
||||
strength: downStrength
|
||||
};
|
||||
}
|
||||
|
||||
return { direction: 'none', strength: 0 };
|
||||
}
|
||||
private analyzeTrendLegs(data: MarketData[]): TrendLeg[] {
|
||||
const trendLegs: TrendLeg[] = [];
|
||||
const atr = this.calculateATR(data, 14);
|
||||
let currentLegStart = 0;
|
||||
|
||||
// Minimum number of candles to consider a valid trend leg
|
||||
const MIN_LEG_LENGTH = 5;
|
||||
|
||||
// Loop through data to identify trend changes
|
||||
for (let i = MIN_LEG_LENGTH; i < data.length - MIN_LEG_LENGTH; i++) {
|
||||
const currentSegment = data.slice(currentLegStart, i + 1);
|
||||
const nextSegment = data.slice(i - MIN_LEG_LENGTH, i + MIN_LEG_LENGTH);
|
||||
|
||||
// Check for trend change
|
||||
if (this.isTrendChange(currentSegment, nextSegment, atr)) {
|
||||
// Add completed trend leg
|
||||
if (i - currentLegStart >= MIN_LEG_LENGTH) {
|
||||
const legData = data.slice(currentLegStart, i);
|
||||
const trendLeg = this.analyzeSingleTrendLeg(legData, currentLegStart, i);
|
||||
trendLegs.push(trendLeg);
|
||||
}
|
||||
currentLegStart = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Add final trend leg
|
||||
if (data.length - currentLegStart >= MIN_LEG_LENGTH) {
|
||||
const finalLegData = data.slice(currentLegStart);
|
||||
const finalTrendLeg = this.analyzeSingleTrendLeg(
|
||||
finalLegData,
|
||||
currentLegStart,
|
||||
data.length - 1
|
||||
);
|
||||
trendLegs.push(finalTrendLeg);
|
||||
}
|
||||
|
||||
return trendLegs;
|
||||
}
|
||||
|
||||
private isTrendChange(
|
||||
currentSegment: MarketData[],
|
||||
nextSegment: MarketData[],
|
||||
atr: number
|
||||
): boolean {
|
||||
const currentDirection = this.determineTrendDirection(currentSegment);
|
||||
const nextDirection = this.determineTrendDirection(nextSegment);
|
||||
|
||||
// Check for direction change
|
||||
if (currentDirection !== nextDirection) {
|
||||
// Verify change is significant (> 1 ATR)
|
||||
const priceChange = Math.abs(
|
||||
nextSegment[nextSegment.length - 1].close - nextSegment[0].close
|
||||
);
|
||||
return priceChange > atr;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private analyzeSingleTrendLeg(
|
||||
data: MarketData[],
|
||||
startIndex: number,
|
||||
endIndex: number
|
||||
): TrendLeg {
|
||||
const swingPoints = this.findSignificantSwings(data, this.calculateATR(data, 14));
|
||||
const priceChange = (data[data.length - 1].close - data[0].close) / data[0].close;
|
||||
const momentum = this.calculateMomentum(data);
|
||||
|
||||
// Determine trend direction
|
||||
let direction: 'up' | 'down' | 'sideways';
|
||||
if (Math.abs(priceChange) < OrderBlockAnalysisService.TREND_PARAMS.priceChangeThreshold) {
|
||||
direction = 'sideways';
|
||||
} else {
|
||||
direction = priceChange > 0 ? 'up' : 'down';
|
||||
}
|
||||
|
||||
// Calculate trend strength based on:
|
||||
// 1. Price change magnitude
|
||||
// 2. Momentum consistency
|
||||
// 3. Swing point alignment
|
||||
const priceStrength = Math.min(Math.abs(priceChange) * 10, 1); // Scale price change
|
||||
const momentumStrength = momentum.strength;
|
||||
const swingStrength = this.calculateSwingAlignment(swingPoints, direction);
|
||||
|
||||
const strength = (
|
||||
priceStrength * 0.4 +
|
||||
momentumStrength * 0.4 +
|
||||
swingStrength * 0.2
|
||||
) * 100; // Convert to percentage
|
||||
|
||||
return {
|
||||
startIndex,
|
||||
endIndex,
|
||||
startTime: data[0].time,
|
||||
endTime: data[data.length - 1].time,
|
||||
direction,
|
||||
strength,
|
||||
startPrice: data[0].close,
|
||||
endPrice: data[data.length - 1].close,
|
||||
swingPoints
|
||||
};
|
||||
}
|
||||
|
||||
private calculateSwingAlignment(
|
||||
swingPoints: SwingPoint[],
|
||||
direction: 'up' | 'down' | 'sideways'
|
||||
): number {
|
||||
if (direction === 'sideways' || swingPoints.length < 2) return 0;
|
||||
|
||||
let aligned = 0;
|
||||
let total = 0;
|
||||
|
||||
for (let i = 1; i < swingPoints.length; i++) {
|
||||
if (direction === 'up') {
|
||||
if (swingPoints[i].price > swingPoints[i - 1].price) aligned++;
|
||||
} else {
|
||||
if (swingPoints[i].price < swingPoints[i - 1].price) aligned++;
|
||||
}
|
||||
total++;
|
||||
}
|
||||
|
||||
return total > 0 ? aligned / total : 0;
|
||||
}
|
||||
|
||||
private determineTrendDirection(data: MarketData[]): 'up' | 'down' | 'sideways' {
|
||||
const priceChange = (data[data.length - 1].close - data[0].close) / data[0].close;
|
||||
|
||||
if (Math.abs(priceChange) < OrderBlockAnalysisService.TREND_PARAMS.priceChangeThreshold) {
|
||||
return 'sideways';
|
||||
}
|
||||
return priceChange > 0 ? 'up' : 'down';
|
||||
}
|
||||
/**
|
||||
* Calculate sequence strength relative to ATR
|
||||
*/
|
||||
private calculateSequenceStrength(
|
||||
points: SwingPoint[],
|
||||
direction: 'up' | 'down',
|
||||
atr: number
|
||||
): number {
|
||||
if (points.length < 2) return 0;
|
||||
|
||||
let validMoves = 0;
|
||||
let totalMoves = 0;
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const move = direction === 'up'
|
||||
? points[i].price - points[i-1].price
|
||||
: points[i-1].price - points[i].price;
|
||||
|
||||
if (move > 0 && Math.abs(move) > atr * 0.5) {
|
||||
validMoves++;
|
||||
}
|
||||
totalMoves++;
|
||||
}
|
||||
|
||||
return totalMoves > 0 ? validMoves / totalMoves : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate trend strength (0-1)
|
||||
*/
|
||||
private calculateTrendStrength(
|
||||
points: SwingPoint[],
|
||||
direction: 'up' | 'down'
|
||||
): number {
|
||||
if (points.length < 2) return 0;
|
||||
|
||||
let increasing = 0;
|
||||
let total = 0;
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const current = points[i].price;
|
||||
const previous = points[i - 1].price;
|
||||
|
||||
if (direction === 'up' && current > previous) increasing++;
|
||||
if (direction === 'down' && current < previous) increasing++;
|
||||
total++;
|
||||
}
|
||||
|
||||
return total > 0 ? increasing / total : 0;
|
||||
}
|
||||
/**
|
||||
* Find order blocks
|
||||
*/
|
||||
private findOrderBlocks(
|
||||
data: MarketData[],
|
||||
trend: { direction: 'up' | 'down' | 'none'; strength: number; }
|
||||
): OrderBlock[] {
|
||||
const orderBlocks: OrderBlock[] = [];
|
||||
|
||||
// Only proceed if we have a clear trend
|
||||
if (trend.direction === 'none' || trend.strength < OrderBlockAnalysisService.ANALYSIS_PARAMS.trendStrengthThreshold) {
|
||||
return orderBlocks;
|
||||
}
|
||||
|
||||
// Use a sliding window of 7 candles (3 before, current, 3 after)
|
||||
for (let i = 3; i < data.length - 3; i++) {
|
||||
const window = {
|
||||
before: data.slice(i - 3, i),
|
||||
current: data[i],
|
||||
after: data.slice(i + 1, i + 4)
|
||||
};
|
||||
|
||||
// Check for potential order block
|
||||
const orderBlock = this.validateOrderBlock(window, trend.direction);
|
||||
if (orderBlock) {
|
||||
orderBlocks.push(orderBlock);
|
||||
}
|
||||
}
|
||||
|
||||
return this.filterOverlappingBlocks(orderBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate potential order block
|
||||
*/
|
||||
private validateOrderBlock(
|
||||
window: {
|
||||
before: MarketData[];
|
||||
current: MarketData;
|
||||
after: MarketData[];
|
||||
},
|
||||
trendDirection: 'up' | 'down'
|
||||
): OrderBlock | null {
|
||||
const { before, current, after } = window;
|
||||
|
||||
// Check for bullish order block in uptrend
|
||||
if (trendDirection === 'up') {
|
||||
const isBearishCandle = current.close < current.open;
|
||||
const hasImpulsiveMove = this.hasImpulsiveMove(after, 'up');
|
||||
const pvg = this.calculatePriceValueGap([...before, current, ...after]);
|
||||
|
||||
if (isBearishCandle && hasImpulsiveMove && pvg > OrderBlockAnalysisService.ANALYSIS_PARAMS.minGapSize) {
|
||||
return {
|
||||
id: `OB_${current.time}`,
|
||||
type: 'bullish',
|
||||
startTime: current.time,
|
||||
endTime: after[0].time,
|
||||
highPrice: current.high,
|
||||
lowPrice: current.low,
|
||||
volume: current.volume,
|
||||
impulseMagnitude: this.calculateImpulseMagnitude([...before, current, ...after]),
|
||||
priceValueGap: pvg,
|
||||
validationMetrics: {
|
||||
trendAlignment: true,
|
||||
hasBreakOfStructure: this.hasBreakOfStructure([...before, current, ...after], 'up'),
|
||||
gapQuality: this.calculateGapQuality(pvg, current),
|
||||
impulseStrength: this.calculateImpulseStrength(after, before)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for bearish order block in downtrend
|
||||
if (trendDirection === 'down') {
|
||||
const isBullishCandle = current.close > current.open;
|
||||
const hasImpulsiveMove = this.hasImpulsiveMove(after, 'down');
|
||||
const pvg = this.calculatePriceValueGap([...before, current, ...after]);
|
||||
|
||||
if (isBullishCandle && hasImpulsiveMove && pvg > OrderBlockAnalysisService.ANALYSIS_PARAMS.minGapSize) {
|
||||
return {
|
||||
id: `OB_${current.time}`,
|
||||
type: 'bearish',
|
||||
startTime: current.time,
|
||||
endTime: after[0].time,
|
||||
highPrice: current.high,
|
||||
lowPrice: current.low,
|
||||
volume: current.volume,
|
||||
impulseMagnitude: this.calculateImpulseMagnitude([...before, current, ...after]),
|
||||
priceValueGap: pvg,
|
||||
validationMetrics: {
|
||||
trendAlignment: true,
|
||||
hasBreakOfStructure: this.hasBreakOfStructure([...before, current, ...after], 'down'),
|
||||
gapQuality: this.calculateGapQuality(pvg, current),
|
||||
impulseStrength: this.calculateImpulseStrength(after, before)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate price value gap
|
||||
*/
|
||||
private calculatePriceValueGap(candles: MarketData[]): number {
|
||||
let maxGap = 0;
|
||||
|
||||
for (let i = 1; i < candles.length - 1; i++) {
|
||||
const current = candles[i];
|
||||
const next = candles[i + 1];
|
||||
|
||||
// Check for gap between candles
|
||||
if (next.low > current.high) {
|
||||
maxGap = Math.max(maxGap, next.low - current.high);
|
||||
}
|
||||
if (next.high < current.low) {
|
||||
maxGap = Math.max(maxGap, current.low - next.high);
|
||||
}
|
||||
}
|
||||
|
||||
return maxGap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for impulsive move
|
||||
*/
|
||||
private hasImpulsiveMove(candles: MarketData[], direction: 'up' | 'down'): boolean {
|
||||
const moves = candles.map(c => Math.abs(c.close - c.open));
|
||||
const avgMove = moves.reduce((sum, move) => sum + move, 0) / moves.length;
|
||||
|
||||
if (direction === 'up') {
|
||||
return candles[0].close > candles[0].open &&
|
||||
avgMove > OrderBlockAnalysisService.ANALYSIS_PARAMS.minImpulseStrength;
|
||||
} else {
|
||||
return candles[0].close < candles[0].open &&
|
||||
avgMove > OrderBlockAnalysisService.ANALYSIS_PARAMS.minImpulseStrength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate impulse magnitude
|
||||
*/
|
||||
private calculateImpulseMagnitude(candles: MarketData[]): number {
|
||||
const moves = candles.map(c => ({
|
||||
move: Math.abs(c.close - c.open),
|
||||
volume: c.volume
|
||||
}));
|
||||
|
||||
// Volume-weighted average move
|
||||
const weightedMoves = moves.map(m => m.move * (m.volume / Math.max(...moves.map(x => x.volume))));
|
||||
return weightedMoves.reduce((sum, move) => sum + move, 0) / weightedMoves.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate gap quality (0-1)
|
||||
*/
|
||||
private calculateGapQuality(gap: number, candle: MarketData): number {
|
||||
const candleSize = Math.abs(candle.high - candle.low);
|
||||
return Math.min(gap / candleSize, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate impulse strength
|
||||
*/
|
||||
private calculateImpulseStrength(after: MarketData[], before: MarketData[]): number {
|
||||
const afterMoves = after.map(c => Math.abs(c.close - c.open));
|
||||
const beforeMoves = before.map(c => Math.abs(c.close - c.open));
|
||||
|
||||
const avgAfter = afterMoves.reduce((sum, move) => sum + move, 0) / afterMoves.length;
|
||||
const avgBefore = beforeMoves.reduce((sum, move) => sum + move, 0) / beforeMoves.length;
|
||||
|
||||
return avgAfter / avgBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for break of structure
|
||||
*/
|
||||
private hasBreakOfStructure(candles: MarketData[], direction: 'up' | 'down'): boolean {
|
||||
const middle = Math.floor(candles.length / 2);
|
||||
const before = candles.slice(0, middle);
|
||||
const after = candles.slice(middle);
|
||||
|
||||
if (direction === 'up') {
|
||||
const beforeHigh = Math.max(...before.map(c => c.high));
|
||||
const afterHigh = Math.max(...after.map(c => c.high));
|
||||
return afterHigh > beforeHigh;
|
||||
} else {
|
||||
const beforeLow = Math.min(...before.map(c => c.low));
|
||||
const afterLow = Math.min(...after.map(c => c.low));
|
||||
return afterLow < beforeLow;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter overlapping order blocks
|
||||
*/
|
||||
private filterOverlappingBlocks(blocks: OrderBlock[]): OrderBlock[] {
|
||||
return blocks.filter((block, index) => {
|
||||
// Check if this block overlaps with any stronger blocks
|
||||
const hasStrongerOverlap = blocks.some((otherBlock, otherIndex) => {
|
||||
if (index === otherIndex) return false;
|
||||
|
||||
const timeOverlap = block.startTime <= otherBlock.endTime &&
|
||||
block.endTime >= otherBlock.startTime;
|
||||
|
||||
const priceOverlap = block.lowPrice <= otherBlock.highPrice &&
|
||||
block.highPrice >= otherBlock.lowPrice;
|
||||
|
||||
return timeOverlap && priceOverlap &&
|
||||
otherBlock.validationMetrics.impulseStrength >
|
||||
block.validationMetrics.impulseStrength;
|
||||
});
|
||||
|
||||
return !hasStrongerOverlap;
|
||||
});
|
||||
}
|
||||
|
||||
private getTodayMarketOpen(): number {
|
||||
const now = new Date();
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
OrderBlockAnalysisService.MARKET_HOURS.start.hour,
|
||||
OrderBlockAnalysisService.MARKET_HOURS.start.minute
|
||||
).getTime();
|
||||
}
|
||||
|
||||
private getTodayMarketClose(): number {
|
||||
const now = new Date();
|
||||
return new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
OrderBlockAnalysisService.MARKET_HOURS.end.hour,
|
||||
OrderBlockAnalysisService.MARKET_HOURS.end.minute
|
||||
).getTime();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export type { AnalysisResult, OrderBlock, SwingPoint };
|
||||
|
|
@ -1,459 +0,0 @@
|
|||
// src/services/marketDataService.ts
|
||||
import { error, time } from "console";
|
||||
import { MarketDataStorage } from "./marketDataStorage";
|
||||
import { TFile } from "obsidian";
|
||||
import dotenv from 'dotenv';
|
||||
// Initialize dotenv
|
||||
dotenv.config();
|
||||
|
||||
// Create a config object to manage environment variables
|
||||
const config = {
|
||||
primaryApiKey: process.env.ALPHA_VANTAGE_PRIMARY_KEY,
|
||||
secondaryApiKey: process.env.ALPHA_VANTAGE_SECONDARY_KEY,
|
||||
cacheDuration: parseInt(process.env.MARKET_DATA_CACHE_DURATION || '86400000'),
|
||||
timezone: process.env.TIMEZONE || 'US/Eastern',
|
||||
|
||||
// Validate environment variables
|
||||
validate() {
|
||||
if (!this.primaryApiKey) {
|
||||
throw new Error('ALPHA_VANTAGE_PRIMARY_KEY is not set in environment variables');
|
||||
}
|
||||
if (!this.secondaryApiKey) {
|
||||
throw new Error('ALPHA_VANTAGE_SECONDARY_KEY is not set in environment variables');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export interface MarketData {
|
||||
time: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
sma?: number; // Simple Moving Average
|
||||
ema?: number; // Exponential Moving Average
|
||||
rsi?: number; // Relative Strength Index
|
||||
}
|
||||
export interface MarketDataCache {
|
||||
data: MarketData[];
|
||||
timestamp: number;
|
||||
}
|
||||
export interface MarketHours {
|
||||
start: number; // 4:00 AM ET
|
||||
end: number; // 19:59 PM ET
|
||||
timeZone: string; // 'America/New_York'
|
||||
}
|
||||
export type IntervalType = '1min' | '5min' | '15min' | '30min' | '60min' | 'daily';
|
||||
|
||||
export interface TimeframeConfig {
|
||||
interval: IntervalType;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export interface CacheEntry {
|
||||
data: MarketData[];
|
||||
timestamp: number;
|
||||
interval: IntervalType;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
timezone:string;
|
||||
}
|
||||
|
||||
export interface CacheMetadata {
|
||||
[symbol: string]: {
|
||||
[interval in IntervalType]?: CacheEntry[];
|
||||
};
|
||||
}
|
||||
export class MarketDataService {
|
||||
static async fetchHistoricalData(
|
||||
symbol: string,
|
||||
outputsize: 'full' | 'compact' = 'full'
|
||||
): Promise<{data:MarketData[],timezone: string}> {
|
||||
// Validate environment variables
|
||||
config.validate();
|
||||
|
||||
const url = `https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=${symbol}&outputsize=${outputsize}&apikey=${config.primaryApiKey}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
// Check for API errors
|
||||
if (data['Error Message']) {
|
||||
throw new Error(data['Error Message']);
|
||||
}
|
||||
|
||||
const timezone = data['Meta Data']['6. Time Zone'];
|
||||
const timeSeries = data['Time Series (Daily)'];
|
||||
|
||||
const marketData= Object.entries(timeSeries).map(([date, values]: [string, any]) => ({
|
||||
time: new Date(date).getTime(),
|
||||
open: parseFloat(values['1. open']),
|
||||
high: parseFloat(values['2. high']),
|
||||
low: parseFloat(values['3. low']),
|
||||
close: parseFloat(values['4. close']),
|
||||
volume: parseInt(values['5. volume'])
|
||||
})).reverse();
|
||||
return{data:marketData,timezone}
|
||||
} catch (error) {
|
||||
console.error('Error fetching historical data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async fetchIntraday(
|
||||
symbol: string,
|
||||
interval: '1min' | '5min' | '15min' | '30min' | '60min' = '5min'
|
||||
): Promise<{data: MarketData[], timezone: string }> {
|
||||
// Validate environment variables
|
||||
config.validate();
|
||||
|
||||
const url = `https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=${symbol}&interval=${interval}&outputsize=full&apikey=${config.secondaryApiKey}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
// Check for rate limit message
|
||||
/* if (data.Information && data.Information.includes('rate limit')) {
|
||||
throw new Error('API rate limit reached. Please try again in a minute.');
|
||||
}*/
|
||||
// Log full response to see structure
|
||||
console.log('Alpha Vantage Response:', data);
|
||||
|
||||
// Method 1: Log all top-level keys
|
||||
console.log('Top level keys:', Object.keys(data));
|
||||
const timezone = data['Meta Data']['5. Time Zone'];
|
||||
const timeSeriesKey = `Time Series (${interval})`;
|
||||
const timeSeries = data[timeSeriesKey];
|
||||
|
||||
if (!timeSeries) {
|
||||
throw new Error('No data returned from API. You may have exceeded the daily limit.');
|
||||
}
|
||||
|
||||
const marketData= Object.entries(timeSeries).map(([date, values]: [string, any]) => ({
|
||||
time: new Date(date).getTime(),
|
||||
open: parseFloat(values['1. open']),
|
||||
high: parseFloat(values['2. high']),
|
||||
low: parseFloat(values['3. low']),
|
||||
close: parseFloat(values['4. close']),
|
||||
volume: parseInt(values['5. volume'])
|
||||
})).reverse();
|
||||
|
||||
return { data: marketData, timezone}
|
||||
} catch (error) {
|
||||
console.error('Error fetching intraday data:', error);
|
||||
throw error;
|
||||
//For when we want to hide implementation details
|
||||
//throw new Error('Unable to fetch intraday data. Please try again later.');
|
||||
}
|
||||
}
|
||||
static readonly MARKET_HOURS: MarketHours = {
|
||||
start: 4 * 60, // 4:00 AM in minutes
|
||||
end: 20 * 60 - 1, // 19:59 PM in minutes
|
||||
timeZone: config.timezone
|
||||
};
|
||||
|
||||
// Add method for getting multiple timeframes in one call
|
||||
static async fetchMultiTimeframe(
|
||||
symbol: string,
|
||||
timeframes: Array<'1min' | '5min' | '15min' | '30min' | '60min' | 'daily'>
|
||||
): Promise<Record<string, MarketData[]>> {
|
||||
try {
|
||||
const results: Record<string, MarketData[]> = {};
|
||||
|
||||
await Promise.all(
|
||||
timeframes.map(async (timeframe) => {
|
||||
if (timeframe === 'daily') {
|
||||
results[timeframe] = (await this.fetchHistoricalData(symbol)).data;
|
||||
} else {
|
||||
data:results[timeframe] = (await this.fetchIntraday(symbol, timeframe)).data;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error fetching multiple timeframes:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
private static INTERVALS_MINUTES = {
|
||||
'1min': 1,
|
||||
'5min': 5,
|
||||
'15min': 15,
|
||||
'30min': 30,
|
||||
'60min': 60,
|
||||
'daily': 1440
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
private static adjustTimeEndRequest(timestamp: number): number {
|
||||
const date = new Date(timestamp);
|
||||
const day = date.getDay(); // 0 = Sunday, 6 = Saturday
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
// If weekend, adjust to Friday
|
||||
if (day === 1&&hours<9) {date.setDate(date.getDate() - 3);date.setHours(19, 59, 0, 0);} // Monday -> Friday
|
||||
if (day === 0) {date.setDate(date.getDate() - 2); date.setHours(19, 59, 0, 0);} // Sunday -> Friday
|
||||
if (day === 6) {date.setDate(date.getDate() - 1);date.setHours(19, 59, 0, 0);} // Saturday -> Friday
|
||||
|
||||
// Set to 16:00 (4 PM) ET if after market close
|
||||
if (hours > 19|| hours<4 ) {
|
||||
date.setHours(19, 59, 0, 0);
|
||||
date.setDate(date.getDate() - hours<4?1:0);
|
||||
}
|
||||
return date.getTime();
|
||||
}
|
||||
private static doesCacheCover(cache: CacheEntry, request: TimeframeConfig): boolean {
|
||||
|
||||
// If cache's last data point is within 24 hours, consider it current
|
||||
const adjustedEndTime = this.adjustTimeEndRequest(
|
||||
cache.endTime +24*60*60*1000>= request.endTime ? cache.endTime : request.endTime
|
||||
);
|
||||
console.log('Request Time:', {
|
||||
|
||||
Time: new Date(adjustedEndTime).toLocaleString()
|
||||
});
|
||||
return cache.interval === request.interval &&
|
||||
cache.startTime <= request.startTime &&
|
||||
cache.endTime >= adjustedEndTime;
|
||||
}
|
||||
private static aggregateToHigherTimeframe(
|
||||
data: MarketData[],
|
||||
fromInterval: IntervalType,
|
||||
toInterval: IntervalType
|
||||
): MarketData[] {
|
||||
const fromMinutes = this.INTERVALS_MINUTES[fromInterval];
|
||||
const toMinutes = this.INTERVALS_MINUTES[toInterval];
|
||||
const barsPerPeriod = toMinutes / fromMinutes;
|
||||
|
||||
// Group data into higher timeframe periods
|
||||
const groupedData: { [key: number]: MarketData[] } = {};
|
||||
data.forEach(bar => {
|
||||
const periodStart = Math.floor(bar.time / (toMinutes * 60 * 1000)) * (toMinutes * 60 * 1000);
|
||||
if (!groupedData[periodStart]) {
|
||||
groupedData[periodStart] = [];
|
||||
}
|
||||
groupedData[periodStart].push(bar);
|
||||
});
|
||||
|
||||
// Aggregate each period
|
||||
return Object.entries(groupedData)
|
||||
.filter(([_, bars]) => bars.length >= barsPerPeriod * 0.7) // Require at least 70% of bars
|
||||
.map(([time, bars]) => ({
|
||||
time: parseInt(time),
|
||||
open: bars[0].open,
|
||||
high: Math.max(...bars.map(b => b.high)),
|
||||
low: Math.min(...bars.map(b => b.low)),
|
||||
close: bars[bars.length - 1].close,
|
||||
volume: bars.reduce((sum, b) => sum + b.volume, 0)
|
||||
}));
|
||||
}
|
||||
|
||||
static addMarketTime(time: number, minutesToAdd: number): number {
|
||||
let date = new Date(time);
|
||||
let currentMinutes = date.getHours() * 60 + date.getMinutes();
|
||||
let newMinutes = currentMinutes + minutesToAdd;
|
||||
|
||||
// If would exceed market close
|
||||
if (newMinutes > this.MARKET_HOURS.end) {
|
||||
// Move to next day at market open
|
||||
date.setDate(date.getDate() + 1);
|
||||
date.setHours(this.MARKET_HOURS.start/60, 0, 0, 0);
|
||||
|
||||
// If weekend, move to Monday
|
||||
while (date.getDay() === 0 || date.getDay() === 6) {
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
} else {
|
||||
// Stay on same day, just update time
|
||||
date.setHours(Math.floor(newMinutes / 60), newMinutes % 60, 0, 0);
|
||||
}
|
||||
|
||||
return date.getTime();
|
||||
}
|
||||
static async getMarketData(
|
||||
symbol: string,
|
||||
config: TimeframeConfig,
|
||||
storage: MarketDataStorage
|
||||
): Promise<MarketData[]> {
|
||||
console.log('Request Config:', {
|
||||
symbol,
|
||||
interval: config.interval,
|
||||
startTime: new Date(config.startTime).toLocaleString(),
|
||||
endTime: new Date(config.endTime).toLocaleString()
|
||||
});
|
||||
try{
|
||||
// Get all cache entries for this symbol/interval
|
||||
const cacheEntries = await storage.getMarketData(symbol, config.interval);
|
||||
console.log(`Found ${cacheEntries.length} cache entries`);
|
||||
//console.log(`Checking cache for ${symbol} ${config.interval} data...`);
|
||||
const overlappingEntries = cacheEntries.filter(cache =>
|
||||
// Entry ends after request starts AND entry starts before request ends
|
||||
this.addMarketTime(cache.endTime,1) >= config.startTime && cache.startTime <= this.addMarketTime(config.endTime,1)
|
||||
);
|
||||
if (cacheEntries.length > 0) {
|
||||
// Find overlapping cache entries
|
||||
|
||||
|
||||
/*
|
||||
console.log('Found overlapping cache entries:', overlappingEntries.map(cache => ({
|
||||
startTime: new Date(cache.startTime).toLocaleString(),
|
||||
endTime: new Date(cache.endTime).toLocaleString(),
|
||||
dataPoints: cache.data.length
|
||||
})));*/
|
||||
|
||||
// Check if any single cache entry covers our request
|
||||
const coveringEntry =overlappingEntries.find(cache => this.doesCacheCover(cache, config))
|
||||
|
||||
if(coveringEntry) {
|
||||
console.log('Found complete coverage in cache:', {
|
||||
start: new Date(coveringEntry.startTime).toLocaleString(),
|
||||
end: new Date(coveringEntry.endTime).toLocaleString()
|
||||
});
|
||||
return coveringEntry.data.filter(d =>
|
||||
d.time >= config.startTime && d.time <= config.endTime
|
||||
);
|
||||
}
|
||||
console.log(`Found ${overlappingEntries.length} overlapping entries but no complete coverage`);
|
||||
}
|
||||
console.log(`Cache miss for ${symbol} ${config.interval}, fetching from API...`);
|
||||
// Check if we can build from lower timeframe data
|
||||
const intervals = Object.keys(this.INTERVALS_MINUTES) as IntervalType[];
|
||||
const lowerIntervals = intervals.filter(interval =>
|
||||
this.canDeriveFromCache(interval, config.interval)
|
||||
);
|
||||
|
||||
for (const interval of lowerIntervals) {
|
||||
const lowerCacheEntries = await storage.getMarketData(symbol, interval);
|
||||
// Find overlapping entries from lower timeframe
|
||||
const overlappingEntries = lowerCacheEntries.filter(cache =>
|
||||
cache.endTime >= config.startTime && cache.startTime <= config.endTime
|
||||
);
|
||||
|
||||
// Check if we have complete coverage from lower timeframe
|
||||
const hasFullCoverage = overlappingEntries.some(cache =>
|
||||
this.doesCacheCover(cache, {
|
||||
...config,
|
||||
interval: interval
|
||||
})
|
||||
);
|
||||
|
||||
if (hasFullCoverage) {
|
||||
// Merge overlapping lower timeframe data
|
||||
const mergedLowerData = overlappingEntries.reduce((acc, cache) =>
|
||||
[...acc, ...cache.data], [] as MarketData[]
|
||||
).sort((a, b) => a.time - b.time);
|
||||
|
||||
// We can build the higher timeframe from this data
|
||||
const aggregatedData = this.aggregateToHigherTimeframe(
|
||||
mergedLowerData,
|
||||
interval,
|
||||
config.interval
|
||||
);
|
||||
|
||||
// Cache the aggregated data
|
||||
await storage.saveMarketData(symbol, {
|
||||
data: aggregatedData,
|
||||
timestamp: Date.now(),
|
||||
interval: config.interval,
|
||||
startTime: Math.min(...aggregatedData.map(d => d.time)),
|
||||
endTime: Math.max(...aggregatedData.map(d => d.time)),
|
||||
timezone: overlappingEntries[0].timezone, // Assume timezone is consistent
|
||||
});
|
||||
|
||||
return aggregatedData.filter(d =>
|
||||
d.time >= config.startTime && d.time <= config.endTime
|
||||
);
|
||||
}
|
||||
}
|
||||
// If we can't build from cache, fetch new data
|
||||
const {data, timezone} = await this.fetchAppropriateData(symbol, config);
|
||||
let mergedData = data;
|
||||
// If we have existing cached data, try to merge
|
||||
if (cacheEntries.length > 0) {
|
||||
|
||||
if (overlappingEntries.length > 0) {
|
||||
console.log('Attempting to merge with cached data');
|
||||
mergedData = this.mergeMarketData(
|
||||
overlappingEntries.reduce((acc, cache) => [...acc, ...cache.data], [] as MarketData[]),
|
||||
data);
|
||||
|
||||
console.log('Merged with overlapping cached data:', {
|
||||
overlappingCachePoints: overlappingEntries.reduce((sum, cache) => sum + cache.data.length, 0),
|
||||
newPoints: data.length,
|
||||
finalPoints: mergedData.length
|
||||
});
|
||||
}
|
||||
}
|
||||
const newStartTime = Math.min(...mergedData.map(d => d.time));
|
||||
const newEndTime = Math.max(...mergedData.map(d => d.time));
|
||||
|
||||
// Check if this exact time range already exists in cache
|
||||
const exactMatch = overlappingEntries.some(entry =>
|
||||
Math.abs(entry.startTime - newStartTime) < 60000 && // Within 1 minute
|
||||
Math.abs(entry.endTime - newEndTime) < 60000 && // Within 1 minute
|
||||
entry.interval === config.interval
|
||||
);
|
||||
|
||||
if (!exactMatch) {
|
||||
await storage.saveMarketData(symbol, {
|
||||
data: mergedData,
|
||||
timestamp: Date.now(),
|
||||
interval: config.interval,
|
||||
startTime: Math.min(...mergedData.map(d => d.time)),
|
||||
endTime: Math.max(...mergedData.map(d => d.time)),
|
||||
timezone
|
||||
});
|
||||
} else {
|
||||
console.log('Skipping cache save - exact time range already exists');
|
||||
}
|
||||
|
||||
return mergedData;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error in getMarketData:', error);
|
||||
throw error; // Re-throw to handle in component
|
||||
}
|
||||
|
||||
}
|
||||
private static canDeriveFromCache(sourceInterval: IntervalType, targetInterval: IntervalType): boolean {
|
||||
const sourceMinutes = this.INTERVALS_MINUTES[sourceInterval];
|
||||
const targetMinutes = this.INTERVALS_MINUTES[targetInterval];
|
||||
return sourceMinutes < targetMinutes && targetMinutes % sourceMinutes === 0;
|
||||
}
|
||||
|
||||
|
||||
private static async fetchAppropriateData(
|
||||
symbol: string,
|
||||
config: TimeframeConfig
|
||||
): Promise<{data:MarketData[], timezone: string}> {
|
||||
if (config.interval === 'daily') {
|
||||
return await this.fetchHistoricalData(symbol);
|
||||
} else {
|
||||
return await this.fetchIntraday(symbol, config.interval);
|
||||
}
|
||||
}
|
||||
private static mergeMarketData(existingData: MarketData[], newData: MarketData[]): MarketData[] {
|
||||
// Combine both arrays
|
||||
const combined = [...existingData, ...newData];
|
||||
|
||||
// Sort by time
|
||||
combined.sort((a, b) => a.time - b.time);
|
||||
|
||||
// Remove duplicates based on timestamp
|
||||
return combined.filter((item, index, self) =>
|
||||
index === 0 || item.time !== self[index - 1].time
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
// src/services/marketDataStorage.ts
|
||||
import { TFile, Vault } from 'obsidian';
|
||||
import { MarketData, CacheEntry, CacheMetadata, IntervalType } from './marketDataService';
|
||||
|
||||
export class MarketDataStorage {
|
||||
private vault: Vault;
|
||||
private noteFile: TFile;
|
||||
private basePath: string;
|
||||
|
||||
constructor(vault: Vault, noteFile: TFile) {
|
||||
this.vault = vault;
|
||||
this.noteFile = noteFile;
|
||||
|
||||
// Determine the appropriate base path
|
||||
this.basePath = this.determineBasePath();
|
||||
}
|
||||
private determineBasePath(): string {
|
||||
// Check if note exists and is a markdown file
|
||||
if (!this.noteFile || this.noteFile.extension !== 'md') {
|
||||
throw new Error('Invalid note file');
|
||||
}
|
||||
|
||||
// Get the parent folder path, defaulting to root if none
|
||||
const parentPath = this.noteFile.parent?.path ?? '';
|
||||
|
||||
// If at root, store in vault root
|
||||
if (!parentPath) {
|
||||
return '.market-data';
|
||||
}
|
||||
|
||||
// Store in parent folder
|
||||
return `${parentPath}/.market-data`;
|
||||
}
|
||||
getBasePath(): string {
|
||||
return this.basePath;
|
||||
}
|
||||
private async ensureDirectory(path: string) {
|
||||
try {
|
||||
if (!(await this.vault.adapter.exists(path))) {
|
||||
await this.vault.adapter.mkdir(path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to create directory ${path}:`, error);
|
||||
throw new Error(`Failed to create market data directory: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
// Ensure base directory exists
|
||||
await this.ensureDirectory(this.basePath);
|
||||
|
||||
// Create metadata file if it doesn't exist
|
||||
const metadataPath = `${this.basePath}/metadata.json`;
|
||||
if (!(await this.vault.adapter.exists(metadataPath))) {
|
||||
await this.vault.adapter.write(
|
||||
metadataPath,
|
||||
JSON.stringify({
|
||||
created: Date.now(),
|
||||
noteSource: this.noteFile.path
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize market data storage:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Helper method to get absolute path in vault
|
||||
getAbsolutePath(relativePath: string): string {
|
||||
return `${this.basePath}/${relativePath}`;
|
||||
}
|
||||
|
||||
// Method to check if storage is properly initialized
|
||||
async isInitialized(): Promise<boolean> {
|
||||
try {
|
||||
return await this.vault.adapter.exists(this.basePath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get storage location info
|
||||
async getStorageInfo(): Promise<{
|
||||
basePath: string;
|
||||
isInitialized: boolean;
|
||||
parentFolder: string;
|
||||
noteFile: string;
|
||||
}> {
|
||||
return {
|
||||
basePath: this.basePath,
|
||||
isInitialized: await this.isInitialized(),
|
||||
parentFolder: this.noteFile.parent?.path ?? '(root)',
|
||||
noteFile: this.noteFile.path
|
||||
};
|
||||
}
|
||||
private getSymbolPath(symbol: string) {
|
||||
return `${this.basePath}/${symbol}`;
|
||||
}
|
||||
|
||||
private getDataPath(symbol: string, interval: IntervalType) {
|
||||
return `${this.getSymbolPath(symbol)}/${interval}.json`;
|
||||
}
|
||||
|
||||
async saveMarketData(symbol: string, cacheEntry: CacheEntry): Promise<void> {
|
||||
await this.initialize();
|
||||
|
||||
// Ensure symbol directory exists
|
||||
const symbolPath = this.getSymbolPath(symbol);
|
||||
await this.ensureDirectory(symbolPath);
|
||||
|
||||
// Save data file
|
||||
const dataPath = this.getDataPath(symbol, cacheEntry.interval);
|
||||
await this.vault.adapter.write(
|
||||
dataPath,
|
||||
JSON.stringify(cacheEntry, null, 2)
|
||||
);
|
||||
|
||||
// Update metadata
|
||||
await this.updateMetadata(symbol, cacheEntry);
|
||||
}
|
||||
|
||||
async getMarketData(symbol: string, interval: IntervalType): Promise<CacheEntry[]> {
|
||||
const dataPath = this.getDataPath(symbol, interval);
|
||||
|
||||
if (await this.vault.adapter.exists(dataPath)) {
|
||||
const content = await this.vault.adapter.read(dataPath);
|
||||
const parsed = JSON.parse(content);
|
||||
// Ensure we always return an array
|
||||
return Array.isArray(parsed) ? parsed : [parsed];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async updateMetadata(symbol: string, cacheEntry: CacheEntry) {
|
||||
const metadataPath = `${this.basePath}/metadata.json`;
|
||||
let metadata: CacheMetadata = {};
|
||||
try{
|
||||
if (await this.vault.adapter.exists(metadataPath)) {
|
||||
const content = await this.vault.adapter.read(metadataPath);
|
||||
metadata = JSON.parse(content);
|
||||
}
|
||||
|
||||
// Update metadata for this symbol
|
||||
metadata[symbol] = metadata[symbol] || {};
|
||||
if (!metadata[symbol][cacheEntry.interval]) {
|
||||
metadata[symbol][cacheEntry.interval] = [];
|
||||
}
|
||||
|
||||
// Add new cache entry info
|
||||
const cacheList = metadata[symbol][cacheEntry.interval];
|
||||
if (cacheList) {
|
||||
cacheList.push({
|
||||
timestamp: cacheEntry.timestamp,
|
||||
startTime: cacheEntry.startTime,
|
||||
endTime: cacheEntry.endTime,
|
||||
interval: cacheEntry.interval,
|
||||
data: [], // Don't store actual data in metadata
|
||||
timezone: cacheEntry.timezone
|
||||
});
|
||||
|
||||
// Keep only last 5 entries
|
||||
if (cacheList.length > 5) {
|
||||
cacheList.shift();
|
||||
}
|
||||
}
|
||||
|
||||
await this.vault.adapter.write(
|
||||
metadataPath,
|
||||
JSON.stringify(metadata, null, 2)
|
||||
);
|
||||
}catch (error) {
|
||||
console.error(`Failed to update metadata for ${symbol}:`, error);
|
||||
throw new Error(`Failed to update metadata: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getCacheStats(): Promise<{
|
||||
totalSize: number;
|
||||
symbolCount: number;
|
||||
fileCount: number;
|
||||
symbols: Record<string, {
|
||||
intervals: IntervalType[];
|
||||
lastUpdated: number;
|
||||
}>;
|
||||
}> {
|
||||
let totalSize = 0;
|
||||
let fileCount = 0;
|
||||
const symbols: Record<string, {
|
||||
intervals: IntervalType[];
|
||||
lastUpdated: number;
|
||||
}> = {};
|
||||
try{
|
||||
if (await this.vault.adapter.exists(this.basePath)) {
|
||||
const files = await this.vault.adapter.list(this.basePath);
|
||||
|
||||
for (const file of files.files) {
|
||||
if (file.endsWith('.json')) {
|
||||
const stat = await this.vault.adapter.stat(file);
|
||||
if (!stat) continue; // Skip if stat is null
|
||||
totalSize += stat.size;
|
||||
fileCount++;
|
||||
|
||||
// Parse symbol and interval info
|
||||
const pathParts = file.split('/');
|
||||
if (pathParts.length >= 2) {
|
||||
const symbol = pathParts[pathParts.length - 2];
|
||||
const interval = pathParts[pathParts.length - 1].replace('.json', '') as IntervalType;
|
||||
|
||||
if (!symbols[symbol]) {
|
||||
symbols[symbol] = {
|
||||
intervals: [],
|
||||
lastUpdated: stat.mtime
|
||||
};
|
||||
}
|
||||
symbols[symbol].intervals.push(interval);
|
||||
symbols[symbol].lastUpdated = Math.max(symbols[symbol].lastUpdated, stat.mtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize,
|
||||
symbolCount: Object.keys(symbols).length,
|
||||
fileCount,
|
||||
symbols
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get cache stats:', error);
|
||||
return {
|
||||
totalSize: 0,
|
||||
symbolCount: 0,
|
||||
fileCount: 0,
|
||||
symbols: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup(maxAge: number = 7 * 24 * 60 * 60 * 1000) { // Default 7 days
|
||||
const stats = await this.getCacheStats();
|
||||
const now = Date.now();
|
||||
|
||||
for (const [symbol, info] of Object.entries(stats.symbols)) {
|
||||
if (now - info.lastUpdated > maxAge) {
|
||||
// Remove old symbol data
|
||||
const symbolPath = this.getSymbolPath(symbol);
|
||||
if (await this.vault.adapter.exists(symbolPath)) {
|
||||
await this.vault.adapter.rmdir(symbolPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue