Added simple API, updated README
This commit is contained in:
parent
5d2710350e
commit
a7a7e2058c
8 changed files with 109 additions and 5 deletions
|
|
@ -15,7 +15,7 @@ npm install
|
||||||
|
|
||||||
### 2. Run the Bot
|
### 2. Run the Bot
|
||||||
|
|
||||||
**Basic:**
|
**With default parameters:**
|
||||||
```bash
|
```bash
|
||||||
node index.js
|
node index.js
|
||||||
```
|
```
|
||||||
|
|
@ -55,7 +55,7 @@ cp .env.example .env
|
||||||
# THRESHOLD=0.01
|
# THRESHOLD=0.01
|
||||||
|
|
||||||
# Start services
|
# Start services
|
||||||
docker compose up -d
|
docker compose up -d --build
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
docker compose logs -f bot
|
docker compose logs -f bot
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
services:
|
services:
|
||||||
bot:
|
bot:
|
||||||
build: .
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
|
- PORT=3000
|
||||||
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
- PAIRS=${PAIRS:-BTC-USD}
|
- PAIRS=${PAIRS:-BTC-USD}
|
||||||
- INTERVAL=${INTERVAL:-5000}
|
- INTERVAL=${INTERVAL:-5000}
|
||||||
|
|
@ -21,7 +24,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U uphold -d uphold_alerts"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-uphold} -d ${POSTGRES_DB:-uphold_alerts}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
|
||||||
2
index.js
2
index.js
|
|
@ -4,6 +4,7 @@ import { prefetchRates } from "./src/api.js";
|
||||||
import { Bot } from "./src/bot.js";
|
import { Bot } from "./src/bot.js";
|
||||||
import logger from "./src/logger.js";
|
import logger from "./src/logger.js";
|
||||||
import { initDB, insertIntoDB, closePool } from "./src/db.js";
|
import { initDB, insertIntoDB, closePool } from "./src/db.js";
|
||||||
|
import { startServer } from "./src/server.js";
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
process.on("uncaughtException", (err) => {
|
||||||
logger.fatal(err, "Uncaught exception. Application crashing...");
|
logger.fatal(err, "Uncaught exception. Application crashing...");
|
||||||
|
|
@ -69,6 +70,7 @@ async function main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await initDB();
|
await initDB();
|
||||||
|
startServer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prefetchRates(pairs);
|
await prefetchRates(pairs);
|
||||||
|
|
|
||||||
14
src/api.js
14
src/api.js
|
|
@ -1,6 +1,7 @@
|
||||||
import { BigNumber } from "bignumber.js";
|
import { BigNumber } from "bignumber.js";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import logger from "./logger.js";
|
import logger from "./logger.js";
|
||||||
|
import { incrementReq } from "./stats.js";
|
||||||
|
|
||||||
const queue = new PQueue({ interval: 1000, intervalCap: 10 });
|
const queue = new PQueue({ interval: 1000, intervalCap: 10 });
|
||||||
|
|
||||||
|
|
@ -19,8 +20,17 @@ export async function fetchRate(pair) {
|
||||||
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
if (res.status === 429) throw new Error("Rate limited");
|
if (res.status === 429) {
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
incrementReq("ratelimit");
|
||||||
|
throw new Error("Rate limited");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
incrementReq("fail");
|
||||||
|
throw new Error(`HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementReq("success");
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { fetchRate } from "./api.js";
|
import { fetchRate } from "./api.js";
|
||||||
import logger from "./logger.js";
|
import logger from "./logger.js";
|
||||||
|
import { updatePair, incrementAlerts } from "./stats.js";
|
||||||
|
|
||||||
export class Bot {
|
export class Bot {
|
||||||
constructor(config, onAlert) {
|
constructor(config, onAlert) {
|
||||||
|
|
@ -32,6 +33,8 @@ export class Bot {
|
||||||
try {
|
try {
|
||||||
const price = await fetchRate(this.pair);
|
const price = await fetchRate(this.pair);
|
||||||
|
|
||||||
|
updatePair(this.pair, price.toFixed(6));
|
||||||
|
|
||||||
if (!this.lastPrice) {
|
if (!this.lastPrice) {
|
||||||
this.lastPrice = price;
|
this.lastPrice = price;
|
||||||
logger.info(`[${this.pair}] Initial price: ${price.toFixed(2)}`);
|
logger.info(`[${this.pair}] Initial price: ${price.toFixed(2)}`);
|
||||||
|
|
@ -42,6 +45,7 @@ export class Bot {
|
||||||
.multipliedBy(100);
|
.multipliedBy(100);
|
||||||
|
|
||||||
if (change.abs().gte(this.threshold)) {
|
if (change.abs().gte(this.threshold)) {
|
||||||
|
incrementAlerts();
|
||||||
const dir = change.gt(0) ? "UP" : "DOWN";
|
const dir = change.gt(0) ? "UP" : "DOWN";
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,14 @@ export async function insertIntoDB(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRecentAlerts(limit = 50) {
|
||||||
|
const res = await pool.query(
|
||||||
|
"SELECT * FROM alerts ORDER BY created_at DESC LIMIT $1",
|
||||||
|
[limit],
|
||||||
|
);
|
||||||
|
return res.rows;
|
||||||
|
}
|
||||||
|
|
||||||
export async function closePool() {
|
export async function closePool() {
|
||||||
if (pool) {
|
if (pool) {
|
||||||
await pool.end();
|
await pool.end();
|
||||||
|
|
|
||||||
46
src/server.js
Normal file
46
src/server.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import http from "node:http";
|
||||||
|
import { URL } from "node:url";
|
||||||
|
import { stats } from "./stats.js";
|
||||||
|
import { getRecentAlerts } from "./db.js";
|
||||||
|
import logger from "./logger.js";
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
const sendJSON = (res, data, status = 200) => {
|
||||||
|
res.writeHead(status, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify(data, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
const { pathname, searchParams } = parsedUrl;
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/health") {
|
||||||
|
return sendJSON(res, { status: "ok", uptime: process.uptime() });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/stats") {
|
||||||
|
return sendJSON(res, stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/alerts") {
|
||||||
|
const limit = parseInt(searchParams.get("limit")) || 50;
|
||||||
|
const alerts = await getRecentAlerts(limit);
|
||||||
|
return sendJSON(res, { count: alerts.length, data: alerts });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 404
|
||||||
|
sendJSON(res, { error: "Not Found" }, 404);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "API Error");
|
||||||
|
sendJSON(res, { error: "Internal Server Error" }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const startServer = () => {
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
logger.info(`[API] Server listening on port ${PORT}`);
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
};
|
||||||
31
src/stats.js
Normal file
31
src/stats.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
export const stats = {
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
api: {
|
||||||
|
totalRequests: 0,
|
||||||
|
successful: 0,
|
||||||
|
failed: 0,
|
||||||
|
rateLimited: 0,
|
||||||
|
},
|
||||||
|
bot: {
|
||||||
|
totalAlerts: 0,
|
||||||
|
activePairs: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const incrementReq = (type) => {
|
||||||
|
stats.api.totalRequests++;
|
||||||
|
if (type === "success") stats.api.successful++;
|
||||||
|
if (type === "fail") stats.api.failed++;
|
||||||
|
if (type === "ratelimit") stats.api.rateLimited++;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePair = (pair, price) => {
|
||||||
|
stats.bot.activePairs[pair] = {
|
||||||
|
currentPrice: price,
|
||||||
|
lastUpdate: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const incrementAlerts = () => {
|
||||||
|
stats.bot.totalAlerts++;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue