@tomei/live-price
Version:
Tomei live-price Package
556 lines (516 loc) • 21.7 kB
text/typescript
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;
}
}
}