diff --git a/CHANGELOG.md b/CHANGELOG.md index a92543cb..17708a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a service to tweet the current _Fear & Greed Index_ (market mood) + ### Changed - Improved the mobile layout of the position detail dialog (countries and sectors charts) diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 46b2c906..e3d8a3be 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -8,6 +8,7 @@ import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.mod import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; +import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; @@ -65,6 +66,7 @@ import { UserModule } from './user/user.module'; }), SubscriptionModule, SymbolModule, + TwitterBotModule, UserModule ], controllers: [AppController], diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index 203dc6dc..f13679ef 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -9,7 +9,8 @@ import { PROPERTY_IS_READ_ONLY_MODE, PROPERTY_SLACK_COMMUNITY_USERS, PROPERTY_STRIPE_CONFIG, - PROPERTY_SYSTEM_MESSAGE + PROPERTY_SYSTEM_MESSAGE, + ghostfolioFearAndGreedIndexDataSource } from '@ghostfolio/common/config'; import { encodeDataSource } from '@ghostfolio/common/helper'; import { InfoItem } from '@ghostfolio/common/interfaces'; @@ -18,7 +19,6 @@ import { Subscription } from '@ghostfolio/common/interfaces/subscription.interfa import { permissions } from '@ghostfolio/common/permissions'; import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { DataSource } from '@prisma/client'; import * as bent from 'bent'; import { subDays } from 'date-fns'; @@ -52,7 +52,9 @@ export class InfoService { } if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { - info.fearAndGreedDataSource = encodeDataSource(DataSource.RAKUTEN); + info.fearAndGreedDataSource = encodeDataSource( + ghostfolioFearAndGreedIndexDataSource + ); } if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) { diff --git a/apps/api/src/app/symbol/symbol.module.ts b/apps/api/src/app/symbol/symbol.module.ts index c5143632..2b47334a 100644 --- a/apps/api/src/app/symbol/symbol.module.ts +++ b/apps/api/src/app/symbol/symbol.module.ts @@ -8,13 +8,14 @@ import { SymbolController } from './symbol.controller'; import { SymbolService } from './symbol.service'; @Module({ + controllers: [SymbolController], + exports: [SymbolService], imports: [ ConfigurationModule, DataProviderModule, MarketDataModule, PrismaModule ], - controllers: [SymbolController], providers: [SymbolService] }) export class SymbolModule {} diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index 0c358f09..abbfa664 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -39,6 +39,10 @@ export class ConfigurationService { ROOT_URL: str({ default: 'http://localhost:4200' }), STRIPE_PUBLIC_KEY: str({ default: '' }), STRIPE_SECRET_KEY: str({ default: '' }), + TWITTER_ACCESS_TOKEN: str({ default: 'dummyAccessToken' }), + TWITTER_ACCESS_TOKEN_SECRET: str({ default: 'dummyAccessTokenSecret' }), + TWITTER_API_KEY: str({ default: 'dummyApiKey' }), + TWITTER_API_SECRET: str({ default: 'dummyApiSecret' }), WEB_AUTH_RP_ID: host({ default: 'localhost' }) }); } diff --git a/apps/api/src/services/cron.service.ts b/apps/api/src/services/cron.service.ts index be29d5a7..fdda206e 100644 --- a/apps/api/src/services/cron.service.ts +++ b/apps/api/src/services/cron.service.ts @@ -3,12 +3,14 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { DataGatheringService } from './data-gathering.service'; import { ExchangeRateDataService } from './exchange-rate-data.service'; +import { TwitterBotService } from './twitter-bot/twitter-bot.service'; @Injectable() export class CronService { public constructor( private readonly dataGatheringService: DataGatheringService, - private readonly exchangeRateDataService: ExchangeRateDataService + private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly twitterBotService: TwitterBotService ) {} @Cron(CronExpression.EVERY_MINUTE) @@ -21,6 +23,11 @@ export class CronService { await this.exchangeRateDataService.loadCurrencies(); } + @Cron(CronExpression.EVERY_DAY_AT_6PM) + public async runEveryDayAtSixPM() { + this.twitterBotService.tweetFearAndGreedIndex(); + } + @Cron(CronExpression.EVERY_WEEKEND) public async runEveryWeekend() { await this.dataGatheringService.gatherProfileData(); diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index 86b04954..82cf08cb 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -30,5 +30,9 @@ export interface Environment extends CleanedEnvAccessors { ROOT_URL: string; STRIPE_PUBLIC_KEY: string; STRIPE_SECRET_KEY: string; + TWITTER_ACCESS_TOKEN: string; + TWITTER_ACCESS_TOKEN_SECRET: string; + TWITTER_API_KEY: string; + TWITTER_API_SECRET: string; WEB_AUTH_RP_ID: string; } diff --git a/apps/api/src/services/twitter-bot/twitter-bot.module.ts b/apps/api/src/services/twitter-bot/twitter-bot.module.ts new file mode 100644 index 00000000..d74d6f10 --- /dev/null +++ b/apps/api/src/services/twitter-bot/twitter-bot.module.ts @@ -0,0 +1,11 @@ +import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service'; +import { Module } from '@nestjs/common'; + +@Module({ + exports: [TwitterBotService], + imports: [ConfigurationModule, SymbolModule], + providers: [TwitterBotService] +}) +export class TwitterBotModule {} diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts new file mode 100644 index 00000000..750e4fe3 --- /dev/null +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -0,0 +1,60 @@ +import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { + ghostfolioFearAndGreedIndexDataSource, + ghostfolioFearAndGreedIndexSymbol +} from '@ghostfolio/common/config'; +import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper'; +import { Injectable, Logger } from '@nestjs/common'; +import { TwitterApi, TwitterApiReadWrite } from 'twitter-api-v2'; + +@Injectable() +export class TwitterBotService { + private twitterClient: TwitterApiReadWrite; + + public constructor( + private readonly configurationService: ConfigurationService, + private readonly symbolService: SymbolService + ) { + this.twitterClient = new TwitterApi({ + accessSecret: this.configurationService.get( + 'TWITTER_ACCESS_TOKEN_SECRET' + ), + accessToken: this.configurationService.get('TWITTER_ACCESS_TOKEN'), + appKey: this.configurationService.get('TWITTER_API_KEY'), + appSecret: this.configurationService.get('TWITTER_API_SECRET') + }).readWrite; + } + + public async tweetFearAndGreedIndex() { + if (!this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { + return; + } + + try { + const symbolItem = await this.symbolService.get({ + dataGatheringItem: { + dataSource: ghostfolioFearAndGreedIndexDataSource, + symbol: ghostfolioFearAndGreedIndexSymbol + } + }); + + if (symbolItem?.marketPrice) { + const { emoji, text } = resolveFearAndGreedIndex( + symbolItem.marketPrice + ); + + const status = `Current Market Mood: ${emoji} ${text} (${symbolItem.marketPrice}/100)\n\n#FearAndGreed #Markets #ServiceTweet`; + const { data: createdTweet } = await this.twitterClient.v2.tweet( + status + ); + + Logger.log( + `Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}` + ); + } + } catch (error) { + Logger.error(error); + } + } +} diff --git a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts index a024b0f5..a42c80a3 100644 --- a/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts +++ b/apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts @@ -24,12 +24,9 @@ export class FearAndGreedIndexComponent implements OnChanges, OnInit { public ngOnInit() {} public ngOnChanges() { - this.fearAndGreedIndexEmoji = resolveFearAndGreedIndex( - this.fearAndGreedIndex - ).emoji; + const { emoji, text } = resolveFearAndGreedIndex(this.fearAndGreedIndex); - this.fearAndGreedIndexText = resolveFearAndGreedIndex( - this.fearAndGreedIndex - ).text; + this.fearAndGreedIndexEmoji = emoji; + this.fearAndGreedIndexText = text; } } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 8f2c9d8f..a0473662 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -1,3 +1,5 @@ +import { DataSource } from '@prisma/client'; + import { ToggleOption } from './types'; export const baseCurrency = 'USD'; @@ -14,6 +16,7 @@ export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f'; export const ghostfolioScraperApiSymbolPrefix = '_GF_'; export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`; +export const ghostfolioFearAndGreedIndexDataSource = DataSource.RAKUTEN; export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`; export const locale = 'de-CH'; diff --git a/package.json b/package.json index cd8c0ec8..8e289475 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "stripe": "8.199.0", "svgmap": "2.6.0", "tslib": "2.0.0", + "twitter-api-v2": "1.10.3", "uuid": "8.3.2", "yahoo-finance": "0.3.6", "zone.js": "0.11.4" diff --git a/yarn.lock b/yarn.lock index 8671fd03..756eca7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17714,6 +17714,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +twitter-api-v2@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/twitter-api-v2/-/twitter-api-v2-1.10.3.tgz#07441bd9c4d27433aa0284d900cf60f6328b8239" + integrity sha512-AbCboiTOWv4DUPbAlF43Uyk4iK/QRk354pNdKgtOmv45+BWGB5Kdv6ls+C99pww/DyLBiXgQEnuyGv4d1HdRhw== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"