UNPKG

@tomei/live-price

Version:

Tomei live-price Package

556 lines (516 loc) 21.7 kB
import { ClassError, ObjectBase } from '@tomei/general'; import { FeedCutOffHistory } from '../feed-cut-off-history/feed-cut-off-history'; import { ISourceFeedAttr } from '../../interfaces/price-source-feed-attr.interface'; import { SourceFeedRepository } from './source-feed.repository'; import { ActivationTrigger, NonSourceFeedName, SourceFeedName, } from '../../enum/feed.enum'; import { IPriceData } from '../../interfaces/price-data.interface'; import { FeedHistory } from '../feed-history/feed-history'; import { Feed } from '../feed/feed'; import { IFeedAttr } from '../../interfaces/price-feed-attr.interface'; import { FeedConfig } from '../feed-config/feed-config'; import { FeedHistoryRepository } from '../feed-history/feed-history.repository'; import { Op, Transaction } from 'sequelize'; import { LoginUser } from '@tomei/sso'; import { PriceSourceBase } from '../../base/PriceSourceBase'; import { TomeiPriceHistory } from '../tomei-price/tomei-price'; import { ActionEnum, Activity, ActivityHistoryRepository, } from '@tomei/activity-history'; export class SourceFeed extends Feed implements ISourceFeedAttr { ObjectId: string; ObjectName: string; ObjectType: string; TableName: string; FeedName: SourceFeedName; Status: string; IsSourceFeedAvailableYN: string; IsFeedCutOffYN: string; CurrentCutOffHistoryId: string; AutomaticCutOffVariance: number; AutomaticCutOffMinutes: number; protected static _SourceRepo = new SourceFeedRepository(); protected static _FeedHistoryRepo = new FeedHistoryRepository(); protected static _ActivityRepo = new ActivityHistoryRepository(); protected constructor( sourcefeedAttr?: ISourceFeedAttr, feedAttr?: IFeedAttr, ) { super(feedAttr); if (sourcefeedAttr) { this.FeedName = sourcefeedAttr.FeedName; this.Status = sourcefeedAttr.Status; this.IsSourceFeedAvailableYN = sourcefeedAttr.IsSourceFeedAvailableYN; this.IsFeedCutOffYN = sourcefeedAttr.IsFeedCutOffYN; this.CurrentCutOffHistoryId = sourcefeedAttr.CurrentCutOffHistoryId; this.AutomaticCutOffVariance = sourcefeedAttr.AutomaticCutOffVariance; this.AutomaticCutOffMinutes = sourcefeedAttr.AutomaticCutOffMinutes; } } public static async init(dbTransaction: any, FeedName?: string) { try { if (FeedName) { const sourcefeed = await SourceFeed._SourceRepo.findByPk( FeedName, dbTransaction, ); const feed = await Feed._Repo.findByPk(FeedName, dbTransaction); if (sourcefeed) { return new SourceFeed(sourcefeed, feed); } else { throw new ClassError('SourceFeed', 'FeedErrMsg00', 'Feed Not Found'); } } return new SourceFeed(); } catch (error) { throw new ClassError('SourceFeed', 'FeedErrMsg01', error.message); } } public async save(dbTransaction: any) { try { await super.save(dbTransaction); const sourcefeed = await SourceFeed._SourceRepo.findOne({ where: { FeedName: this.FeedName, }, transaction: dbTransaction, }); const payload: ISourceFeedAttr = { FeedName: this.FeedName, Status: this.Status, IsSourceFeedAvailableYN: this.IsSourceFeedAvailableYN, IsFeedCutOffYN: this.IsFeedCutOffYN, CurrentCutOffHistoryId: this.CurrentCutOffHistoryId, AutomaticCutOffVariance: this.AutomaticCutOffVariance, AutomaticCutOffMinutes: this.AutomaticCutOffMinutes, }; if (sourcefeed) { await SourceFeed._SourceRepo.update(payload, { where: { FeedName: this.FeedName, }, transaction: dbTransaction, }); } else { await SourceFeed._SourceRepo.create(payload, { transaction: dbTransaction, }); } } catch (error) { throw new ClassError('Feed', 'FeedErrMsg02', error.message); } } public async getCutOffDetails(dbTransaction: any) { try { if (this.CurrentCutOffHistoryId) { return await FeedCutOffHistory.init( this.CurrentCutOffHistoryId, dbTransaction, ); } return null; } catch (error) { throw new ClassError('Feed', 'FeedErrMsg03', error.message); } } public async recordFeedPrice( priceData: IPriceData, dbTransaction: any, ): Promise<FeedHistory> { try { // Based on the current price feed from MKS, set the following priceFeedData: // Date & Time // Product - Set it to the product name as per No. 1 above. const feedName = this.FeedName; // Source Feed Buy - The original buy price from MKS. NA is the MKS feed is unavailable. const sourceFeedBuyPrice = priceData.BuyPrice; // Source Feed Sell - The original sell price from MKS. NA is the MKS feed is unavailable. const sourceFeedSellPrice = priceData.SellPrice; // Feed Buy - Set the Feed Buy Price based on the following logic: // If Manual Price is Activated, set the Feed Buy price to Manual Buy Price. // Else If Cut-Off is Activated be it by the automatic circuit breaker or manually, set the Feed Buy price to NA. // Else // If MKS feed is available, set the Feed Buy price to MKS Buy price. // Else If MKS Feed is unavailable, set the Feed Buy Price to NA. // Feed Sell - Set the Feed Sell Price based on the following logic: // If Manual Price is Activated, set the Feed Sell price to Manual Sell Price. // Else If Cut-Off is Activated be it by the automatic circuit breaker or manually, set the Feed Sell price to NA. // Else // If MKS feed is available, set the Feed Sell price to MKS Sell price. // Else If MKS Feed is unavailable, set the Feed Buy Price to NA. let FeedBuyPrice: number; let FeedSellPrice: number; // MCActivatedYN (Manual Cut-Off) - Yes or No, depending if Manual Cut Off has been activated. let IsManualCutOffYN = 'N'; // CBActivatedYN (Circuit Breaker) - Yes or No, depending if Circuit Breaker has been activated. let IsCircuitBreakerActivatedYN = 'N'; if (this.IsManualPriceActivatedYN === 'Y') { //Get latest price history const manualPriceHistory = await FeedHistory.getLatestPrice( this.FeedName, this.FeedName.split('/')[1], null, dbTransaction, ); FeedBuyPrice = manualPriceHistory.FeedBuyPrice; FeedSellPrice = manualPriceHistory.FeedSellPrice; } else if (this.IsFeedCutOffYN === 'Y') { FeedBuyPrice = 0; FeedSellPrice = 0; //Retrive the cut off details const feedCutOffHistory = await FeedCutOffHistory.init( this.CurrentCutOffHistoryId, dbTransaction, ); if (feedCutOffHistory.ActivationTrigger === ActivationTrigger.MANUAL) { IsManualCutOffYN = 'Y'; } else { IsCircuitBreakerActivatedYN = 'Y'; } } else { FeedBuyPrice = priceData.SellPrice; FeedSellPrice = priceData.SellPrice; } // SFAvailableYN (Source Feed) - Yes or No, depending if MKS feed is available. const isSourceFeedAvailableYN = priceData.IsSourceFeedAvailableYN; // MCActivatedYN (Manual Cut-Off) - Yes or No, depending if Manual Cut Off has been activated. const isManualPriceActivatedYN = this.IsManualPriceActivatedYN; // Store the price feed information in price_PriceFeedHistory table using the saveFeedPrice method. let feedHistory = await FeedHistory.init(dbTransaction); feedHistory = await feedHistory.saveFeedPrice( { FeedName: feedName, DateTime: new Date(), Currency: priceData.Currency, SourceFeedBuyPrice: sourceFeedBuyPrice, SourceFeedSellPrice: sourceFeedSellPrice, FeedBuyPrice: FeedBuyPrice, FeedSellPrice: FeedSellPrice, IsSourceFeedAvailableYN: isSourceFeedAvailableYN, IsCircuitBreakerActivatedYN: IsCircuitBreakerActivatedYN, IsManualPriceActivatedYN: isManualPriceActivatedYN, IsManualCutOffYN: IsManualCutOffYN, }, dbTransaction, ); // Check Feed Buy and Feed Sell: // If Feed Buy and Feed Sell is not NA, null or 0, then set Feed.Status in the price_Feed table to "Available". // If Feed Buy and Feed Sell is NA, null or 0, then set Feed.Status in the price_Feed table to "Unavailable". if (FeedBuyPrice !== 0 && FeedSellPrice !== 0) { this.Status = 'Available'; } else { this.Status = 'Unavailable'; } // Save the Feed Status to the price_Feed table using the save method. await this.save(dbTransaction); return feedHistory; } catch (error) { throw error; } } private async getPreviousSourceFeedConfig(dbTransaction: any): Promise< | { AutomaticCutOffVariance: number; AutomaticCutOffMinutes: number; UpdatedAt: Date; } | false > { try { const previousActivity = await SourceFeed._ActivityRepo.findOne({ where: { EntityType: 'SourceFeed', EntityId: this.FeedName, Action: ActionEnum.UPDATE, Description: 'Update Circuit Breaker Config for :' + this.FeedName, }, transaction: dbTransaction, order: [['PerformedAt', 'DESC']], }); if (previousActivity) { const previousConfig: { AutomaticCutOffVariance: number; AutomaticCutOffMinutes: number; FeedName: string; Status: string; IsSourceFeedAvailableYN: string; IsFeedCutOffYN: string; CurrentCutOffHistoryId: string; } = JSON.parse(previousActivity.EntityValueBefore); return { AutomaticCutOffVariance: previousConfig.AutomaticCutOffVariance, AutomaticCutOffMinutes: previousConfig.AutomaticCutOffMinutes, UpdatedAt: previousActivity.PerformedAt, }; } return false; // No previous configuration found } catch (error) { throw error; } } public async circuitBreakerCheck( priceData: IPriceData, priceSource: PriceSourceBase<SourceFeed>, dbTransaction?: any, ): Promise<boolean> { // Circuit breaker or automatic price cut-off will be activated based on the settings in feed config. // The variance check will be applied to the Source Prices from price_PriceFeedHistory table. // For example, if the user sets up 5% within 5 minutes, the system will automatically cut off the feed // when there is a 5% variance in price compared to the price 5 minutes back. This applies to both Sell and Buy prices. try { let automaticCutOffVariance = this.AutomaticCutOffVariance; // Default variance from the current configuration let automaticCutOffMinutes = this.AutomaticCutOffMinutes; // Default time window from the current configuration // Step 1: Retrieve the previous configuration from the database const previousConfig = await this.getPreviousSourceFeedConfig(dbTransaction); // Step 2: Check if a previous configuration exists if (previousConfig) { const currentTime = new Date().getTime(); // Get the current timestamp const previousConfigUpdatedTime = previousConfig.UpdatedAt.getTime(); // Timestamp when the previous config was updated const currentConfigTimeWindow = automaticCutOffMinutes * 60000; // Convert current time window to milliseconds // Step 3: Determine the effective start time for observing the new configuration // This ensures the new configuration only takes effect after the previous time window has elapsed const effectiveStartTime = Math.max( previousConfigUpdatedTime + currentConfigTimeWindow, currentTime, ); // Step 4: Decide whether to use the previous or current configuration if (currentTime < effectiveStartTime) { // If the current time is before the effective start time, continue using the previous configuration automaticCutOffVariance = previousConfig.AutomaticCutOffVariance; automaticCutOffMinutes = previousConfig.AutomaticCutOffMinutes; } else { // If the current time is after the effective start time, switch to the current configuration automaticCutOffVariance = this.AutomaticCutOffVariance; automaticCutOffMinutes = this.AutomaticCutOffMinutes; } } else { // If no previous configuration exists, use the current configuration values automaticCutOffVariance = this.AutomaticCutOffVariance; automaticCutOffMinutes = this.AutomaticCutOffMinutes; } // Step 5: Retrieve the feed history for the last `automaticCutOffMinutes` minutes const feedHistories = await SourceFeed._FeedHistoryRepo.findAll({ where: { FeedName: this.FeedName, DateTime: { [Op.gte]: new Date( new Date().getTime() - this.AutomaticCutOffMinutes * 60000, ), }, }, order: [['DateTime', 'DESC']], transaction: dbTransaction, }); // Step 6: Skip circuit breaker check if any feed history is manually activated if ( feedHistories.some( ({ IsManualPriceActivatedYN }) => IsManualPriceActivatedYN === 'Y', ) ) { return true; // Skip the circuit breaker check } // Step 7: Filter out feed histories with non-zero prices const validFeedHistories = feedHistories.filter( ({ SourceFeedBuyPrice, SourceFeedSellPrice }) => SourceFeedBuyPrice !== 0 && SourceFeedSellPrice !== 0, ); // Step 8: Skip circuit breaker check if no valid feed histories exist if (validFeedHistories.length === 0) return true; // Step 9: Extract buy and sell prices from feed histories const buyPrices = validFeedHistories.map( ({ SourceFeedBuyPrice }) => SourceFeedBuyPrice, ); const sellPrices = validFeedHistories.map( ({ SourceFeedSellPrice }) => SourceFeedSellPrice, ); // Step 10: Include current price data in the range calculations buyPrices.push(priceData.BuyPrice); sellPrices.push(priceData.SellPrice); // Step 11: Calculate min and max for buy and sell prices const [minBuyPrice, maxBuyPrice] = [ Math.min(...buyPrices), Math.max(...buyPrices), ]; const [minSellPrice, maxSellPrice] = [ Math.min(...sellPrices), Math.max(...sellPrices), ]; // Step 12: Calculate variances for buy and sell prices const buyVariance = ((maxBuyPrice - minBuyPrice) / minBuyPrice) * 100; const sellVariance = ((maxSellPrice - minSellPrice) / minSellPrice) * 100; // Step 13: Trigger circuit breaker if variance exceeds threshold and feed is not already cut off if ( (buyVariance > this.AutomaticCutOffVariance || sellVariance > this.AutomaticCutOffVariance) && this.IsFeedCutOffYN === 'N' ) { await this.cutOff(priceSource, dbTransaction); // Trigger the circuit breaker return false; // Circuit breaker triggered } return true; // Circuit breaker not triggered } catch (error) { throw error; // Handle any errors that occur during the process } } public async cutOff( priceSource: PriceSourceBase<SourceFeed>, dbTransaction: any, activatedById?: string, ) { try { //create a new feed cut off history let feedCutOffHistory = await FeedCutOffHistory.init(); feedCutOffHistory.FeedName = this.FeedName; let activatedBy = null; let isManualCutOff = 'N'; let isCircuitBreakerActivated = 'N'; if (activatedById) { //If the feed is cut off manually, set the activation trigger to manual const user = await LoginUser.init(null, parseInt(activatedById)); activatedBy = user.FullName; feedCutOffHistory.ActivationTrigger = ActivationTrigger.MANUAL; isManualCutOff = 'Y'; } else { //If the feed is cut off automatically, set the activation trigger to limit breach feedCutOffHistory.ActivationTrigger = ActivationTrigger.LIMIT_BREACH; isCircuitBreakerActivated = 'Y'; } //create a new feed cut off history record feedCutOffHistory = await feedCutOffHistory.create( dbTransaction, activatedById, ); //create a new feed history record let feedHistory = await FeedHistory.init(dbTransaction); const fetchResult = await priceSource.fetchPrice({ [this.FeedName]: this, }); const priceData = fetchResult[this.FeedName]; feedHistory = await feedHistory.saveFeedPrice( { FeedName: this.FeedName, DateTime: new Date(), Currency: priceData.Currency, SourceFeedBuyPrice: priceData.BuyPrice, SourceFeedSellPrice: priceData.SellPrice, FeedBuyPrice: 0, FeedSellPrice: 0, IsSourceFeedAvailableYN: priceData.IsSourceFeedAvailableYN, IsManualCutOffYN: isManualCutOff, IsCircuitBreakerActivatedYN: isCircuitBreakerActivated, IsManualPriceActivatedYN: 'N', }, dbTransaction, ); //create a new tomei price history await this.createTomeiPriceHistory(feedHistory, dbTransaction); //update the feed status to cut off this.IsFeedCutOffYN = 'Y'; this.CurrentCutOffHistoryId = feedCutOffHistory.FeedCutOffHistoryId; //save the feed status await this.save(dbTransaction); } catch (error) { throw error; } } public async deactivateCutOff( priceSource: PriceSourceBase<SourceFeed>, dbTransaction: any, deactivatedById: string, ) { try { //deactivate the feed cut off await FeedCutOffHistory.deactivate( this.FeedName, deactivatedById, dbTransaction, ); //update the feed status this.IsFeedCutOffYN = 'N'; //update the feed record in the database await this.save(dbTransaction); //Retrieve the price from Source Feed const fetchResult = await priceSource.fetchPrice({ [this.FeedName]: this, }); const priceData = fetchResult[this.FeedName]; //Record the feed price const feedHistory = await this.recordFeedPrice(priceData, dbTransaction); //Create a new tomei price history await this.createTomeiPriceHistory(feedHistory, dbTransaction); } catch (error) { throw error; } } public async updateCircuitBreakerConfig( AutomaticCutOffVariance: number, AutomaticCutOffMinutes: number, dbTransaction: any, loginUser: LoginUser, ) { try { const EntityValueBefore = { AutomaticCutOffVariance: this.AutomaticCutOffVariance, AutomaticCutOffMinutes: this.AutomaticCutOffMinutes, FeedName: this.FeedName, Status: this.Status, IsSourceFeedAvailableYN: this.IsSourceFeedAvailableYN, IsFeedCutOffYN: this.IsFeedCutOffYN, CurrentCutOffHistoryId: this.CurrentCutOffHistoryId, }; this.AutomaticCutOffVariance = AutomaticCutOffVariance; this.AutomaticCutOffMinutes = AutomaticCutOffMinutes; await SourceFeed._SourceRepo.update( { AutomaticCutOffVariance: this.AutomaticCutOffVariance, AutomaticCutOffMinutes: this.AutomaticCutOffMinutes, }, { where: { FeedName: this.FeedName, }, transaction: dbTransaction, }, ); // Log the activity history for updating the circuit breaker configuration const activity = new Activity(); activity.ActivityId = activity.createId(); activity.Description = 'Update Circuit Breaker Config for :' + this.FeedName; activity.Action = ActionEnum.UPDATE; activity.EntityType = 'SourceFeed'; activity.EntityId = this.FeedName; activity.EntityValueBefore = JSON.stringify(EntityValueBefore); activity.EntityValueAfter = JSON.stringify({ AutomaticCutOffVariance: this.AutomaticCutOffVariance, AutomaticCutOffMinutes: this.AutomaticCutOffMinutes, FeedName: this.FeedName, Status: this.Status, IsSourceFeedAvailableYN: this.IsSourceFeedAvailableYN, IsFeedCutOffYN: this.IsFeedCutOffYN, CurrentCutOffHistoryId: this.CurrentCutOffHistoryId, }); await activity.create(loginUser.ObjectId, dbTransaction); } catch (error) { throw error; } } public static async getAllSourceFeed( dbTransaction: any, ): Promise<SourceFeed[]> { try { const sourceFeeds = await SourceFeed._SourceRepo.findAll({ transaction: dbTransaction, }); return sourceFeeds.map((sourceFeed) => new SourceFeed(sourceFeed)); } catch (error) { throw error; } } }