|
|
@ -8,16 +8,21 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces'; |
|
|
import { DataService } from '@ghostfolio/ui/services'; |
|
|
import { DataService } from '@ghostfolio/ui/services'; |
|
|
|
|
|
|
|
|
import { CommonModule } from '@angular/common'; |
|
|
import { CommonModule } from '@angular/common'; |
|
|
|
|
|
import type { HttpErrorResponse } from '@angular/common/http'; |
|
|
import { |
|
|
import { |
|
|
ChangeDetectionStrategy, |
|
|
ChangeDetectionStrategy, |
|
|
Component, |
|
|
Component, |
|
|
|
|
|
computed, |
|
|
|
|
|
DestroyRef, |
|
|
EventEmitter, |
|
|
EventEmitter, |
|
|
|
|
|
inject, |
|
|
|
|
|
input, |
|
|
Input, |
|
|
Input, |
|
|
OnChanges, |
|
|
OnChanges, |
|
|
OnDestroy, |
|
|
|
|
|
OnInit, |
|
|
OnInit, |
|
|
Output |
|
|
Output |
|
|
} from '@angular/core'; |
|
|
} from '@angular/core'; |
|
|
|
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; |
|
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; |
|
|
import { MatButtonModule } from '@angular/material/button'; |
|
|
import { MatButtonModule } from '@angular/material/button'; |
|
|
import { MatDialog } from '@angular/material/dialog'; |
|
|
import { MatDialog } from '@angular/material/dialog'; |
|
|
@ -40,7 +45,7 @@ import { first, last } from 'lodash'; |
|
|
import ms from 'ms'; |
|
|
import ms from 'ms'; |
|
|
import { DeviceDetectorService } from 'ngx-device-detector'; |
|
|
import { DeviceDetectorService } from 'ngx-device-detector'; |
|
|
import { parse as csvToJson } from 'papaparse'; |
|
|
import { parse as csvToJson } from 'papaparse'; |
|
|
import { EMPTY, Subject, takeUntil } from 'rxjs'; |
|
|
import { EMPTY } from 'rxjs'; |
|
|
import { catchError } from 'rxjs/operators'; |
|
|
import { catchError } from 'rxjs/operators'; |
|
|
|
|
|
|
|
|
import { GfHistoricalMarketDataEditorDialogComponent } from './historical-market-data-editor-dialog/historical-market-data-editor-dialog.component'; |
|
|
import { GfHistoricalMarketDataEditorDialogComponent } from './historical-market-data-editor-dialog/historical-market-data-editor-dialog.component'; |
|
|
@ -54,74 +59,80 @@ import { HistoricalMarketDataEditorDialogParams } from './historical-market-data |
|
|
templateUrl: './historical-market-data-editor.component.html' |
|
|
templateUrl: './historical-market-data-editor.component.html' |
|
|
}) |
|
|
}) |
|
|
export class GfHistoricalMarketDataEditorComponent |
|
|
export class GfHistoricalMarketDataEditorComponent |
|
|
implements OnChanges, OnDestroy, OnInit |
|
|
implements OnChanges, OnInit |
|
|
{ |
|
|
{ |
|
|
|
|
|
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( |
|
|
|
|
|
new Date(), |
|
|
|
|
|
DATE_FORMAT |
|
|
|
|
|
)};123.45`;
|
|
|
|
|
|
|
|
|
@Input() currency: string; |
|
|
@Input() currency: string; |
|
|
@Input() dataSource: DataSource; |
|
|
@Input() dataSource: DataSource; |
|
|
@Input() dateOfFirstActivity: string; |
|
|
@Input() dateOfFirstActivity: string; |
|
|
@Input() locale = getLocale(); |
|
|
|
|
|
@Input() marketData: MarketData[]; |
|
|
|
|
|
@Input() symbol: string; |
|
|
@Input() symbol: string; |
|
|
@Input() user: User; |
|
|
@Input() user: User; |
|
|
|
|
|
|
|
|
@Output() marketDataChanged = new EventEmitter<boolean>(); |
|
|
@Output() marketDataChanged = new EventEmitter<boolean>(); |
|
|
|
|
|
|
|
|
public days = Array(31); |
|
|
|
|
|
public defaultDateFormat: string; |
|
|
|
|
|
public deviceType: string; |
|
|
|
|
|
public historicalDataForm = this.formBuilder.group({ |
|
|
public historicalDataForm = this.formBuilder.group({ |
|
|
historicalData: this.formBuilder.group({ |
|
|
historicalData: this.formBuilder.group({ |
|
|
csvString: '' |
|
|
csvString: '' |
|
|
}) |
|
|
}) |
|
|
}); |
|
|
}); |
|
|
public historicalDataItems: LineChartItem[]; |
|
|
|
|
|
public marketDataByMonth: { |
|
|
public marketDataByMonth: { |
|
|
[yearMonth: string]: { |
|
|
[yearMonth: string]: { |
|
|
[day: string]: Pick<MarketData, 'date' | 'marketPrice'> & { day: number }; |
|
|
[day: string]: { |
|
|
|
|
|
date: Date; |
|
|
|
|
|
day: number; |
|
|
|
|
|
marketPrice?: number; |
|
|
|
|
|
}; |
|
|
}; |
|
|
}; |
|
|
} = {}; |
|
|
} = {}; |
|
|
|
|
|
|
|
|
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( |
|
|
public readonly locale = input(getLocale()); |
|
|
new Date(), |
|
|
public readonly marketData = input.required<MarketData[]>(); |
|
|
DATE_FORMAT |
|
|
|
|
|
)};123.45`;
|
|
|
protected readonly days = Array.from({ length: 31 }, (_, i) => i + 1); |
|
|
|
|
|
protected readonly defaultDateFormat = computed(() => |
|
|
|
|
|
getDateFormatString(this.locale()) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
private unsubscribeSubject = new Subject<void>(); |
|
|
private readonly destroyRef = inject(DestroyRef); |
|
|
|
|
|
private readonly deviceDetectorService = inject(DeviceDetectorService); |
|
|
|
|
|
private readonly deviceType = computed( |
|
|
|
|
|
() => this.deviceDetectorService.deviceInfo().deviceType |
|
|
|
|
|
); |
|
|
|
|
|
private readonly historicalDataItems = computed<LineChartItem[]>(() => |
|
|
|
|
|
this.marketData().map(({ date, marketPrice }) => { |
|
|
|
|
|
return { |
|
|
|
|
|
date: format(date, DATE_FORMAT), |
|
|
|
|
|
value: marketPrice |
|
|
|
|
|
}; |
|
|
|
|
|
}) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
public constructor( |
|
|
public constructor( |
|
|
private dataService: DataService, |
|
|
private dataService: DataService, |
|
|
private deviceService: DeviceDetectorService, |
|
|
|
|
|
private dialog: MatDialog, |
|
|
private dialog: MatDialog, |
|
|
private formBuilder: FormBuilder, |
|
|
private formBuilder: FormBuilder, |
|
|
private snackBar: MatSnackBar |
|
|
private snackBar: MatSnackBar |
|
|
) { |
|
|
) {} |
|
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public ngOnInit() { |
|
|
public ngOnInit() { |
|
|
this.initializeHistoricalDataForm(); |
|
|
this.initializeHistoricalDataForm(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public ngOnChanges() { |
|
|
public ngOnChanges() { |
|
|
this.defaultDateFormat = getDateFormatString(this.locale); |
|
|
|
|
|
|
|
|
|
|
|
this.historicalDataItems = this.marketData.map(({ date, marketPrice }) => { |
|
|
|
|
|
return { |
|
|
|
|
|
date: format(date, DATE_FORMAT), |
|
|
|
|
|
value: marketPrice |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (this.dateOfFirstActivity) { |
|
|
if (this.dateOfFirstActivity) { |
|
|
let date = parseISO(this.dateOfFirstActivity); |
|
|
let date = parseISO(this.dateOfFirstActivity); |
|
|
|
|
|
|
|
|
const missingMarketData: Partial<MarketData>[] = []; |
|
|
const missingMarketData: { date: Date; marketPrice?: number }[] = []; |
|
|
|
|
|
|
|
|
if (this.historicalDataItems?.[0]?.date) { |
|
|
if (this.historicalDataItems()?.[0]?.date) { |
|
|
while ( |
|
|
while ( |
|
|
isBefore( |
|
|
isBefore( |
|
|
date, |
|
|
date, |
|
|
parse(this.historicalDataItems[0].date, DATE_FORMAT, new Date()) |
|
|
parse(this.historicalDataItems()[0].date, DATE_FORMAT, new Date()) |
|
|
) |
|
|
) |
|
|
) { |
|
|
) { |
|
|
missingMarketData.push({ |
|
|
missingMarketData.push({ |
|
|
@ -133,9 +144,10 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const marketDataItems = [...missingMarketData, ...this.marketData]; |
|
|
const marketDataItems = [...missingMarketData, ...this.marketData()]; |
|
|
|
|
|
|
|
|
if (!isToday(last(marketDataItems)?.date)) { |
|
|
const lastDate = last(marketDataItems)?.date; |
|
|
|
|
|
if (!lastDate || !isToday(lastDate)) { |
|
|
marketDataItems.push({ date: new Date() }); |
|
|
marketDataItems.push({ date: new Date() }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -160,11 +172,15 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
|
|
|
|
|
|
// Fill up missing months
|
|
|
// Fill up missing months
|
|
|
const dates = Object.keys(this.marketDataByMonth).sort(); |
|
|
const dates = Object.keys(this.marketDataByMonth).sort(); |
|
|
|
|
|
const startDateString = first(dates); |
|
|
const startDate = min([ |
|
|
const startDate = min([ |
|
|
parseISO(this.dateOfFirstActivity), |
|
|
parseISO(this.dateOfFirstActivity), |
|
|
parseISO(first(dates)) |
|
|
...(startDateString ? [parseISO(startDateString)] : []) |
|
|
]); |
|
|
]); |
|
|
const endDate = parseISO(last(dates)); |
|
|
const endDateString = last(dates); |
|
|
|
|
|
|
|
|
|
|
|
if (endDateString) { |
|
|
|
|
|
const endDate = parseISO(endDateString); |
|
|
|
|
|
|
|
|
let currentDate = startDate; |
|
|
let currentDate = startDate; |
|
|
|
|
|
|
|
|
@ -178,6 +194,11 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public formatDay(day: number): string { |
|
|
|
|
|
return day < 10 ? `0${day}` : `${day}`; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
public isDateOfInterest(aDateString: string) { |
|
|
public isDateOfInterest(aDateString: string) { |
|
|
// Date is valid and in the past
|
|
|
// Date is valid and in the past
|
|
|
@ -201,7 +222,8 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
|
|
|
|
|
|
const dialogRef = this.dialog.open< |
|
|
const dialogRef = this.dialog.open< |
|
|
GfHistoricalMarketDataEditorDialogComponent, |
|
|
GfHistoricalMarketDataEditorDialogComponent, |
|
|
HistoricalMarketDataEditorDialogParams |
|
|
HistoricalMarketDataEditorDialogParams, |
|
|
|
|
|
{ withRefresh: boolean } |
|
|
>(GfHistoricalMarketDataEditorDialogComponent, { |
|
|
>(GfHistoricalMarketDataEditorDialogComponent, { |
|
|
data: { |
|
|
data: { |
|
|
marketPrice, |
|
|
marketPrice, |
|
|
@ -211,13 +233,13 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
symbol: this.symbol, |
|
|
symbol: this.symbol, |
|
|
user: this.user |
|
|
user: this.user |
|
|
}, |
|
|
}, |
|
|
height: this.deviceType === 'mobile' ? '98vh' : '80vh', |
|
|
height: this.deviceType() === 'mobile' ? '98vh' : '80vh', |
|
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem' |
|
|
width: this.deviceType() === 'mobile' ? '100vw' : '50rem' |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
dialogRef |
|
|
dialogRef |
|
|
.afterClosed() |
|
|
.afterClosed() |
|
|
.pipe(takeUntil(this.unsubscribeSubject)) |
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
.subscribe(({ withRefresh } = { withRefresh: false }) => { |
|
|
.subscribe(({ withRefresh } = { withRefresh: false }) => { |
|
|
this.marketDataChanged.emit(withRefresh); |
|
|
this.marketDataChanged.emit(withRefresh); |
|
|
}); |
|
|
}); |
|
|
@ -225,15 +247,15 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
|
|
|
|
|
|
public onImportHistoricalData() { |
|
|
public onImportHistoricalData() { |
|
|
try { |
|
|
try { |
|
|
const marketData = csvToJson( |
|
|
const marketData = csvToJson<UpdateMarketDataDto>( |
|
|
this.historicalDataForm.controls['historicalData'].controls['csvString'] |
|
|
this.historicalDataForm.controls.historicalData.controls.csvString |
|
|
.value, |
|
|
.value ?? '', |
|
|
{ |
|
|
{ |
|
|
dynamicTyping: true, |
|
|
dynamicTyping: true, |
|
|
header: true, |
|
|
header: true, |
|
|
skipEmptyLines: true |
|
|
skipEmptyLines: true |
|
|
} |
|
|
} |
|
|
).data as UpdateMarketDataDto[]; |
|
|
).data; |
|
|
|
|
|
|
|
|
this.dataService |
|
|
this.dataService |
|
|
.postMarketData({ |
|
|
.postMarketData({ |
|
|
@ -244,13 +266,13 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
symbol: this.symbol |
|
|
symbol: this.symbol |
|
|
}) |
|
|
}) |
|
|
.pipe( |
|
|
.pipe( |
|
|
catchError(({ error, message }) => { |
|
|
catchError(({ error, message }: HttpErrorResponse) => { |
|
|
this.snackBar.open(`${error}: ${message[0]}`, undefined, { |
|
|
this.snackBar.open(`${error}: ${message[0]}`, undefined, { |
|
|
duration: ms('3 seconds') |
|
|
duration: ms('3 seconds') |
|
|
}); |
|
|
}); |
|
|
return EMPTY; |
|
|
return EMPTY; |
|
|
}), |
|
|
}), |
|
|
takeUntil(this.unsubscribeSubject) |
|
|
takeUntilDestroyed(this.destroyRef) |
|
|
) |
|
|
) |
|
|
.subscribe(() => { |
|
|
.subscribe(() => { |
|
|
this.initializeHistoricalDataForm(); |
|
|
this.initializeHistoricalDataForm(); |
|
|
@ -268,11 +290,6 @@ export class GfHistoricalMarketDataEditorComponent |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public ngOnDestroy() { |
|
|
|
|
|
this.unsubscribeSubject.next(); |
|
|
|
|
|
this.unsubscribeSubject.complete(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private initializeHistoricalDataForm() { |
|
|
private initializeHistoricalDataForm() { |
|
|
this.historicalDataForm.setValue({ |
|
|
this.historicalDataForm.setValue({ |
|
|
historicalData: { |
|
|
historicalData: { |
|
|
|