From 7fd8ae788c710205769d3474efe3ef12e47d1ea6 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 16 Aug 2022 14:03:50 +0430 Subject: [PATCH] Fix prevPrice logic (#5) * Fix prevPrice logic * Add CI for build and test * rename getPrevPrice to getLatestAvailablePrice - Also add getLatestAvailablePriceWithinDuration Co-authored-by: Jayant Krishnamurthy --- .github/workflows/node.js.yml | 29 ++++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- src/__tests__/PriceFeed.test.ts | 48 ++++++++++++++++++++++++-- src/index.ts | 61 +++++++++++++++++++++++++++------ 5 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/node.js.yml diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..fb4c00d --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,29 @@ +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x, 18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run build + - run: npm run lint + - run: npm test diff --git a/package-lock.json b/package-lock.json index 2a6b04c..1bfcb6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pythnetwork/pyth-sdk-js", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@pythnetwork/pyth-sdk-js", - "version": "0.1.0", + "version": "0.2.0", "license": "Apache-2.0", "devDependencies": { "@types/jest": "^27.4.1", diff --git a/package.json b/package.json index f925c08..7046c4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-sdk-js", - "version": "0.1.0", + "version": "0.2.0", "description": "Pyth Network SDK in JS", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/src/__tests__/PriceFeed.test.ts b/src/__tests__/PriceFeed.test.ts index 78aa7d2..a241080 100644 --- a/src/__tests__/PriceFeed.test.ts +++ b/src/__tests__/PriceFeed.test.ts @@ -1,5 +1,9 @@ import { Price, PriceFeed, PriceStatus } from "../index"; +beforeAll(() => { + jest.useFakeTimers(); +}); + test("Parsing Price Feed works as expected", () => { const data = { conf: "1", @@ -28,11 +32,17 @@ test("Parsing Price Feed works as expected", () => { expect(priceFeed.publishTime).toBe(11); expect(priceFeed.getCurrentPrice()).toStrictEqual(new Price("1", 4, "10")); expect(priceFeed.getEmaPrice()).toStrictEqual(new Price("2", 4, "3")); - expect(priceFeed.getPrevPriceUnchecked()).toStrictEqual([ - new Price("7", 4, "8"), - 9, + expect(priceFeed.getLatestAvailablePriceUnchecked()).toStrictEqual([ + new Price("1", 4, "10"), + 11, ]); + jest.setSystemTime(20000); + expect(priceFeed.getLatestAvailablePriceWithinDuration(15)).toStrictEqual( + new Price("1", 4, "10") + ); + expect(priceFeed.getLatestAvailablePriceWithinDuration(5)).toBeUndefined(); + expect(priceFeed.toJson()).toStrictEqual(data); }); @@ -57,3 +67,35 @@ test("getCurrentPrice returns undefined if status is not Trading", () => { const priceFeed = PriceFeed.fromJson(data); expect(priceFeed.getCurrentPrice()).toBeUndefined(); }); + +test("getLatestAvailablePrice returns prevPrice when status is not Trading", () => { + const data = { + conf: "1", + ema_conf: "2", + ema_price: "3", + expo: 4, + id: "abcdef0123456789", + max_num_publishers: 6, + num_publishers: 5, + prev_conf: "7", + prev_price: "8", + prev_publish_time: 9, + price: "10", + product_id: "0123456789abcdef", + publish_time: 11, + status: PriceStatus.Unknown, + }; + + const priceFeed = PriceFeed.fromJson(data); + + expect(priceFeed.getLatestAvailablePriceUnchecked()).toStrictEqual([ + new Price("7", 4, "8"), + 9, + ]); + + jest.setSystemTime(20000); + expect(priceFeed.getLatestAvailablePriceWithinDuration(15)).toStrictEqual( + new Price("7", 4, "8") + ); + expect(priceFeed.getLatestAvailablePriceWithinDuration(10)).toBeUndefined(); +}); diff --git a/src/index.ts b/src/index.ts index f9d4509..c635734 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { Convert, PriceFeed as JsonPriceFeed } from "./schemas/PriceFeed"; export type UnixTimestamp = number; +export type DurationInSeconds = number; export type HexString = string; /** @@ -190,9 +191,14 @@ export class PriceFeed { /** * Get the current price and confidence interval as fixed-point numbers of the form a * 10^e. + * This function returns the current best estimate of the price at the time that this `PriceFeed` was + * published (`publish_time`). This function returns `undefined` if the oracle was unable to determine + * the price at that time; this condition can happen for various reasons, such as certain markets only + * trading during certain times. * - * @returns a struct containing the current price, confidence interval, and the exponent for - * both numbers. Returns `undefined` if price information is currently unavailable for any reason. + * @returns a struct containing the price and confidence interval as of `publish_time`, along with + * the exponent for both numbers. Returns `undefined` if price information is currently unavailable + * for any reason. */ getCurrentPrice(): Price | undefined { if (this.status !== PriceStatus.Trading) { @@ -216,20 +222,55 @@ export class PriceFeed { } /** - * Get the "unchecked" previous price with Trading status, along with the timestamp at which it was generated. + * Get the latest available price, along with the timestamp when it was generated. + * This function returns the same price as `getCurrentPrice` in the case where a price was available + * at the time this `PriceFeed` was published (`publish_time`). However, if a price was not available + * at that time, this function returns the price from the latest time at which the price was available. + * The returned price can be from arbitrarily far in the past; this function makes no guarantees that + * the returned price is recent or useful for any particular application. * - * @returns a struct containing the previous price, confidence interval, and the exponent for - * both numbers along with the timestamp that the price was generated. + * Users of this function should check the returned timestamp to ensure that the returned price is + * sufficiently recent for their application. If you are considering using this function, it may be + * safer / easier to use either `getCurrentPrice` or `getLatestAvailablePriceWithinDuration`. * - * WARNING: - * We make no guarantees about the unchecked price and confidence returned by - * this function: it could differ significantly from the current price. - * We strongly encourage you to use `get_current_price` instead. + * @returns a struct containing the latest available price, confidence interval, and the exponent for + * both numbers along with the timestamp when that price was generated. */ - getPrevPriceUnchecked(): [Price, UnixTimestamp] { + getLatestAvailablePriceUnchecked(): [Price, UnixTimestamp] { + // If the price status is Trading then it's the latest price + // with the Trading status. + if (this.status === PriceStatus.Trading) { + return [new Price(this.conf, this.expo, this.price), this.publishTime]; + } + return [ new Price(this.prevConf, this.expo, this.prevPrice), this.prevPublishTime, ]; } + + /** + * Get the latest price as long as it was updated within `duration` seconds of the current time. + * This function is a sanity-checked version of `getLatestAvailablePriceUnchecked` which is useful in + * applications that require a sufficiently-recent price. Returns `undefined` if the price wasn't + * updated sufficiently recently. + * + * @param duration return a price as long as it has been updated within this number of seconds + * @returns a struct containing the latest available price, confidence interval and the exponent for + * both numbers, or `undefined` if no price update occurred within `duration` seconds of the current time. + */ + getLatestAvailablePriceWithinDuration(duration: DurationInSeconds): Price | undefined { + const [price, timestamp] = this.getLatestAvailablePriceUnchecked(); + + const currentTime: UnixTimestamp = Math.floor(Date.now() / 1000); + + // This checks the absolute difference as a sanity check + // for the cases that the system time is behind or price + // feed timestamp happen to be in the future (a bug). + if (Math.abs(currentTime - timestamp) > duration) { + return undefined; + } + + return price; + } }