eww: Added config
Currently 1 widget: - freqtrade
This commit is contained in:
parent
230c47b9f7
commit
838f6939cd
5 changed files with 297 additions and 0 deletions
1
bar/eww/.gitignore
vendored
Normal file
1
bar/eww/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.env
|
30
bar/eww/eww.scss
Normal file
30
bar/eww/eww.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
* {
|
||||
all: unset;
|
||||
font-family: "FiraCode Nerd Font";
|
||||
}
|
||||
|
||||
.trades-box {
|
||||
margin: 12px;
|
||||
padding: 8px;
|
||||
background-color: #121212;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.profit {
|
||||
background-color: #121212;
|
||||
border: solid 4px;
|
||||
border-color: #12bb7b;
|
||||
border-radius: 5px;
|
||||
|
||||
padding: 3px;
|
||||
margin-left: 8px;
|
||||
|
||||
&.loss {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-left: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
59
bar/eww/eww.yuck
Normal file
59
bar/eww/eww.yuck
Normal file
|
@ -0,0 +1,59 @@
|
|||
(defpoll bots
|
||||
:initial `[]`
|
||||
:interval "3s"
|
||||
`curl http://localhost:42667/list`
|
||||
)
|
||||
|
||||
(defpoll absolute_profit
|
||||
:interval "1s"
|
||||
:initial 0
|
||||
`echo $((RANDOM % 11))`
|
||||
)
|
||||
|
||||
(defwidget profit-box [percent price]
|
||||
(box :class {price >= 0 ? "profit" : "profit loss"} :space-evenly false
|
||||
(label :text {price >= 0 ? "▲" : "▼"} :class "arrow" :halign "start")
|
||||
(label :text "${round(percent, 2)}% (${round(price, 2)})" :class "price" :justify "center" :hexpand true)
|
||||
)
|
||||
)
|
||||
|
||||
(defwidget trades-box []
|
||||
(box :orientation "v" :class "trades-box"
|
||||
(box :orientation "h" :style "margin: 2px; font-weight: bold"
|
||||
(label :text "Freqtrade" :style "font-family: Arial; font-size: 24px")
|
||||
(label :text "Open profit")
|
||||
(label :text "Closed profit")
|
||||
)
|
||||
(for bot in bots
|
||||
(box :orientation "h" :style "margin: 2px"
|
||||
(label :text "${bot.name}" :halign "start" :style "font-weight: bold; margin-right: 8px")
|
||||
(profit-box :percent {bot.open_profit_pct} :price {bot.open_profit})
|
||||
(profit-box :percent {bot.closed_profit_pct} :price {bot.closed_profit})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defwidget profits-graph []
|
||||
(box :class "trades-box"
|
||||
(graph
|
||||
:value {arraylength(bots) > 1 ? -bots[2].open_profit_pct * 2.0 : 0}
|
||||
:time-range "20m"
|
||||
:thickness 2
|
||||
:line-style "round"
|
||||
:dynamic true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defwindow freqtrade
|
||||
:monitor 0
|
||||
:stacking "bg"
|
||||
:geometry (geometry :x "0%"
|
||||
:anchor "top right"
|
||||
)
|
||||
(box :orientation "v"
|
||||
(trades-box)
|
||||
(profits-graph)
|
||||
)
|
||||
)
|
206
bar/eww/scripts/freqtraded.ts
Executable file
206
bar/eww/scripts/freqtraded.ts
Executable file
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env bun
|
||||
|
||||
import http from "http";
|
||||
|
||||
const API_ADDRESS = "127.0.0.1";
|
||||
const API_PORT = 42667;
|
||||
|
||||
type LoginResponse = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
type BotData = {
|
||||
url: string;
|
||||
auth: LoginResponse;
|
||||
summary: BotSummary;
|
||||
};
|
||||
|
||||
type BotSummary = {
|
||||
name: string;
|
||||
dry_run: boolean;
|
||||
closed_profit: number;
|
||||
closed_profit_pct: number;
|
||||
open_profit: number;
|
||||
open_profit_pct: number;
|
||||
};
|
||||
|
||||
let bots: BotData[] = [];
|
||||
|
||||
const URLS = JSON.parse(process.env.FREQTRADE_URLS);
|
||||
|
||||
async function login(
|
||||
url: string,
|
||||
user: string,
|
||||
pass: string,
|
||||
): Promise<LoginResponse> {
|
||||
const response = await fetch(`${url}/api/v1/token/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`${user}:${pass}`),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw Error("Failed to login");
|
||||
}
|
||||
|
||||
return (await response.json()) as LoginResponse;
|
||||
}
|
||||
|
||||
async function refresh_token(bot: BotData) {
|
||||
const response = await fetch(`${bot.url}/api/v1/token/refresh`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer " + bot.auth.refresh_token,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw Error("Failed to login");
|
||||
}
|
||||
|
||||
bot.auth.access_token = (await response.json()).access_token;
|
||||
}
|
||||
|
||||
async function get_summary(
|
||||
url: string,
|
||||
auth: LoginResponse,
|
||||
): Promise<BotSummary> {
|
||||
// Fetch config
|
||||
const config_response = await fetch(`${url}/api/v1/show_config`, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + auth.access_token,
|
||||
},
|
||||
});
|
||||
|
||||
if (config_response.status !== 200) {
|
||||
throw Error("Failed to get bot config");
|
||||
}
|
||||
|
||||
const config = await config_response.json();
|
||||
|
||||
// Fetch trades
|
||||
const trades_response = await fetch(`${url}/api/v1/status`, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + auth.access_token,
|
||||
},
|
||||
});
|
||||
|
||||
if (trades_response.status !== 200) {
|
||||
throw Error("Failed to get bot config");
|
||||
}
|
||||
|
||||
const trades = await trades_response.json();
|
||||
|
||||
// Fetch balance
|
||||
const profit_response = await fetch(`${url}/api/v1/profit`, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + auth.access_token,
|
||||
},
|
||||
});
|
||||
|
||||
if (profit_response.status !== 200) {
|
||||
throw Error("Failed to get bot config");
|
||||
}
|
||||
|
||||
const profit = await profit_response.json();
|
||||
|
||||
return {
|
||||
name: config.bot_name as string,
|
||||
dry_run: config.dry_run as boolean,
|
||||
open_profit: trades.length
|
||||
? trades.map((v) => v.profit_abs).reduce((a, b) => a + b)
|
||||
: 0,
|
||||
open_profit_pct: trades.length
|
||||
? trades.map((v) => v.profit_pct).reduce((a, b) => a + b) / trades.length
|
||||
: 0,
|
||||
closed_profit: profit.profit_closed_coin,
|
||||
closed_profit_pct: profit.profit_closed_percent,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestListener(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
) {
|
||||
switch (req.url) {
|
||||
case "/":
|
||||
res.writeHead(200);
|
||||
res.end("Welcome to the freqtrade API");
|
||||
break;
|
||||
case "/list":
|
||||
// Update bots
|
||||
await Promise.all(
|
||||
bots.map(async (bot) => {
|
||||
bot.summary = await get_summary(bot.url, bot.auth);
|
||||
}),
|
||||
);
|
||||
|
||||
res.writeHead(200);
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify(bots.map((b) => b.summary)));
|
||||
break;
|
||||
default:
|
||||
let bot_name = req.url?.split("/")[1];
|
||||
|
||||
let bot = bots.find((v) => v.summary.name == bot_name);
|
||||
|
||||
if (bot) {
|
||||
let response = await fetch(
|
||||
`${bot.url}${req.url?.replace(`/${bot_name}`, "")}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer " + bot.auth.access_token,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
res.writeHead(response.status);
|
||||
res.end(await response.text());
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end(JSON.stringify({ error: "Resource not found" }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function daemon() {
|
||||
for (let url of URLS) {
|
||||
console.info("[INFO] Logging in for " + url);
|
||||
let auth = await login(
|
||||
url,
|
||||
process.env.FREQTRADE_USER!,
|
||||
process.env.FREQTRADE_PASS!,
|
||||
);
|
||||
|
||||
let bot: BotData = {
|
||||
url,
|
||||
auth,
|
||||
summary: await get_summary(url, auth),
|
||||
};
|
||||
|
||||
setInterval(
|
||||
async () => {
|
||||
console.info("[INFO] Refreshing token for " + url);
|
||||
await refresh_token(bot);
|
||||
},
|
||||
10 * (1 - (Math.random() - 0.5) / 4) * 60 * 1000,
|
||||
);
|
||||
|
||||
bots.push(bot);
|
||||
}
|
||||
|
||||
console.info("[INFO] Starting Web Server");
|
||||
|
||||
const server = http.createServer(requestListener);
|
||||
server.listen(API_PORT, API_ADDRESS, () => {
|
||||
console.info(
|
||||
`[INFO] Web server running on http://${API_ADDRESS}:${API_PORT}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
await daemon();
|
1
sync
1
sync
|
@ -24,6 +24,7 @@ synced_files = [
|
|||
("term/alacritty/", "~/.config/alacritty/"),
|
||||
("bar/waybar/", "~/.config/waybar/"),
|
||||
("bar/i3status-rust/", "~/.config/i3status-rust/"),
|
||||
("bar/eww/", "~/.config/eww/"),
|
||||
("home/xinitrc", "~/.xinitrc"),
|
||||
("misc/picom/", "~/.config/picom/"),
|
||||
("misc/runst/", "~/.config/runst/"),
|
||||
|
|
Loading…
Reference in a new issue