Feature/add benchmarks to twitter bot service (#959)
* Extend benchmarks with market condition and adapt twitter bot service * Update changelog
This commit is contained in:
parent
3498ed8549
commit
c3768a882d
@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the benchmarks of the markets overview by the current market condition (bear and bull market)
|
||||||
|
- Extended the twitter bot service by benchmarks
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Upgraded `prisma` from version `3.12.0` to `3.14.0`
|
- Upgraded `prisma` from version `3.12.0` to `3.14.0`
|
||||||
|
@ -11,6 +11,7 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [BenchmarkController],
|
controllers: [BenchmarkController],
|
||||||
|
exports: [BenchmarkService],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
|
@ -53,6 +53,9 @@ export class BenchmarkService {
|
|||||||
.minus(1);
|
.minus(1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
marketCondition: this.getMarketCondition(
|
||||||
|
performancePercentFromAllTimeHigh
|
||||||
|
),
|
||||||
name: assetProfiles.find(({ dataSource, symbol }) => {
|
name: assetProfiles.find(({ dataSource, symbol }) => {
|
||||||
return (
|
return (
|
||||||
dataSource === benchmarkAssets[index].dataSource &&
|
dataSource === benchmarkAssets[index].dataSource &&
|
||||||
@ -74,4 +77,8 @@ export class BenchmarkService {
|
|||||||
|
|
||||||
return benchmarks;
|
return benchmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getMarketCondition(aPerformanceInPercent: Big) {
|
||||||
|
return aPerformanceInPercent.lte(-0.2) ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
|
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
exports: [TwitterBotService],
|
exports: [TwitterBotService],
|
||||||
imports: [ConfigurationModule, SymbolModule],
|
imports: [BenchmarkModule, ConfigurationModule, PropertyModule, SymbolModule],
|
||||||
providers: [TwitterBotService]
|
providers: [TwitterBotService]
|
||||||
})
|
})
|
||||||
export class TwitterBotModule {}
|
export class TwitterBotModule {}
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
|
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import {
|
import {
|
||||||
|
PROPERTY_BENCHMARKS,
|
||||||
ghostfolioFearAndGreedIndexDataSource,
|
ghostfolioFearAndGreedIndexDataSource,
|
||||||
ghostfolioFearAndGreedIndexSymbol
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
resolveFearAndGreedIndex,
|
||||||
|
resolveMarketCondition
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
|
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { isSunday } from 'date-fns';
|
import { isSunday } from 'date-fns';
|
||||||
|
import * as roundTo from 'round-to';
|
||||||
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
|
import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -14,7 +22,9 @@ export class TwitterBotService {
|
|||||||
private twitterClient: TwitterApiReadWrite;
|
private twitterClient: TwitterApiReadWrite;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly benchmarkService: BenchmarkService,
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
|
private readonly propertyService: PropertyService,
|
||||||
private readonly symbolService: SymbolService
|
private readonly symbolService: SymbolService
|
||||||
) {
|
) {
|
||||||
this.twitterClient = new TwitterApi({
|
this.twitterClient = new TwitterApi({
|
||||||
@ -48,7 +58,16 @@ export class TwitterBotService {
|
|||||||
symbolItem.marketPrice
|
symbolItem.marketPrice
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = `Current Market Mood: ${emoji} ${text} (${symbolItem.marketPrice}/100)\n\n#FearAndGreed #Markets #ServiceTweet`;
|
let status = `Current Market Mood: ${emoji} ${text} (${symbolItem.marketPrice}/100)`;
|
||||||
|
|
||||||
|
const benchmarkListing = await this.getBenchmarkListing(3);
|
||||||
|
|
||||||
|
if (benchmarkListing?.length > 1) {
|
||||||
|
status += '\n\n';
|
||||||
|
status += '±% from ATH\n';
|
||||||
|
status += benchmarkListing;
|
||||||
|
}
|
||||||
|
|
||||||
const { data: createdTweet } = await this.twitterClient.v2.tweet(
|
const { data: createdTweet } = await this.twitterClient.v2.tweet(
|
||||||
status
|
status
|
||||||
);
|
);
|
||||||
@ -62,4 +81,36 @@ export class TwitterBotService {
|
|||||||
Logger.error(error, 'TwitterBotService');
|
Logger.error(error, 'TwitterBotService');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getBenchmarkListing(aMax: number) {
|
||||||
|
const benchmarkAssets: UniqueAsset[] =
|
||||||
|
((await this.propertyService.getByKey(
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
|
)) as UniqueAsset[]) ?? [];
|
||||||
|
|
||||||
|
const benchmarks = await this.benchmarkService.getBenchmarks(
|
||||||
|
benchmarkAssets
|
||||||
|
);
|
||||||
|
|
||||||
|
const benchmarkListing: string[] = [];
|
||||||
|
|
||||||
|
for (const [index, benchmark] of benchmarks.entries()) {
|
||||||
|
if (index > aMax - 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmarkListing.push(
|
||||||
|
`${benchmark.name} ${roundTo(
|
||||||
|
benchmark.performances.allTimeHigh.performancePercent * 100,
|
||||||
|
1
|
||||||
|
)}%${
|
||||||
|
benchmark.marketCondition !== 'NEUTRAL_MARKET'
|
||||||
|
? ' ' + resolveMarketCondition(benchmark.marketCondition).emoji
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return benchmarkListing.join('\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { DataSource } from '@prisma/client';
|
|||||||
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
||||||
|
|
||||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||||
|
import { Benchmark } from './interfaces';
|
||||||
|
|
||||||
export function capitalize(aString: string) {
|
export function capitalize(aString: string) {
|
||||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||||
@ -178,6 +179,18 @@ export function resolveFearAndGreedIndex(aValue: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveMarketCondition(
|
||||||
|
aMarketCondition: Benchmark['marketCondition']
|
||||||
|
) {
|
||||||
|
if (aMarketCondition === 'BEAR_MARKET') {
|
||||||
|
return { emoji: '🐻' };
|
||||||
|
} else if (aMarketCondition === 'BULL_MARKET') {
|
||||||
|
return { emoji: '🐮' };
|
||||||
|
} else {
|
||||||
|
return { emoji: '⚪' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const DATE_FORMAT = 'yyyy-MM-dd';
|
export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||||
|
|
||||||
export function parseDate(date: string) {
|
export function parseDate(date: string) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||||
|
|
||||||
export interface Benchmark {
|
export interface Benchmark {
|
||||||
|
marketCondition: 'BEAR_MARKET' | 'BULL_MARKET' | 'NEUTRAL_MARKET';
|
||||||
name: EnhancedSymbolProfile['name'];
|
name: EnhancedSymbolProfile['name'];
|
||||||
performances: {
|
performances: {
|
||||||
allTimeHigh: {
|
allTimeHigh: {
|
||||||
|
@ -29,4 +29,21 @@
|
|||||||
<small class="d-none d-sm-block text-nowrap" i18n>from All Time High</small
|
<small class="d-none d-sm-block text-nowrap" i18n>from All Time High</small
|
||||||
><small class="d-block d-sm-none text-nowrap" i18n>from ATH</small>
|
><small class="d-block d-sm-none text-nowrap" i18n>from ATH</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ml-2">
|
||||||
|
<div
|
||||||
|
*ngIf="benchmark?.marketCondition"
|
||||||
|
[title]="benchmark?.marketCondition"
|
||||||
|
>
|
||||||
|
{{ resolveMarketCondition(benchmark.marketCondition).emoji }}
|
||||||
|
</div>
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="!benchmark?.marketCondition"
|
||||||
|
animation="pulse"
|
||||||
|
appearance="circle"
|
||||||
|
[theme]="{
|
||||||
|
height: '1rem',
|
||||||
|
width: '1rem'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
|
import { resolveMarketCondition } from '@ghostfolio/common/helper';
|
||||||
import { Benchmark } from '@ghostfolio/common/interfaces';
|
import { Benchmark } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -11,5 +12,7 @@ export class BenchmarkComponent {
|
|||||||
@Input() benchmark: Benchmark;
|
@Input() benchmark: Benchmark;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
|
|
||||||
|
public resolveMarketCondition = resolveMarketCondition;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user