eww: Added config

Currently 1 widget:
 - freqtrade
This commit is contained in:
GaspardCulis 2024-01-03 15:09:10 +01:00
parent 230c47b9f7
commit 838f6939cd
5 changed files with 297 additions and 0 deletions

1
bar/eww/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

30
bar/eww/eww.scss Normal file
View 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
View 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
View 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
View file

@ -24,6 +24,7 @@ synced_files = [
("term/alacritty/", "~/.config/alacritty/"), ("term/alacritty/", "~/.config/alacritty/"),
("bar/waybar/", "~/.config/waybar/"), ("bar/waybar/", "~/.config/waybar/"),
("bar/i3status-rust/", "~/.config/i3status-rust/"), ("bar/i3status-rust/", "~/.config/i3status-rust/"),
("bar/eww/", "~/.config/eww/"),
("home/xinitrc", "~/.xinitrc"), ("home/xinitrc", "~/.xinitrc"),
("misc/picom/", "~/.config/picom/"), ("misc/picom/", "~/.config/picom/"),
("misc/runst/", "~/.config/runst/"), ("misc/runst/", "~/.config/runst/"),