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
|
||||
|
||||
**Basic:**
|
||||
**With default parameters:**
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
|
|
@ -55,7 +55,7 @@ cp .env.example .env
|
|||
# THRESHOLD=0.01
|
||||
|
||||
# Start services
|
||||
docker compose up -d
|
||||
docker compose up -d --build
|
||||
|
||||
# View logs
|
||||
docker compose logs -f bot
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
services:
|
||||
bot:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
- PAIRS=${PAIRS:-BTC-USD}
|
||||
- INTERVAL=${INTERVAL:-5000}
|
||||
|
|
@ -21,7 +24,7 @@ services:
|
|||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
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
|
||||
timeout: 5s
|
||||
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 logger from "./src/logger.js";
|
||||
import { initDB, insertIntoDB, closePool } from "./src/db.js";
|
||||
import { startServer } from "./src/server.js";
|
||||
|
||||
process.on("uncaughtException", (err) => {
|
||||
logger.fatal(err, "Uncaught exception. Application crashing...");
|
||||
|
|
@ -69,6 +70,7 @@ async function main() {
|
|||
);
|
||||
|
||||
await initDB();
|
||||
startServer();
|
||||
|
||||
try {
|
||||
await prefetchRates(pairs);
|
||||
|
|
|
|||
14
src/api.js
14
src/api.js
|
|
@ -1,6 +1,7 @@
|
|||
import { BigNumber } from "bignumber.js";
|
||||
import PQueue from "p-queue";
|
||||
import logger from "./logger.js";
|
||||
import { incrementReq } from "./stats.js";
|
||||
|
||||
const queue = new PQueue({ interval: 1000, intervalCap: 10 });
|
||||
|
||||
|
|
@ -19,8 +20,17 @@ export async function fetchRate(pair) {
|
|||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (res.status === 429) throw new Error("Rate limited");
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
if (res.status === 429) {
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { fetchRate } from "./api.js";
|
||||
import logger from "./logger.js";
|
||||
import { updatePair, incrementAlerts } from "./stats.js";
|
||||
|
||||
export class Bot {
|
||||
constructor(config, onAlert) {
|
||||
|
|
@ -32,6 +33,8 @@ export class Bot {
|
|||
try {
|
||||
const price = await fetchRate(this.pair);
|
||||
|
||||
updatePair(this.pair, price.toFixed(6));
|
||||
|
||||
if (!this.lastPrice) {
|
||||
this.lastPrice = price;
|
||||
logger.info(`[${this.pair}] Initial price: ${price.toFixed(2)}`);
|
||||
|
|
@ -42,6 +45,7 @@ export class Bot {
|
|||
.multipliedBy(100);
|
||||
|
||||
if (change.abs().gte(this.threshold)) {
|
||||
incrementAlerts();
|
||||
const dir = change.gt(0) ? "UP" : "DOWN";
|
||||
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() {
|
||||
if (pool) {
|
||||
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