nomics-platform
Advanced tools
Comparing version 0.13.4 to 0.13.5
@@ -110,3 +110,4 @@ const axios = require('axios') | ||
try { | ||
valid = (new Date(d.toISOString())).getTime() === d.getTime() | ||
valid = new Date(d.toISOString()).getTime() === d.getTime() | ||
valid = valid && o[p].slice(-1) === 'Z' | ||
} catch (e) { | ||
@@ -113,0 +114,0 @@ err = e.toString() + ' ' |
@@ -18,37 +18,43 @@ const au = require('../au') | ||
info: async () => { | ||
au.assertStringProperty(this.info, 'name') | ||
au.assertStringProperty(this.info, 'description', { required: true }) | ||
try { | ||
au.assertStringProperty(this.info, 'name') | ||
au.assertStringProperty(this.info, 'description', { required: true }) | ||
if (!process.env.NOMICS_PLATFORM_RELAX) { | ||
au.assert(this.info.description.length >= 1000, 'Expected description to be at least 1,000 characters') | ||
if (!process.env.NOMICS_PLATFORM_RELAX) { | ||
au.assert(this.info.description.length >= 1000, 'Expected description to be at least 1,000 characters') | ||
au.assertStringProperty(this.info, 'location', { required: true }) | ||
au.assertURLProperty(this.info, 'logo', { required: true, https: true }) | ||
} | ||
au.assertStringProperty(this.info, 'location', { required: true }) | ||
au.assertURLProperty(this.info, 'logo', { required: true, https: true }) | ||
} | ||
au.assertURLProperty(this.info, 'website', { required: true }) | ||
au.assertStringProperty(this.info, 'twitter', { required: false }) | ||
if (au.assertProperty(this.info, 'capability', { required: false })) { | ||
au.assertPropertyType(this.info, 'capability', 'object') | ||
const capability = this.info.capability | ||
au.assertBooleanProperty(capability, 'candles', { required: false }) | ||
au.assertBooleanProperty(capability, 'markets', { required: false }) | ||
au.assertBooleanProperty(capability, 'orders', { required: false }) | ||
au.assertBooleanProperty(capability, 'ordersSnapshot', { required: false }) | ||
au.assertBooleanProperty(capability, 'ordersSocket', { required: false }) | ||
au.assertBooleanProperty(capability, 'ticker', { required: false }) | ||
au.assertBooleanProperty(capability, 'trades', { required: false }) | ||
au.assertBooleanProperty(capability, 'tradesByTimestamp', { required: false }) | ||
au.assertBooleanProperty(capability, 'tradesSocket', { required: false }) | ||
au.assertURLProperty(this.info, 'website', { required: true }) | ||
au.assertStringProperty(this.info, 'twitter', { required: false }) | ||
if (au.assertProperty(this.info, 'capability', { required: false })) { | ||
au.assertPropertyType(this.info, 'capability', 'object') | ||
const capability = this.info.capability | ||
au.assertBooleanProperty(capability, 'candles', { required: false }) | ||
au.assertBooleanProperty(capability, 'markets', { required: false }) | ||
au.assertBooleanProperty(capability, 'orders', { required: false }) | ||
au.assertBooleanProperty(capability, 'ordersSnapshot', { required: false }) | ||
au.assertBooleanProperty(capability, 'ordersSocket', { required: false }) | ||
au.assertBooleanProperty(capability, 'ticker', { required: false }) | ||
au.assertBooleanProperty(capability, 'trades', { required: false }) | ||
au.assertBooleanProperty(capability, 'tradesByTimestamp', { required: false }) | ||
au.assertBooleanProperty(capability, 'tradesSocket', { required: false }) | ||
} | ||
au.assert( | ||
this.info.capability.trades || | ||
this.info.capability.tradesByTimestamp || | ||
this.info.capability.orders || | ||
this.info.capability.ordersSnapshot || | ||
this.info.capability.ticker || | ||
this.info.capability.candles, | ||
'Expected at least one data capability of trades, candles, orders, or ticker' | ||
) | ||
} catch (err) { | ||
console.log(`FAILED: URL /info failed with "${err.message}"`) | ||
throw err | ||
} | ||
au.assert( | ||
this.info.capability.trades || | ||
this.info.capability.tradesByTimestamp || | ||
this.info.capability.orders || | ||
this.info.capability.ordersSnapshot || | ||
this.info.capability.ticker || | ||
this.info.capability.candles, | ||
'Expected at least one data capability of trades, candles, orders, or ticker' | ||
) | ||
}, | ||
@@ -61,19 +67,31 @@ | ||
const data = await au.get('/markets') | ||
try { | ||
const data = await au.get('/markets') | ||
au.assert(data.length > 0, 'Expected at least one market') | ||
data.forEach(m => au.assertStringProperty(m, 'id')) | ||
data.forEach(m => au.assertStringProperty(m, 'base', { required: false })) | ||
data.forEach(m => au.assertStringProperty(m, 'quote', { required: false })) | ||
au.assert(data.length > 0, 'Expected at least one market') | ||
const idCounts = data.reduce((memo, m) => { | ||
if (!memo[m.id]) { | ||
memo[m.id] = 0 | ||
data.forEach(m => au.assertStringProperty(m, 'id')) | ||
data.forEach(m => au.assertStringProperty(m, 'base')) | ||
data.forEach(m => au.assertStringProperty(m, 'quote')) | ||
if (!process.env.NOMICS_PLATFORM_RELAX) { | ||
data.forEach(t => au.assertPropertyInSet(t, 'type', ['spot', 'future', 'option', 'derivative', 'index'])) | ||
data.forEach(m => au.assertBooleanProperty(m, 'active')) | ||
} | ||
memo[m.id]++ | ||
return memo | ||
}, {}) | ||
Object.keys(idCounts).forEach(id => { | ||
au.assert(idCounts[id] === 1, `Duplicate market ID: ${id}`) | ||
}) | ||
const idCounts = data.reduce((memo, m) => { | ||
if (!memo[m.id]) { | ||
memo[m.id] = 0 | ||
} | ||
memo[m.id]++ | ||
return memo | ||
}, {}) | ||
Object.keys(idCounts).forEach(id => { | ||
au.assert(idCounts[id] === 1, `Duplicate market ID: ${id}`) | ||
}) | ||
} catch (err) { | ||
console.log(`FAILED: URL /markets failed with "${err.message}"`) | ||
throw err | ||
} | ||
}, | ||
@@ -104,2 +122,3 @@ | ||
au.assert(trades.length > 2, 'Expected more than two trades') | ||
trades.forEach(t => au.assertStringProperty(t, 'id')) | ||
@@ -174,7 +193,7 @@ trades.forEach(t => au.assertTimestampProperty(t, 'timestamp')) | ||
!second.find(t => t.timestamp < trades[0].timestamp), | ||
`Trades with since=${trades[0].id} contained a trade with a timestamp before the since parameter. Only trades *after or equal to* the since id should be returned` | ||
`Trades with since=${trades[0].timestamp} contained a trade with a timestamp before the since parameter. Only trades *after or equal to* the since id should be returned` | ||
) | ||
au.assert( | ||
second.find(t2 => t2.id === trades.find(t => t.timestamp > since).id), | ||
`Trades with since=${trades[0].id} didn't contain an overlap with the first page. Expected to see trade with id ${trades[1].id}` | ||
`Trades with since=${trades[0].timestamp} didn't contain an overlap with the first page. Expected to see trade with id ${trades[1].id}` | ||
) | ||
@@ -193,2 +212,41 @@ | ||
tradesSnapshot: async () => { | ||
if (!this.info.capability || !this.info.capability.markets || !this.info.capability.tradesSnapshot) { | ||
return | ||
} | ||
const markets = await au.get('/markets') | ||
if (markets.length === 0) { | ||
// If there are no markets, pass, since markets spec will fail | ||
return | ||
} | ||
let success = false | ||
let uri | ||
for (let i = 0; i < markets.length && !success; i++) { | ||
uri = null | ||
try { | ||
const market = markets[i] | ||
uri = `/trades/snapshot?market=${encodeURIComponent(market.id)}` | ||
const trades = await au.get(uri) | ||
au.assert(trades.length > 2, 'Expected more than two trades') | ||
trades.forEach(t => au.assertStringProperty(t, 'id')) | ||
trades.forEach(t => au.assertTimestampProperty(t, 'timestamp')) | ||
trades.forEach(t => au.assertNumericStringProperty(t, 'price')) | ||
trades.forEach(t => au.assertNumericStringProperty(t, 'amount')) | ||
trades.forEach(t => au.assertStringProperty(t, 'order', { required: false })) | ||
trades.forEach(t => au.assertPropertyInSet(t, 'type', ['market', 'limit', 'ask', 'bid'], { required: false })) | ||
trades.forEach(t => au.assertPropertyInSet(t, 'side', ['buy', 'sell'], { required: false })) | ||
} catch (err) { | ||
console.log(`FAILED: URL ${uri || err.stack} failed with "${err.message}"`) | ||
throw err | ||
} | ||
} | ||
}, | ||
ordersSnapshot: async () => { | ||
@@ -279,82 +337,85 @@ if (!this.info.capability || !this.info.capability.markets || !this.info.capability.ordersSnapshot) { | ||
const intervals = ['1d', '1h', '1m'] | ||
let uri | ||
for (const interval of intervals) { | ||
let candles | ||
try { | ||
for (const interval of intervals) { | ||
uri = `/candles?market=${encodeURIComponent(market.id)}&interval=${interval}` | ||
try { | ||
candles = await au.get(`/candles?market=${encodeURIComponent(market.id)}&interval=${interval}`) | ||
} catch (e) { | ||
au.assert(false, `Expected to find candles endpoint for interval \`${interval}\``) | ||
} | ||
const candles = await au.get(uri) | ||
const c = JSON.parse(JSON.stringify(candles)) // deep clone | ||
let t = candles.map(c => c.timestamp) | ||
let tSorted = c.map(c => c.timestamp).sort() | ||
const c = JSON.parse(JSON.stringify(candles)) // deep clone | ||
let t = candles.map(c => c.timestamp) | ||
let tSorted = c.map(c => c.timestamp).sort() | ||
au.assert( | ||
JSON.stringify(t) === JSON.stringify(tSorted), | ||
`Expected ${interval} candles to be sorted by timestamp ascending` | ||
) | ||
if (interval === '1d') { | ||
au.assert(candles.length >= 7, 'Expected at least 7 1d candles') | ||
au.assert( | ||
new Date(candles[candles.length - 1].timestamp).getTime() > new Date().getTime() - 2 * DAY, | ||
'Expected last 1d candle to be within the last 48 hours' | ||
JSON.stringify(t) === JSON.stringify(tSorted), | ||
`Expected ${interval} candles to be sorted by timestamp ascending` | ||
) | ||
} | ||
if (interval === '1h') { | ||
au.assert(candles.length >= 24, 'Expected at least 24 1h candles') | ||
au.assert( | ||
new Date(candles[candles.length - 1].timestamp).getTime() > new Date().getTime() - 2 * HOUR, | ||
'Expected last 1h candle to be within the last 2 hours' | ||
) | ||
} | ||
if (interval === '1m') { | ||
au.assert(candles.length >= 60, 'Expected at least 60 1m candles') | ||
au.assert( | ||
new Date(candles[candles.length - 1].timestamp).getTime() > new Date().getTime() - 10 * MINUTE, | ||
'Expected last 1m candle to be within the last 10 minutes' | ||
) | ||
} | ||
candles.forEach(c => { | ||
au.assertTimestampProperty(c, 'timestamp') | ||
let date = new Date(c.timestamp) | ||
if (interval === '1d') { | ||
au.assert(date.getTime() % DAY === 0, 'Expected timestamp to aligned to day candle size in UTC') | ||
au.assert(candles.length >= 7, 'Expected at least 7 1d candles') | ||
au.assert( | ||
new Date(candles[candles.length - 1].timestamp).getTime() > new Date().getTime() - 2 * DAY, | ||
'Expected last 1d candle to be within the last 48 hours' | ||
) | ||
} | ||
if (interval === '1h') { | ||
au.assert(date.getTime() % HOUR === 0, 'Expected timestamp to aligned to hour candle size in UTC') | ||
au.assert(candles.length >= 24, 'Expected at least 24 1h candles') | ||
au.assert( | ||
new Date(candles[candles.length - 1].timestamp).getTime() > new Date().getTime() - 2 * HOUR, | ||
'Expected last 1h candle to be within the last 2 hours' | ||
) | ||
} | ||
if (interval === '1d') { | ||
au.assert(date.getTime() % MINUTE === 0, 'Expected timestamp to aligned to minute candle size in UTC') | ||
if (interval === '1m') { | ||
au.assert(candles.length >= 60, 'Expected at least 60 1m candles') | ||
au.assert( | ||
new Date(candles[candles.length - 1].timestamp).getTime() > new Date().getTime() - 10 * MINUTE, | ||
'Expected last 1m candle to be within the last 10 minutes' | ||
) | ||
} | ||
}) | ||
candles.forEach(c => { | ||
au.assertNumericStringProperty(c, 'open') | ||
candles.forEach(c => { | ||
au.assertTimestampProperty(c, 'timestamp') | ||
au.assertNumericStringProperty(c, 'high') | ||
au.assert(parseFloat(c.high) >= parseFloat(c.open), 'Expected high to be greater than or equal to open') | ||
au.assert(parseFloat(c.high) >= parseFloat(c.close), 'Expected high to be greater than or equal to close') | ||
au.assert(parseFloat(c.high) >= parseFloat(c.low), 'Expected high to be greater than or equal to low') | ||
let date = new Date(c.timestamp) | ||
au.assertNumericStringProperty(c, 'low') | ||
au.assert(parseFloat(c.low) > 0, 'Expected low to be greater than 0') | ||
au.assert(parseFloat(c.low) <= parseFloat(c.open), 'Expected low to be less than or equal to open') | ||
au.assert(parseFloat(c.low) <= parseFloat(c.close), 'Expected low to be less than or equal to close') | ||
au.assert(parseFloat(c.low) <= parseFloat(c.high), 'Expected low to be less than or equal to high') | ||
if (interval === '1d') { | ||
au.assert(date.getTime() % DAY === 0, 'Expected timestamp to aligned to day candle size in UTC') | ||
} | ||
au.assertNumericStringProperty(c, 'close') | ||
if (interval === '1h') { | ||
au.assert(date.getTime() % HOUR === 0, 'Expected timestamp to aligned to hour candle size in UTC') | ||
} | ||
au.assertNumericStringProperty(c, 'volume') | ||
au.assert(parseFloat(c.volume) >= 0, 'Expected volume to be greater than or equal to 0') | ||
}) | ||
if (interval === '1d') { | ||
au.assert(date.getTime() % MINUTE === 0, 'Expected timestamp to aligned to minute candle size in UTC') | ||
} | ||
}) | ||
candles.forEach(c => { | ||
au.assertNumericStringProperty(c, 'open') | ||
au.assertNumericStringProperty(c, 'high') | ||
au.assert(parseFloat(c.high) >= parseFloat(c.open), 'Expected high to be greater than or equal to open') | ||
au.assert(parseFloat(c.high) >= parseFloat(c.close), 'Expected high to be greater than or equal to close') | ||
au.assert(parseFloat(c.high) >= parseFloat(c.low), 'Expected high to be greater than or equal to low') | ||
au.assertNumericStringProperty(c, 'low') | ||
au.assert(parseFloat(c.low) > 0, 'Expected low to be greater than 0') | ||
au.assert(parseFloat(c.low) <= parseFloat(c.open), 'Expected low to be less than or equal to open') | ||
au.assert(parseFloat(c.low) <= parseFloat(c.close), 'Expected low to be less than or equal to close') | ||
au.assert(parseFloat(c.low) <= parseFloat(c.high), 'Expected low to be less than or equal to high') | ||
au.assertNumericStringProperty(c, 'close') | ||
au.assertNumericStringProperty(c, 'volume') | ||
au.assert(parseFloat(c.volume) >= 0, 'Expected volume to be greater than or equal to 0') | ||
}) | ||
} | ||
} catch (err) { | ||
console.log(`FAILED: URL ${uri || err.stack} failed with "${err.message}"`) | ||
throw err | ||
} | ||
@@ -361,0 +422,0 @@ }, |
{ | ||
"name": "nomics-platform", | ||
"version": "0.13.4", | ||
"version": "0.13.5", | ||
"description": "Nomics Platform Toolkit", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -16,3 +16,4 @@ # Nomics Platform | ||
If data functionality needs to be audited prior to all metadata being available, you can use the `NOMICS_PLATFORM_RELAX` | ||
environment variable to temporarily relax requirements for description length, logo URL, and location. | ||
environment variable to temporarily relax requirements for description length, logo URL, and location. This flag will | ||
also skip checks for markets `type` and `active` flags. | ||
@@ -19,0 +20,0 @@ ```bash |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
27474
602
26
4