Added classes from old Cauldron (NON TESTED / BROKEN!)

This commit is contained in:
2026-06-16 19:59:48 +02:00
parent b8bf70a178
commit 5b62bd8d12
100 changed files with 6910 additions and 2 deletions
+15 -2
View File
@@ -12,6 +12,7 @@ if (version == "UNSET" && gradle.startParameter.taskNames.any { it.startsWith("p
application { application {
mainClass.set("com.r35157.nenjim.hubd.impl.ref.Main") mainClass.set("com.r35157.nenjim.hubd.impl.ref.Main")
applicationDefaultJvmArgs = listOf("--enable-preview")
} }
repositories { repositories {
@@ -43,9 +44,13 @@ dependencies {
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0") runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0") runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
implementation("org.slf4j:slf4j-api:2.0.18")
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
implementation("com.fazecast:jSerialComm:2.11.4") implementation("com.fazecast:jSerialComm:2.11.4")
implementation("com.google.code.gson:gson:2.14.0")
implementation("commons-codec:commons-codec:1.22.0")
implementation("org.apache.commons:commons-collections4:4.5.0")
implementation("org.apache.commons:commons-lang3:3.20.0")
implementation("org.slf4j:slf4j-api:2.0.18")
} }
java { java {
@@ -54,7 +59,15 @@ java {
tasks.withType<JavaCompile>().configureEach { tasks.withType<JavaCompile>().configureEach {
options.release.set(25) options.release.set(25)
options.compilerArgs.addAll(listOf("-Xmaxerrs", "1"))
options.compilerArgs.addAll(
listOf(
"--enable-preview",
//"-Xlint:deprecation",
//"-Xlint:unchecked",
"-Xmaxerrs", "1"
)
)
} }
val generatedDetagMain = layout.buildDirectory.dir("generated/sources/detag/main/java") val generatedDetagMain = layout.buildDirectory.dir("generated/sources/detag/main/java")
+1
View File
@@ -17,6 +17,7 @@ DEBUG_SUSPEND="${DEBUG_SUSPEND:-y}"
echo "Starting NenjimHub in DEBUG mode on port ${DEBUG_PORT} (suspend=${DEBUG_SUSPEND})" echo "Starting NenjimHub in DEBUG mode on port ${DEBUG_PORT} (suspend=${DEBUG_SUSPEND})"
exec java \ exec java \
--enable-preview \
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=*:${DEBUG_PORT}" \ "-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=*:${DEBUG_PORT}" \
-Dlog4j.configurationFile=conf/log4j2.xml \ -Dlog4j.configurationFile=conf/log4j2.xml \
-cp "$CLASSPATH" \ -cp "$CLASSPATH" \
+1
View File
@@ -12,6 +12,7 @@ fi
CLASSPATH=$(IFS=:; echo "${jars[*]}") CLASSPATH=$(IFS=:; echo "${jars[*]}")
exec java \ exec java \
--enable-preview \
-Dlog4j.configurationFile=conf/log4j2.xml \ -Dlog4j.configurationFile=conf/log4j2.xml \
-cp "$CLASSPATH" \ -cp "$CLASSPATH" \
com.r35157.nenjim.hubd.impl.ref.Main com.r35157.nenjim.hubd.impl.ref.Main
@@ -0,0 +1,35 @@
package crypto.r35157;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public class RingBuffer {
public RingBuffer(int capacity) {
this.capacity = capacity;
this.buffer = new ArrayDeque<>(capacity);
}
public void add(String command) {
if (getSize() == capacity) {
buffer.removeFirst();
}
buffer.addLast(command);
}
public List<String> getAll() {
return new ArrayList<>(buffer);
}
public int getSize() {
return buffer.size();
}
public int getCapacity() {
return capacity;
}
private final int capacity;
private final Deque<String> buffer;
}
+13
View File
@@ -0,0 +1,13 @@
package crypto.r35157;
public class SemVer {
public SemVer(String semVer) {
this.semVer = semVer;
}
public String getSemVer() {
return semVer;
}
private String semVer;
}
@@ -0,0 +1,322 @@
package crypto.r35157;
import java.time.Duration;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.List;
public class TimeTools {
public static String formatLaterTime(long millis) {
LocalTime now = LocalTime.now();
Duration duration = Duration.ofMillis(millis);
LocalTime futureTime = now.plus(duration);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
String timeStr = futureTime.format(formatter);
return timeStr;
}
/**
* Converts a time string into milliseconds.
* Supported units:
* y = year (average Gregorian year = 365.2425 days)
* M = month (average Gregorian month = 30.436875 days)
* w = week (7 days)
* d = day
* h = hour
* m = minute
* s = second
*
* Note: The conversion for years and months is approximate.
*
* @param timeString the input time string (e.g., "1w3d" or "1h40m")
* @return the total time in milliseconds
* @throws IllegalArgumentException if an unsupported unit is encountered
*/
public static long parseTimeToMillis(String timeString) {
long millis = 0L;
// Regular expression to match sequences like "1y", "3d", "40m", etc.
Pattern pattern = Pattern.compile("(\\d+)([yMwdhms])");
Matcher matcher = pattern.matcher(timeString);
// Base conversion factors:
final long millisPerSecond = 1000L;
final long millisPerMinute = millisPerSecond * 60;
final long millisPerHour = millisPerMinute * 60;
final long millisPerDay = millisPerHour * 24;
final long millisPerWeek = millisPerDay * 7;
// Average values for Gregorian calendar
final double millisPerYear = 365.2425 * millisPerDay;
final double millisPerMonth = millisPerYear / 12;
while (matcher.find()) {
long value = Long.parseLong(matcher.group(1));
String unit = matcher.group(2);
switch(unit) {
case "y":
millis += (long)(value * millisPerYear);
break;
case "M":
millis += (long)(value * millisPerMonth);
break;
case "w":
millis += value * millisPerWeek;
break;
case "d":
millis += value * millisPerDay;
break;
case "h":
millis += value * millisPerHour;
break;
case "m":
millis += value * millisPerMinute;
break;
case "s":
millis += value * millisPerSecond;
break;
default:
throw new IllegalArgumentException("Invalid time unit: " + unit);
}
}
return millis;
}
/**
* Formats a duration (in milliseconds) using up to maxElements most-significant parts.
* Rounding is applied to the last displayed element. If rounding occurs or any lower parts
* are dropped, the output is prefixed with "≈".
*
* Examples:
* formatTimeFromMillis(691200000L, 1) returns "8 days"
* formatTimeFromMillis(691200001L, 1) returns "≈8 days"
* formatTimeFromMillis(691200001L, 2) returns "8 days and 1 millisecond"
* formatTimeFromMillis(183856012L) returns "2 days, 3 hours, 4 minutes, 16 seconds, 12 milliseconds"
* formatTimeFromMillis(183856012L, 1) returns "≈2 days"
* formatTimeFromMillis(183856012L, 3) returns "≈2 days, 3 hours, 4 minutes"
* formatTimeFromMillis(285000L, 2) returns "4 minutes and 45 seconds"
* formatTimeFromMillis(285000L, 1) returns "≈5 minutes"
*
* Years and months are included if the duration is long enough.
*
* @param totalMs the total duration in milliseconds.
* @param maxElements maximum number of time parts to show (must be >= 1)
* @return the formatted time string.
*/
public static String formatTimeFromMillis(long totalMs, int maxElements) {
if (maxElements < 1) {
throw new IllegalArgumentException("maxElements must be at least 1");
}
// Compute each unit in order.
long years = (long)(totalMs / MILLIS_PER_YEAR);
long remainder = totalMs - (long)(years * MILLIS_PER_YEAR);
long months = (long)(remainder / MILLIS_PER_MONTH);
remainder -= (long)(months * MILLIS_PER_MONTH);
long days = remainder / MILLIS_PER_DAY;
remainder %= MILLIS_PER_DAY;
long hours = remainder / MILLIS_PER_HOUR;
remainder %= MILLIS_PER_HOUR;
long minutes = remainder / MILLIS_PER_MINUTE;
remainder %= MILLIS_PER_MINUTE;
long seconds = remainder / MILLIS_PER_SECOND;
long milliseconds = remainder % MILLIS_PER_SECOND;
// Breakdown values in order: years, months, days, hours, minutes, seconds, milliseconds.
long[] values = new long[] { years, months, days, hours, minutes, seconds, milliseconds };
double[] factors = new double[] { MILLIS_PER_YEAR, MILLIS_PER_MONTH, MILLIS_PER_DAY,
MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, 1 };
// Determine the first nonzero unit (if all zero, use the smallest unit).
int startIndex = 0;
while (startIndex < NUM_UNITS && values[startIndex] == 0) {
startIndex++;
}
if (startIndex == NUM_UNITS) {
startIndex = NUM_UNITS - 1;
}
// Build a list of nonzero components starting from startIndex.
List<Component> components = new ArrayList<>();
for (int i = startIndex; i < NUM_UNITS; i++) {
if (values[i] != 0) {
components.add(new Component(i, values[i]));
}
}
// Ensure at least one component is present.
if (components.isEmpty()) {
components.add(new Component(NUM_UNITS - 1, 0));
}
// If the caller wants to show all components (or more than available), join them exactly.
if (maxElements >= components.size()) {
return joinComponents(components, false);
}
// Otherwise, show (maxElements - 1) components exactly, and the last one rounded.
List<Component> resultComponents = new ArrayList<>();
for (int i = 0; i < maxElements - 1; i++) {
resultComponents.add(components.get(i));
}
int lastUnitIndex = components.get(maxElements - 1).unitIndex;
// Sum the contribution (in ms) of all units more significant than the last displayed unit.
double msAccounted = 0;
for (int i = startIndex; i < lastUnitIndex; i++) {
msAccounted += values[i] * factors[i];
}
// Compute the remaining ms for the last unit.
double valueExact = (totalMs - msAccounted) / factors[lastUnitIndex];
long roundedValue = Math.round(valueExact);
// Mark approximation if any lower parts were dropped or if rounding changed the exact value.
boolean approx = (components.size() > maxElements) || (Math.abs(valueExact - Math.floor(valueExact)) > 1e-9);
resultComponents.add(new Component(lastUnitIndex, roundedValue));
return joinComponents(resultComponents, approx);
}
/**
* Overload that returns the full breakdown (all nonzero parts).
*/
public static String formatTimeFromMillis(long totalMs) {
return formatTimeFromMillis(totalMs, NUM_UNITS);
}
/**
* Overloaded method that converts the entire duration into a single unit (as specified by the enum).
* For example:
* formatTimeFromMillis(400L * MILLIS_PER_DAY, TimeUnit.HOURS) returns "9600 hours"
* formatTimeFromMillis(400L * MILLIS_PER_DAY - 1, TimeUnit.HOURS) returns "≈9600 hours"
*
* @param totalMs the total duration in milliseconds.
* @param unit the target time unit.
* @return the formatted string representing the duration in the given unit.
*/
public static String formatTimeFromMillis(long totalMs, TimeUnit unit) {
double factor;
switch(unit) {
case YEARS:
factor = MILLIS_PER_YEAR;
break;
case MONTHS:
factor = MILLIS_PER_MONTH;
break;
case DAYS:
factor = MILLIS_PER_DAY;
break;
case HOURS:
factor = MILLIS_PER_HOUR;
break;
case MINUTES:
factor = MILLIS_PER_MINUTE;
break;
case SECONDS:
factor = MILLIS_PER_SECOND;
break;
case MILLISECONDS:
factor = 1;
break;
default:
throw new IllegalArgumentException("Unsupported time unit: " + unit);
}
double exactValue = totalMs / factor;
long roundedValue = Math.round(exactValue);
boolean approx = (Math.abs(exactValue - roundedValue) > 1e-9);
return (approx ? "≈" : "") + unit.format(roundedValue);
}
// Joins the list of components into a single string.
// If approx is true, the string is prefixed with "≈".
private static String joinComponents(List<Component> comps, boolean approx) {
List<String> parts = new ArrayList<>();
for (Component comp : comps) {
parts.add(comp.value + " " + unitName(comp.unitIndex, comp.value));
}
String result;
if (parts.size() == 1) {
result = parts.get(0);
} else if (parts.size() == 2) {
result = parts.get(0) + " and " + parts.get(1);
} else {
result = String.join(", ", parts);
}
return approx ? "≈" + result : result;
}
// Returns the proper singular or plural name for each unit based on its index.
private static String unitName(int unitIndex, long value) {
String result = switch (unitIndex) {
case 0 -> value == 1 ? "year" : "years";
case 1 -> value == 1 ? "month" : "months";
case 2 -> value == 1 ? "day" : "days";
case 3 -> value == 1 ? "hour" : "hours";
case 4 -> value == 1 ? "minute" : "minutes";
case 5 -> value == 1 ? "second" : "seconds";
case 6 -> value == 1 ? "millisecond" : "milliseconds";
default -> "";
};
return result;
}
private static class Component {
int unitIndex;
long value;
Component(int unitIndex, long value) {
this.unitIndex = unitIndex;
this.value = value;
}
}
/**
* Enum representing time units with singular and plural names.
*/
enum TimeUnit {
YEARS("year", "years"),
MONTHS("month", "months"),
DAYS("day", "days"),
HOURS("hour", "hours"),
MINUTES("minute", "minutes"),
SECONDS("second", "seconds"),
MILLISECONDS("millisecond", "milliseconds");
private final String singular;
private final String plural;
TimeUnit(String singular, String plural) {
this.singular = singular;
this.plural = plural;
}
/**
* Formats the value with the appropriate singular or plural name.
* @param value the numeric value
* @return a formatted string, e.g. "1 day" or "5 days"
*/
public String format(long value) {
return value + " " + (value == 1 ? singular : plural);
}
}
// Base units (using long for fixed units).
private static final long MILLIS_PER_SECOND = 1000L;
private static final long MILLIS_PER_MINUTE = MILLIS_PER_SECOND * 60;
private static final long MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60;
private static final long MILLIS_PER_DAY = MILLIS_PER_HOUR * 24;
// Approximate values for months and years (as doubles).
private static final double MILLIS_PER_YEAR = 365.2425 * MILLIS_PER_DAY;
private static final double MILLIS_PER_MONTH = MILLIS_PER_YEAR / 12;
// The order of units from most-significant to least:
// 0: years, 1: months, 2: days, 3: hours, 4: minutes, 5: seconds, 6: milliseconds.
private static final int NUM_UNITS = 7;
}
@@ -0,0 +1,27 @@
package crypto.r35157.assetaz.hub;
import crypto.r35157.nenjim.NenjimContext;
import crypto.r35157.nenjim.NenjimHub;
import crypto.r35157.nenjim.NenjimProcess;
public class AssetAZHub implements NenjimProcess {
@Override
public void run() throws Exception {
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub hub) {
}
@Override
public String getProcessName() {
return "AssetAZ Hub";
}
private NenjimContext nenjimContext;
}
@@ -0,0 +1,23 @@
package crypto.r35157.cauldron;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class AssetAZ extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(AssetAZ.class.getResource("Structure.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 1200, 980);
stage.setTitle("AssetAZ");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
@@ -0,0 +1,27 @@
package crypto.r35157.cauldron;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeCell;
public class CheckBoxTreeCell extends TreeCell<String> {
private CheckBox checkBox;
public CheckBoxTreeCell() {
checkBox = new CheckBox();
checkBox.setDisable(true); // Disable the checkbox so it only changes when the item is selected
setGraphic(checkBox);
}
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item);
setGraphic(checkBox);
}
}
}
@@ -0,0 +1,33 @@
package crypto.r35157.cauldron;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
public class FormatterUtils {
public static String formatPercentage(double val) {
DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.getDefault());
symbols.setGroupingSeparator('.');
symbols.setDecimalSeparator(',');
// Create a DecimalFormat object with a pattern
DecimalFormat df = new DecimalFormat("#,##0.00", symbols);
// Format the double value
String formattedValue = df.format(val * 100) + "%";
return formattedValue;
}
public static String formatAmount(double val) {
DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.getDefault());
symbols.setGroupingSeparator('.');
symbols.setDecimalSeparator(',');
// Create a DecimalFormat object with a pattern
DecimalFormat df = new DecimalFormat("#,##0.00", symbols);
// Format the double value
String formattedValue = df.format(val);
return formattedValue;
}
}
@@ -0,0 +1,314 @@
package crypto.r35157.cauldron;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.StackedAreaChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.util.Callback;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static crypto.r35157.cauldron.FormatterUtils.formatAmount;
import static crypto.r35157.cauldron.FormatterUtils.formatPercentage;
import static crypto.r35157.cauldron.Ticker.getTickerItems;
import static crypto.r35157.cauldron.TimeUtils.ONE_DAY;
import static crypto.r35157.cauldron.TimeUtils.formatter_hhmm;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import static java.util.concurrent.TimeUnit.MINUTES;
public class NetworthChartController {
private void startChartUpdater() {
ScheduledExecutorService executorService = newSingleThreadScheduledExecutor();
Runnable task = this::updateUI;
executorService.scheduleAtFixedRate(task, 0, POLL_INTERVAL, POLL_TIMEUNIT);
}
/* This synchronized as both the scheduler and manual UI changes can result in the UI gets updated */
private synchronized void updateUI() {
long currentTimeStamp = System.currentTimeMillis();
List<TickerItem> tickerItems_solana;
List<TickerItem> tickerItems_ethereum;
List<TickerItem> tickerItems_bitcoincash;
List<TickerItem> tickerItems_monero;
List<TickerItem> tickerItems_usdc;
List<TickerItem> tickerItems_bitcoin;
try {
tickerItems_solana = getTickerItems(ASSETID_SOLANA, currentTimeStamp - displayPeriod, currentTimeStamp);
tickerItems_ethereum = getTickerItems(ASSETID_ETHEREUM, currentTimeStamp - displayPeriod, currentTimeStamp);
tickerItems_bitcoincash = getTickerItems(ASSETID_BITCOINCASH, currentTimeStamp - displayPeriod, currentTimeStamp);
tickerItems_monero = getTickerItems(ASSETID_MONERO, currentTimeStamp - displayPeriod, currentTimeStamp);
tickerItems_usdc = getTickerItems(ASSETID_USDC, currentTimeStamp - displayPeriod, currentTimeStamp);
tickerItems_bitcoin = getTickerItems(ASSETID_BITCOIN, currentTimeStamp - displayPeriod, currentTimeStamp);
} catch(Exception e) {
e.printStackTrace();
tickerItems_solana = Collections.emptyList();
tickerItems_ethereum = Collections.emptyList();
tickerItems_bitcoincash = Collections.emptyList();
tickerItems_monero = Collections.emptyList();
tickerItems_usdc = Collections.emptyList();
tickerItems_bitcoin = Collections.emptyList();
}
if (!tickerItems_solana.isEmpty()) {
double latestSOLPrice = getLastPrice(tickerItems_solana);
// double latestETHPrice = getLastPrice(tickerItems_ethereum);
// double latestBCHPrice = getLastPrice(tickerItems_bitcoincash);
// double latestXMRPrice = getLastPrice(tickerItems_monero);
// double latestUSDCPrice = getLastPrice(tickerItems_usdc);
// double latestBTCPrice = getLastPrice(tickerItems_bitcoin);
// Number price_sol = prices.get("solana");
// Number price_btc = prices.get("bitcoin");
// Number price_ethereum = prices.get("ethereum");
// Number price_bitcoincash = prices.get("bitcoin-cash");
// Number price_usdollarcoin = prices.get("usd-coin");
// Number price_monero = prices.get("monero");
// Number price_fiatdkk = 1.0d;
//Number value_skyld_maria = balance_liability_maria.doubleValue() * price_btc.doubleValue();
//Number value_skyld_emil = balance_liability_emil.doubleValue() * price_monero.doubleValue();
//Number value_skyld_sofie = balance_liability_sofie.doubleValue() * price_monero.doubleValue();
Number value_sol = balance_solana.doubleValue() * latestSOLPrice;
//Number value_btc = balance_bitcoin.doubleValue() * price_btc.doubleValue();
//Number value_ethereum = balance_ethereum.doubleValue() * price_ethereum.doubleValue();
//Number value_bitcoincash = balance_bitcoincash.doubleValue() * price_bitcoincash.doubleValue();
//Number value_usdollarcoin = balance_usdollarcoin.doubleValue() * price_usdollarcoin.doubleValue();
//Number value_monero = balance_monero.doubleValue() * price_monero.doubleValue();
//Number value_fiatdkk = balance_fiatdkk.doubleValue() * price_fiatdkk.doubleValue();
Number totalValue =
// value_skyld_maria.doubleValue()
// + value_skyld_emil.doubleValue()
// + value_skyld_sofie.doubleValue()
value_sol.doubleValue();
// + value_btc.doubleValue()
// + value_ethereum.doubleValue()
// + value_bitcoincash.doubleValue()
// + value_usdollarcoin.doubleValue()
// + value_monero.doubleValue()
// + value_fiatdkk.doubleValue();
boolean addChangeDetailsToTitle = !series_asset_fiatdkk.getData().isEmpty();
final String title = calculateTitle(totalValue.doubleValue(), addChangeDetailsToTitle);
final List<TickerItem> finalTickerItems_solana = tickerItems_solana;
Platform.runLater(() -> {
// Add Solana
for(TickerItem tickerItem : finalTickerItems_solana) {
timeStampForLastValueOnChart = tickerItem.timestamp();
Instant instant = Instant.ofEpochMilli(timeStampForLastValueOnChart);
LocalDateTime ts = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
String tsTxt = ts.format(formatter_hhmm);
Number val = balance_solana.doubleValue() * tickerItem.price();
seriesData_asset_solana.add(new XYChart.Data<>(tsTxt, val));
}
chart.setTitle(title);
});
}
}
private String calculateTitle(double totalValue, boolean addChangeDetails) {
String title = "Networth: " + formatAmount(totalValue) + " DKK";
if(addChangeDetails) {
double historicTotal = calculateInitialNetworthTotal();
double change = totalValue - historicTotal;
title += " (";
title += (change > 0) ? "+" : "";
title += formatAmount(change) + " DKK, ";
double pct = change / historicTotal;
title += (pct > 0) ? "+" : "";
title += formatPercentage(pct) + ")";
}
return title;
}
private double calculateInitialNetworthTotal() {
double total =
series_liability_maria.getData().getFirst().getYValue().doubleValue()
+ series_liability_emil.getData().getFirst().getYValue().doubleValue()
+ series_liability_sofie.getData().getFirst().getYValue().doubleValue()
+ series_asset_solana.getData().getFirst().getYValue().doubleValue()
+ series_asset_bitcoin.getData().getFirst().getYValue().doubleValue()
+ series_asset_ethereum.getData().getFirst().getYValue().doubleValue()
+ series_asset_bitcoincash.getData().getFirst().getYValue().doubleValue()
+ series_asset_usdollarcoin.getData().getFirst().getYValue().doubleValue()
+ series_asset_monero.getData().getFirst().getYValue().doubleValue()
+ series_asset_fiatdkk.getData().getFirst().getYValue().doubleValue();
return total;
}
private double getLastPrice(List<TickerItem> tickerItems) {
double price = 0;
TickerItem item = tickerItems.getLast();
if(item != null) {
price = item.price();
}
return price;
}
@FXML
private void initialize() {
treeView_accounts.setShowRoot(false);
TreeItem<String> rootItem = new TreeItem<>();
rootItem.setExpanded(true);
TreeItem<String> item1 = new TreeItem<>("Assets");
TreeItem<String> item2 = new TreeItem<>("Liabilities");
rootItem.getChildren().addAll(item1, item2);
treeView_accounts.setRoot(rootItem);
treeView_accounts.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>() {
@Override
public TreeCell<String> call(TreeView<String> param) {
return new CheckBoxTreeCell();
}
});
chart.setCreateSymbols(false);
axisY.setUpperBound(50000);
axisY.setLowerBound(-15000);
axisY.setTickUnit(5000);
axisY.setLabel("DKK");
axisY.setTickLabelFormatter(new NumberAxis.DefaultFormatter(axisY) {
private final NumberFormat formatter;
{
// Create a custom locale for Denmark
Locale danishLocale = new Locale("da", "DK");
formatter = NumberFormat.getInstance(danishLocale);
formatter.setMinimumFractionDigits(2);
formatter.setMaximumFractionDigits(2);
}
@Override
public String toString(Number object) {
return formatter.format(object);
}
});
axisY.setAutoRanging(false);
series_asset_solana = new XYChart.Series<>();
series_asset_solana.setName("Solana");
series_asset_bitcoin = new XYChart.Series<>();
series_asset_bitcoin.setName("Bitcoin");
series_asset_ethereum = new XYChart.Series<>();
series_asset_ethereum.setName("Ethereum");
series_asset_bitcoincash = new XYChart.Series<>();
series_asset_bitcoincash.setName("Bitcoin Cash");
series_asset_usdollarcoin = new XYChart.Series<>();
series_asset_usdollarcoin.setName("USD Coin");
series_asset_monero = new XYChart.Series<>();
series_asset_monero.setName("Monero");
series_asset_fiatdkk = new XYChart.Series<>();
series_asset_fiatdkk.setName("Fiat DKK");
series_liability_maria = new XYChart.Series<>();
series_liability_maria.setName("Skyld til Maria");
series_liability_emil = new XYChart.Series<>();
series_liability_emil.setName("Skyld til Emil");
series_liability_sofie = new XYChart.Series<>();
series_liability_sofie.setName("Skyld til Sofie");
seriesData_asset_solana = series_asset_solana.getData();
ObservableList<XYChart.Series<String, Number>> chartData = chart.getData();
chartData.addAll(
//series_liability_maria,
//series_liability_emil,
//series_liability_sofie,
series_asset_solana
//series_asset_fiatdkk,
//series_asset_bitcoin,
//series_asset_ethereum,
//series_asset_bitcoincash,
//series_asset_usdollarcoin,
//series_asset_monero
);
this.displayPeriod = ONE_DAY;
this.timeStampForLastValueOnChart = 0L;
startChartUpdater();
}
@FXML
private TreeView<String> treeView_accounts;
@FXML
private StackedAreaChart<String, Number> chart;
@FXML
private CategoryAxis axisX;
@FXML
private NumberAxis axisY;
private static final TimeUnit POLL_TIMEUNIT = MINUTES;
private static final int POLL_INTERVAL = 15;
private XYChart.Series<String, Number> series_asset_solana;
private XYChart.Series<String, Number> series_asset_bitcoin;
private XYChart.Series<String, Number> series_asset_ethereum;
private XYChart.Series<String, Number> series_asset_bitcoincash;
private XYChart.Series<String, Number> series_asset_usdollarcoin;
private XYChart.Series<String, Number> series_asset_monero;
private XYChart.Series<String, Number> series_asset_fiatdkk;
private XYChart.Series<String, Number> series_liability_maria;
private XYChart.Series<String, Number> series_liability_sofie;
private XYChart.Series<String, Number> series_liability_emil;
private ObservableList<XYChart.Data<String, Number>> seriesData_asset_solana;
private ObservableList<XYChart.Data<String, Number>> seriesData_asset_bitcoin;
private ObservableList<XYChart.Data<String, Number>> seriesData_asset_ethereum;
private ObservableList<XYChart.Data<String, Number>> seriesData_asset_bitcoincash;
private ObservableList<XYChart.Data<String, Number>> seriesData_asset_usdollarcoin;
private ObservableList<XYChart.Data<String, Number>> seriesData_asset_monero;
private ObservableList<XYChart.Data<String, Number>> seriesData_asset_fiatdkk;
private ObservableList<XYChart.Data<String, Number>> seriesData_liability_maria;
private ObservableList<XYChart.Data<String, Number>> seriesData_liability_sofie;
private ObservableList<XYChart.Data<String, Number>> seriesData_liability_emil;
private final Number balance_solana = 43.415906951d;
private final Number balance_bitcoin = 0.00016755d;
private final Number balance_ethereum = 0.00233255d;
private final Number balance_bitcoincash = 0.0069836d;
private final Number balance_usdollarcoin = 0.931486d;
private final Number balance_monero = 0.00213335d;
private final Number balance_fiatdkk = 14000d;
private final Number balance_liability_maria = -0.00887165;
private final Number balance_liability_sofie = -3.57;
private final Number balance_liability_emil = -5.78;
private final String ASSETID_SOLANA = "solana";
private final String ASSETID_ETHEREUM = "ethereum";
private final String ASSETID_BITCOINCASH = "bitcoin-cash";
private final String ASSETID_MONERO = "monero";
private final String ASSETID_USDC = "usd-coin";
private final String ASSETID_BITCOIN = "bitcoin";
private final String ASSETID_DKK = "fiatdkk";
private long displayPeriod;
private long timeStampForLastValueOnChart;
}
@@ -0,0 +1,271 @@
package crypto.r35157.cauldron;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import java.text.NumberFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static crypto.r35157.cauldron.FormatterUtils.formatAmount;
import static crypto.r35157.cauldron.FormatterUtils.formatPercentage;
import static crypto.r35157.cauldron.Ticker.getTickerItems;
import static crypto.r35157.cauldron.TimeUtils.*;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import static java.util.concurrent.TimeUnit.MINUTES;
public class PriceChartController {
private void startChartUpdater() {
ScheduledExecutorService executorService = newSingleThreadScheduledExecutor();
Runnable task = this::updateUI;
executorService.scheduleAtFixedRate(task, 0, POLL_INTERVAL, POLL_TIMEUNIT);
}
/* This synchronized as both the scheduler and manual UI changes can result in the UI gets updated */
private synchronized void updateUI() {
long currentTimeStamp = System.currentTimeMillis();
List<TickerItem> tickerItems;
try {
tickerItems = getTickerItems(
ASSETID__SOLANA,
currentTimeStamp - displayPeriod,
currentTimeStamp
);
} catch(Exception e) {
e.printStackTrace();
tickerItems = Collections.emptyList();
}
if (!tickerItems.isEmpty()) {
double latestPrice = getLastPrice(tickerItems);
final List<TickerItem> finalTickerItems = tickerItems;
Platform.runLater(() -> {
seriesData.clear();
// TODO: This update could be done a bit cheaper instead of just clearing everything and recreating it all.
// There might be a little issue in identifying the points as in seriesData we have only human readable
// timestamp and not the unix epoch.
for(TickerItem tickerItem : finalTickerItems) {
timeStampForLastPriceOnChart = tickerItem.timestamp();
Instant instant = Instant.ofEpochMilli(timeStampForLastPriceOnChart);
LocalDateTime ts = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
String tsTxt = ts.format(formatter_hhmm);
seriesData.add(new XYChart.Data<>(tsTxt, tickerItem.price()));
}
setChartLineColor(latestPrice);
boolean addChangeDetailsToTitle = seriesData.size() > 1;
final String title = calculateTitle(latestPrice, addChangeDetailsToTitle);
chart.setTitle(title);
updatePriceYAxisBounds();
});
}
}
private void setChartLineColor(double latestPrice) {
final String color;
if(seriesData.size() < 2) {
color = "black";
} else {
double initialPrice = getInitialPrice();
if(latestPrice == initialPrice) {
color = "black";
} else {
color = (latestPrice > initialPrice) ? "darkgreen" : "darkred";
}
}
series.getNode().lookup(".chart-series-line").setStyle("-fx-stroke: " + color + ";");
}
private String calculateTitle(double price, boolean addChangeDetails) {
String title = "Solana price: " + formatAmount(price) + " DKK";
if (addChangeDetails) {
double historicPrice = getInitialPrice();
double change = price - historicPrice;
title += " (";
title += (change > 0) ? "+" : "";
title += formatAmount(change) + " DKK, ";
double pct = change / historicPrice;
title += (pct > 0) ? "+" : "";
title += formatPercentage(pct) + ")";
}
return title;
}
private double getInitialPrice() {
return seriesData.getFirst().getYValue().doubleValue();
}
private double getLastPrice(List<TickerItem> tickerItems) {
double price = 0;
TickerItem item = tickerItems.getLast();
if(item != null) {
price = item.price();
}
return price;
}
private void updatePriceYAxisBounds() {
double lowerBound = Double.MAX_VALUE;
double upperBound = Double.MIN_VALUE;
for (XYChart.Data<String, Number> data : seriesData) {
double value = data.getYValue().doubleValue();
if (value < lowerBound) {
lowerBound = value;
}
if (value > upperBound) {
upperBound = value;
}
}
if(upperBound == lowerBound) {
upperBound += upperBound * 0.05;
lowerBound -= lowerBound * 0.05;
} else {
upperBound = upperBound + (upperBound - lowerBound) * 0.05;
lowerBound = lowerBound - (upperBound - lowerBound) * 0.05;
}
double range = upperBound - lowerBound;
axisY.setTickUnit(range / 6);
axisY.setUpperBound(upperBound);
axisY.setLowerBound(lowerBound);
}
private void temporarilyLockSomeUIDueToUnimplementation() {
menuButton_baseAssetType.setText("Fiat");
menuButton_baseAssetType.setDisable(true);
menuButton_quoteAssetType.setText("Crypto");
menuButton_quoteAssetType.setDisable(true);
menuButton_quoteDataSource.setText("Coin Gecko");
menuButton_quoteDataSource.setDisable(true);
menuButton_baseAssetId.setText("DKK - Danske Kroner");
menuButton_baseAssetId.setDisable(true);
menuButton_quoteAssetId.setText("SOL - Solana");
menuButton_quoteAssetId.setDisable(true);
}
@FXML
private void initialize() {
chart.setCreateSymbols(false);
chart.setLegendVisible(false);
axisY.setLabel("DKK");
axisY.setTickLabelFormatter(new NumberAxis.DefaultFormatter(axisY) {
private final NumberFormat formatter;
{
// Create a custom locale for Denmark
Locale danishLocale = new Locale("da", "DK");
formatter = NumberFormat.getInstance(danishLocale);
formatter.setMinimumFractionDigits(2);
formatter.setMaximumFractionDigits(2);
}
@Override
public String toString(Number object) {
return formatter.format(object);
}
});
axisY.setAutoRanging(false);
series = new XYChart.Series<>();
series.setName("Solana");
seriesData = series.getData();
ObservableList<XYChart.Series<String, Number>> chartData = chart.getData();
chartData.add(series);
this.displayPeriod = ONE_DAY;
this.timeStampForLastPriceOnChart = 0L;
this.menuButton_period.setText("24 Hours");
temporarilyLockSomeUIDueToUnimplementation();
startChartUpdater();
}
@FXML
private void periodChanged(ActionEvent event) {
MenuItem selectedItem = (MenuItem) event.getSource();
String text = selectedItem.getText();
menuButton_period.setText(text);
switch (text) {
case "24 Hours" -> this.displayPeriod = ONE_DAY;
case "1 Week" -> this.displayPeriod = ONE_WEEK;
case "1 Month" -> this.displayPeriod = ONE_MONTH;
case "3 Months" -> this.displayPeriod = ONE_QUARTER;
case "1 Year" -> this.displayPeriod = ONE_YEAR;
case "5 Years" -> this.displayPeriod = FIVE_YEARS;
case "10 Years" -> this.displayPeriod = ONE_DECADE;
case "All" -> this.displayPeriod = Long.MAX_VALUE;
}
updateUI();
}
@FXML
private LineChart<String, Number> chart;
@FXML
private NumberAxis axisY;
@FXML
private MenuButton menuButton_period;
@FXML
private MenuButton menuButton_baseAssetType;
@FXML
private MenuButton menuButton_quoteAssetType;
@FXML
private MenuButton menuButton_quoteDataSource;
@FXML
private MenuButton menuButton_baseAssetId;
@FXML
private MenuButton menuButton_quoteAssetId;
private static final TimeUnit POLL_TIMEUNIT = MINUTES;
private static final int POLL_INTERVAL = 15;
private XYChart.Series<String, Number> series;
private ObservableList<XYChart.Data<String, Number>> seriesData;
private final String ASSETID__SOLANA = "solana";
private long displayPeriod;
private long timeStampForLastPriceOnChart;
}
@@ -0,0 +1,109 @@
package crypto.r35157.cauldron;
//import org.json.JSONObject;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
public class PriceFetcher {
public static void main(String[] args) throws Exception {
new PriceFetcher();
Thread.currentThread().join();
}
public PriceFetcher() {
File file = new File(PRICE_FILENAME);
boolean isFileEmpty = !file.exists() || file.length() == 0;
ScheduledExecutorService executorService = newSingleThreadScheduledExecutor();
// Initializing BufferedWriter outside the try-with-resources block to manage it manually
BufferedWriter bw;
try {
FileWriter fw = new FileWriter(PRICE_FILENAME, true);
bw = new BufferedWriter(fw);
if (isFileEmpty) {
bw.write(HEADER);
bw.newLine();
}
// Flushing the header line
bw.flush();
BufferedWriter finalBw = bw; // Needed to access bw inside the Runnable
Runnable task = () -> {
try {
ZonedDateTime now = ZonedDateTime.now();
long timestamp = System.currentTimeMillis();
String humanReadableTimestamp = now.format(formatter);
String sourceId = SOURCEID_COINGECKO;
Map<String, Number> prices = getPrices();
for(String assetId : prices.keySet()) {
double price = prices.get(assetId).doubleValue();
finalBw.write(timestamp + ";" + humanReadableTimestamp + ";" + sourceId + ";" + "fiatdkk_" + assetId + ";" + price);
finalBw.newLine();
}
finalBw.flush();
} catch (Exception e) {
e.printStackTrace();
}
};
executorService.scheduleAtFixedRate(task, 0, POLL_INTERVAL, POLL_TIMEUNIT);
} catch (IOException e) {
e.printStackTrace();
}
}
private Map<String, Number> getPrices() throws Exception {
String urlString = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,solana,monero,bitcoin-cash,usd-coin&vs_currencies=dkk";
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
// Read the response
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuilder content = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
}
in.close();
// Parse JSON response
/*
JSONObject jsonResponse = new JSONObject(content.toString());
// Map response to a simple HashMap
Map<String, Number> cryptoPrices = new HashMap<>();
for (String crypto : jsonResponse.keySet()) {
JSONObject cryptoData = jsonResponse.getJSONObject(crypto);
double price = cryptoData.getDouble("dkk");
cryptoPrices.put(crypto, price);
}
return cryptoPrices;
*/
return null;
}
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss z");
private static final String PRICE_FILENAME = "prices.csv";
private static final String HEADER = "# Epoch ; Human readable timestamp ; Source ID ; Pair (for example fiatdkk_bitcoin) ; Price";
private static final String SOURCEID_COINGECKO = "CoinGecko";
private static final TimeUnit POLL_TIMEUNIT = TimeUnit.MINUTES;
private static final int POLL_INTERVAL = 15;
}
@@ -0,0 +1,100 @@
package crypto.r35157.cauldron;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
public class Ticker {
public static @NotNull List<TickerItem> getTickerItems(
long startTimeStamp,
long stopTimeStamp
) {
return getTickerItems((Collection<String>) null, startTimeStamp, stopTimeStamp);
}
public static @NotNull List<TickerItem> getTickerItems(
String assetId,
long startTimeStamp,
long stopTimeStamp
) {
return getTickerItems(Set.of(assetId), startTimeStamp, stopTimeStamp);
}
public static @NotNull List<TickerItem> getTickerItems(
Collection<String> assetIds,
long startTimeStamp,
long stopTimeStamp
) {
List<TickerItem> tickerItems = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(FILEPATH_PRICES))) {
String line;
while ((line = br.readLine()) != null) {
if (line.startsWith("#")) {
continue;
}
int commentIndex = line.indexOf('#');
if (commentIndex != -1) {
line = line.substring(0, commentIndex);
}
String[] fields = line.split(";");
for (int i = 0; i < fields.length; i++) {
fields[i] = fields[i].trim();
}
long timestamp = Long.parseLong(fields[FIELD__EPOCH]);
if(timestamp < startTimeStamp || timestamp > stopTimeStamp) {
// Outside requested period
continue;
}
String source = fields[FIELD__SOURCE_ID];
String humanReadableTimestamp = fields[FIELD__HUMAN_READABLE_TIMESTAMP];
String pair = fields[FIELD__PAIR];
int index = pair.indexOf("_");
String base = pair.substring(0, index);
String quote = pair.substring(index + 1);
if(assetIds != null && !assetIds.isEmpty() && !assetIds.contains(quote)) {
continue;
}
double price = Double.parseDouble(fields[FIELD__PRICE]);
TickerItem tickerItem = new TickerItem(
timestamp,
humanReadableTimestamp,
source,
base,
quote,
price
);
tickerItems.add(tickerItem);
}
} catch(IOException ioe) {
// TODO: Make handling of error more robust!
ioe.printStackTrace();
}
tickerItems.sort(Comparator.comparingLong(TickerItem::timestamp));
return tickerItems;
}
private static final int FIELD__EPOCH = 0;
private static final int FIELD__HUMAN_READABLE_TIMESTAMP = 1;
private static final int FIELD__SOURCE_ID = 2;
private static final int FIELD__PAIR = 3;
private static final int FIELD__PRICE = 4;
private static final String FILEPATH_PRICES = "prices.csv";
}
@@ -0,0 +1,11 @@
package crypto.r35157.cauldron;
public record TickerItem(
long timestamp,
String humanReadableTimestamp,
String source,
String base,
String quote,
double price
) {
}
@@ -0,0 +1,167 @@
package crypto.r35157.cauldron;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.logging.*;
import java.io.IOException;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
//import org.json.JSONObject;
public class TickerService {
public static void main(String[] args) throws Exception {
configureLogging();
logger.info("Initializing TickerService:");
logger.info(" Starting embedded SQLLite...");
configureDatabase();
logger.info(" Validating schema...");
validateSchema();
logger.info(" Starting HTTP Service on port 8123...");
configureHTTPService();
logger.info("Ready to service!");
}
private static void validateSchema() throws SQLException {
boolean exist = doTableExist(dbConnection, "ticks");
if(exist == false) {
createTable(dbConnection);
}
}
private static boolean doTableExist(Connection dbConnection, String tableName) throws SQLException {
String sql = "SELECT name FROM sqlite_master WHERE type='table' AND name=?";
PreparedStatement pstmt = dbConnection.prepareStatement(sql);
pstmt.setString(1, tableName);
ResultSet rs = pstmt.executeQuery();
return rs.next();
}
private static void configureLogging() {
try {
Formatter logFormatter = new CustomLogFormatter();
// Remove the default console handler
Logger rootLogger = Logger.getLogger("");
rootLogger.removeHandler(rootLogger.getHandlers()[0]);
// Add a custom console handler
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.ALL);
consoleHandler.setFormatter(logFormatter);
rootLogger.addHandler(consoleHandler);
// Add a file handler
FileHandler fileHandler = new FileHandler("TickerService.log", true);
fileHandler.setLevel(Level.ALL);
fileHandler.setFormatter(logFormatter);
rootLogger.addHandler(fileHandler);
} catch (IOException e) {
logger.severe("Failed to set up logging: " + e.getMessage());
}
}
private static void configureDatabase() throws SQLException {
dbConnection = DriverManager.getConnection(DB_URL);
}
private static void createTable(Connection dbConnection) throws SQLException {
String sql = "CREATE TABLE ticks (" +
"epoch INTEGER NOT NULL," +
"baseAssetId TEXT NOT NULL," +
"quoteAssetId TEXT NOT NULL," +
"price REAL NOT NULL," +
"PRIMARY KEY (epoch, baseAssetId, quoteAssetId)" +
");";
Statement stmt = dbConnection.createStatement();
stmt.execute(sql);
}
private static void insertStockPrice(Connection conn, String symbol, double price) throws SQLException {
String sql = "INSERT INTO stock_prices(symbol, price) VALUES(?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, symbol);
pstmt.setDouble(2, price);
pstmt.executeUpdate();
}
}
private static void retrieveStockPrices(Connection conn, String symbol) {
String sql = "SELECT id, symbol, price, timestamp FROM stock_prices WHERE symbol = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, symbol);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getInt("id") + "\t" +
rs.getString("symbol") + "\t" +
rs.getDouble("price") + "\t" +
rs.getString("timestamp"));
}
} catch (SQLException e) {
System.out.println(e.getMessage());
}
}
private static void configureHTTPService() throws Exception{
HttpServer server = HttpServer.create(new InetSocketAddress(PORT), BACKLOG);
server.createContext("/ticker", new MyHandler());
server.setExecutor(null); // Creates a default executor
server.start();
}
static class CustomLogFormatter extends Formatter {
@Override
public String format(LogRecord record) {
return String.format("%1$s [%2$s] %3$s%n",
record.getLevel().getName(),
record.getLoggerName(),
record.getMessage());
}
}
static class MyHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
// TODO: Reenable when import is fixed
/*
JSONObject json = new JSONObject();
json.put("message", "Hello, World!");
String response = json.toString();
exchange.getResponseHeaders().set("Content-type", RESPONSE_MIMETYPE);
exchange.sendResponseHeaders(HTTP_OK, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
*/
}
}
private static final Logger logger = Logger.getLogger(TickerService.class.getName());
private static Connection dbConnection;
private static final int PORT = 8123;
private static final int BACKLOG = 10;
private static final String RESPONSE_MIMETYPE = "application/json";
private static final int HTTP_OK = 200;
private static final String DB_URL = "jdbc:sqlite:ticker.db";
}
@@ -0,0 +1,15 @@
package crypto.r35157.cauldron;
import java.time.format.DateTimeFormatter;
public class TimeUtils {
public static final DateTimeFormatter formatter_hhmm = DateTimeFormatter.ofPattern("HH:mm");
public static final long ONE_DAY = 1000 * 60 * 60 * 24;
public static final long ONE_WEEK = ONE_DAY * 7;
public static final long ONE_MONTH = ONE_DAY * 31;
public static final long ONE_QUARTER = ONE_MONTH * 3;
public static final long ONE_HALFYEAR = ONE_MONTH * 6;
public static final long ONE_YEAR = (long)(ONE_DAY * 365.2425f);
public static final long FIVE_YEARS = ONE_YEAR * 5;
public static final long ONE_DECADE = ONE_DAY * 10;
}
@@ -0,0 +1,6 @@
package crypto.r35157.cauldron;
public class Version {
public static final String VERSION_CAULDRON = "trunk";
public static final String VERSION_EVELYN = "trunk";
}
@@ -0,0 +1,19 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public interface BusinessLogic {
BigDecimal calculateContractValueToTrade(
BigDecimal maxMarginAmountToTradePerIteration,
BigDecimal maxStretchMarginAmountToTradePerIteration,
BigDecimal positionValue,
BigDecimal currentPrice,
BigDecimal tokensPerContract,
int leverage,
BigDecimal maxTradePctOfFullPosition
);
BigDecimal min(BigDecimal a, BigDecimal b);
BigDecimal max(BigDecimal a, BigDecimal b);
}
@@ -0,0 +1,51 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
import static java.math.BigDecimal.ZERO;
import static java.math.RoundingMode.HALF_UP;
public class BusinessLogicImpl implements BusinessLogic {
@Override
public BigDecimal calculateContractValueToTrade(
BigDecimal maxMarginAmountToTradePerIteration,
BigDecimal maxStretchMarginAmountToTradePerIteration,
BigDecimal positionValue,
BigDecimal currentPrice,
BigDecimal tokensPerContract,
int leverage,
BigDecimal maxTradePctOfFullPosition
) {
BigDecimal valueToTrade = maxMarginAmountToTradePerIteration.multiply(BigDecimal.valueOf(leverage));
BigDecimal valueOfOneContract = currentPrice.multiply(tokensPerContract);
BigDecimal maxTradePctOfFullPositionPct = maxTradePctOfFullPosition.divide(new BigDecimal(100), 4, HALF_UP);
if (maxTradePctOfFullPositionPct.compareTo(ZERO) > 0) {
// Add soft limit
BigDecimal softLimit = positionValue.multiply(maxTradePctOfFullPositionPct);
valueToTrade = min(softLimit, valueToTrade);
}
// Test if we are allowed to trade this size of contracts
if (valueOfOneContract.compareTo(valueToTrade) > 0) {
// We cannot afford even one single contract - try stretch
BigDecimal maxStretchContractValueToTrade = maxStretchMarginAmountToTradePerIteration.multiply(BigDecimal.valueOf(leverage));
if (valueOfOneContract.compareTo(maxStretchContractValueToTrade) <= 0) {
// With 'stretch' we can afford, at least, one single contract - but limit to ONE contract ONLY!
valueToTrade = valueOfOneContract;
}
}
return valueToTrade;
}
@Override
public BigDecimal min(BigDecimal a, BigDecimal b) {
return a.compareTo(b) < 0 ? a : b;
}
@Override
public BigDecimal max(BigDecimal a, BigDecimal b) {
return a.compareTo(b) < 0 ? b : a;
}
}
@@ -0,0 +1,175 @@
package crypto.r35157.cauldron.afets;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import static crypto.r35157.TimeTools.parseTimeToMillis;
public class Configuration {
public Configuration(@NotNull String configurationFilename) {
this.configurationFilename = configurationFilename;
}
public BigDecimal maxSpendPctOfFullPosition() {
return maxSpendPctOfFullPosition;
}
public long exchangeTransactionDelay() {
return exchangeTransactionDelay;
}
public BigDecimal expensiveFundingFeeThreshold() {
return expensiveFundingFeeThreshold;
}
public int minimumEmptyPositionsToRestart() {
return minimumEmptyPositionsToRestart;
}
public BigDecimal minimumPctEarningForLongs() {
return minimumPctEarningForLongs;
}
public BigDecimal minimumPctEarningForShorts() {
return minimumPctEarningForShorts;
}
public BigDecimal maxMarginAmountToTradePerIteration() {
return maxMarginAmountToTradePerIteration;
}
public BigDecimal maxStretchMarginAmountToTradePerIteration() {
return maxStretchMarginAmountToTradePerIteration;
}
public boolean executeOpen() {
return executeOpen;
}
public BigDecimal adjustedScoreTooBigPositionsPenaltyCoefficient() {
return adjustedScoreTooBigPositionsPenaltyCoefficient;
}
public boolean executeClose() {
return executeClose;
}
public boolean enableNotification() {
return enableNotification;
}
public boolean printFundingFees() {
return printFundingFees;
}
public boolean printEquilibriumStatus() {
return printEquilibriumStatus;
}
public boolean printPositionStatus() {
return printPositionStatus;
}
public boolean printProfitLoss() {
return printProfitLoss;
}
public boolean restartPositions() {
return restartPositions;
}
public long getIterationTime() {
return iterationTime;
}
public long getTimeOut() {
return timeOut;
}
public boolean detectAndPrepareForBrandNewPositions() {
return detectAndPrepareForBrandNewPositions;
}
/**
* Update this configuration from the configuration file.
* @throws IOException if the configuration file cannot be read.
*/
public void update() throws IOException {
Path configFile = Paths.get(configurationFilename);
List<String> lines = Files.readAllLines(configFile);
for (String line : lines) {
line = line.trim();
// Skip empty lines or separator lines.
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
// Assume key and value are separated by whitespace.
String[] parts = line.split("\\s+");
if (parts.length < 2) {
continue; // or throw an exception if file format must be strict
}
String key = parts[0];
String value = parts[1];
switch (key) {
case "IterationTime" -> iterationTime = parseTimeToMillis(value);
case "TimeOut" -> timeOut = parseTimeToMillis(value);
case "DetectAndPrepareForBrandNewPositions" -> detectAndPrepareForBrandNewPositions = Boolean.parseBoolean(value);
case "RestartPositions" -> restartPositions = Boolean.parseBoolean(value);
case "PrintEquilibriumStatus" -> printEquilibriumStatus = Boolean.parseBoolean(value);
case "PrintPositionStatus" -> printPositionStatus = Boolean.parseBoolean(value);
case "PrintProfitLoss" -> printProfitLoss = Boolean.parseBoolean(value);
case "PrintFundingFees" -> printFundingFees = Boolean.parseBoolean(value);
case "ExecuteOpen" -> executeOpen = Boolean.parseBoolean(value);
case "ExecuteClose" -> executeClose = Boolean.parseBoolean(value);
case "EnableNotification" -> enableNotification = Boolean.parseBoolean(value);
case "MaxMarginAmountToTradePerIteration" -> maxMarginAmountToTradePerIteration = new BigDecimal(value);
case "MaxStretchMarginAmountToTradePerIteration" -> maxStretchMarginAmountToTradePerIteration = new BigDecimal(value);
case "MinimumPctEarningForLongs" -> minimumPctEarningForLongs = new BigDecimal(value);
case "MinimumPctEarningForShorts" -> minimumPctEarningForShorts = new BigDecimal(value);
case "MinimumEmptyPositionsToRestart" -> minimumEmptyPositionsToRestart = Integer.parseInt(value);
case "ExpensiveFundingFeeThreshold" -> expensiveFundingFeeThreshold = new BigDecimal(value);
case "ExchangeTransactionDelay" -> exchangeTransactionDelay = Long.parseLong(value);
case "MaxSpendPctOfFullPosition" -> maxSpendPctOfFullPosition = new BigDecimal(value);
case "PriceDiscoveryDuration" -> priceDiscoveryDuration = parseTimeToMillis(value);
case "AdjustedScoreTooBigPositionsPenaltyCoefficient" -> adjustedScoreTooBigPositionsPenaltyCoefficient = new BigDecimal(value);
default -> System.out.println("WARNING! Configuration key '" + key + "' unknown! Skipping!");
}
}
}
private final String configurationFilename;
private long iterationTime;
private long timeOut;
private boolean detectAndPrepareForBrandNewPositions;
private boolean restartPositions;
private boolean printEquilibriumStatus;
private boolean printPositionStatus;
private boolean printProfitLoss;
private boolean printFundingFees;
private boolean executeOpen;
private boolean executeClose;
private boolean enableNotification;
private BigDecimal maxMarginAmountToTradePerIteration;
private BigDecimal maxStretchMarginAmountToTradePerIteration;
private BigDecimal minimumPctEarningForLongs;
private BigDecimal minimumPctEarningForShorts;
private int minimumEmptyPositionsToRestart;
private BigDecimal expensiveFundingFeeThreshold;
private long exchangeTransactionDelay;
private BigDecimal maxSpendPctOfFullPosition;
private long priceDiscoveryDuration;
private BigDecimal adjustedScoreTooBigPositionsPenaltyCoefficient;
}
@@ -0,0 +1,7 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public class EarningsStatistics {
BigDecimal totalEarnings;
}
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public record EquilibriumStatus(
BigDecimal longEquilibrium,
BigDecimal shortEquilibrium,
BigDecimal availableEquilibrium
) { }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
package crypto.r35157.cauldron.afets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
public class EventWatcher {
public EventWatcher(Thread workerThread, String directoryName) throws IllegalArgumentException {
this.workerThread = workerThread;
this.directory = Paths.get(directoryName);
if(!(Files.exists(directory) && Files.isDirectory(directory))) {
String error = "'" + directory.toAbsolutePath().normalize() + "' is not a directory or do not exist!";
throw new IllegalArgumentException(error);
}
}
public void start() throws IOException {
log.info("Register watch on '$EVELYN_HOME/tmp/iteration.signal' to signal thread '" + workerThread.getName()
+ "' to start an iteration.");
WatchService watchService = FileSystems.getDefault().newWatchService();
directory.register(watchService, ENTRY_CREATE);
Thread.ofVirtual().start(() -> {
try {
while (true) {
WatchKey key = watchService.take(); // Blocking
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path filename = (Path) event.context();
if (kind == ENTRY_CREATE && "iteration.signal".equals(filename.toString())) {
log.info("EventWatcher found 'tmp/iteration.signal' file "
+ "- sleeping worker thread will be waken now (and the signal file will be deleted).");
Path fullPath = directory.resolve(filename);
try {
Files.deleteIfExists(fullPath);
} catch (IOException e) {
log.error("ERROR: Could NOT delete signal file 'tmp/iteration.signal'! ("
+ e.getMessage() + ")");
}
workerThread.interrupt();
}
}
key.reset();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
private static final Logger log = LoggerFactory.getLogger(EventWatcher.class);
private final Path directory;
private final Thread workerThread;
}
@@ -0,0 +1,10 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
import java.util.List;
public record FundingFeeStatus(
List<String> feeWarnings, // TODO: Well... Presentation here is not too nice :-/
BigDecimal totalFeesToPay,
BigDecimal totalFeesToReceive
) {}
@@ -0,0 +1,14 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public record FutureBasePrices(
Position position,
PositionType type,
BigDecimal quantityAbleToBuy,
BigDecimal contractsAbleToBuy,
BigDecimal futureBasePrice,
BigDecimal rawScore,
BigDecimal adjustedScore,
BigDecimal currentPrice
) {}
@@ -0,0 +1,7 @@
package crypto.r35157.cauldron.afets;
public class InvalidAPIKeyException extends RuntimeException {
public InvalidAPIKeyException(String message) {
super(message);
}
}
@@ -0,0 +1,274 @@
package crypto.r35157.cauldron.afets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MEXCFuturesPlugin {
public MEXCFuturesPlugin(String webKey, String userAgent) {
this.httpClient = HttpClient.newHttpClient();
this.gson = new GsonBuilder().create();
baseHeaders = new HashMap<>();
baseHeaders.put("User-Agent", userAgent);
setWebKey(webKey);
}
public void setWebKey(String webKey) {
if(!webKey.equals(this.webKey)) {
if(this.webKey == null) {
log.debug("Setting WebKey!");
} else {
log.info("WebKey change detected - updating!");
}
this.webKey = webKey;
baseHeaders.put("Authorization", webKey);
baseHeaders.put("Cookie", "u_id=" + webKey);
} else {
log.debug("WebKey unchanged - skipping update!");
}
}
// Place order (market and limit)
public String placeOrder(Object obj) throws IOException, InterruptedException {
return sendPost("/order/create", obj, true);
}
// Get orders trigger
public ServiceResponse getOrdersTrigger() throws IOException, InterruptedException {
// No payload provided in the Python code example.
return sendGet("/planorder/list/orders?states=1", null, false);
}
// Get open positions
public ServiceResponse getOpenPositions() throws IOException, InterruptedException {
return sendGet("/position/open_positions", null, false);
}
public ServiceResponse getLeverage(String symbol) throws IOException, InterruptedException {
return sendGet("/position/leverage?symbol=" + symbol, null, false);
}
public ServiceResponse getAccountDetails(String symbol) throws IOException, InterruptedException {
return sendGet("/account/asset/" + symbol, null, false);
}
// Close order trigger note: Python wraps obj in a list.
public String closeOrderTrigger(Object obj) throws IOException, InterruptedException {
Object payload = List.of(obj);
return sendPost("/planorder/cancel", payload, true);
}
// Close all orders trigger (no payload)
public String closeAllOrdersTrigger() throws IOException, InterruptedException {
Map<String, Object> payload = new HashMap<>();
return sendPost("/planorder/cancel_all", payload, true);
}
// Open order trigger (for plan orders)
public String openOrderTrigger(Object obj) throws IOException, InterruptedException {
return sendPost("/planorder/place/v2", obj, true);
}
// Get open orders limit
public ServiceResponse getOpenOrdersLimit() throws IOException, InterruptedException {
return sendGet("/order/list/open_orders", null, false);
}
// Add Stop Loss / Take Profit for limit orders
public String addSTTPForLimit(Object obj) throws IOException, InterruptedException {
return sendPost("/stoporder/change_price", obj, true);
}
// Close all limits
public String closeAllLimits() throws IOException, InterruptedException {
Map<String, Object> payload = new HashMap<>();
return sendPost("/order/cancel_all", payload, true);
}
public String trade(MEXCPositionSide side, int numOfContracts, String symbol) throws Exception {
return trade(side, numOfContracts, symbol, null);
}
//MEXCPositionSide sideId = (side == PositionType.marginLong)
// ? MEXCPositionSide.OpenLong
// : MEXCPositionSide.OpenShort;
public String trade(MEXCPositionSide side, int numOfContracts, String symbol, Integer leverage) throws Exception {
int vol = numOfContracts; // Number of contracts to trade
MEXCOpenType openType = MEXCOpenType.Cross;
MEXCType type = MEXCType.MarketOrders;
MEXCOrderType orderType = MEXCOrderType.MarketOrders;
// Example order object (for place order)
Map<String, Object> orderObj = new HashMap<>();
orderObj.put("symbol", symbol);
orderObj.put("side", "" + side.getValue());
orderObj.put("openType", "" + openType.getValue());
orderObj.put("type", "" + type.getValue());
orderObj.put("orderType", "" + orderType.getValue());
orderObj.put("vol", vol);
if(leverage != null) {
orderObj.put("leverage", leverage);
}
//orderObj.put("price", 2);
orderObj.put("priceProtect", "0");
// Call open_order_trigger with a sample object similar to obj3 in Python
//Map<String, Object> openOrderObj = new HashMap<>();
//openOrderObj.put("executeCycle", 3);
//openOrderObj.put("leverage", 100);
//openOrderObj.put("openType", 1);
//openOrderObj.put("orderType", 1);
//openOrderObj.put("positionMode", 1);
//openOrderObj.put("price", "30");
//openOrderObj.put("priceProtect", "0");
//openOrderObj.put("side", 1);
//openOrderObj.put("symbol", "TONCOIN_USDT");
//openOrderObj.put("trend", 1);
//openOrderObj.put("triggerPrice", "10");
//openOrderObj.put("triggerType", 1);
//openOrderObj.put("vol", 1);
String openOrderResponse = placeOrder(orderObj);
return openOrderResponse;
}
private static String md5(String value) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(value.getBytes(StandardCharsets.UTF_8));
// Convert to hex string
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException("MD5 algorithm not found", ex);
}
}
private Map<String, String> mexcCrypto(Object obj) {
// Current time in ms as string.
String dateNow = String.valueOf(Instant.now().toEpochMilli());
// Compute g = md5(key + dateNow) and take substring starting at index 7.
String g = md5(webKey + dateNow).substring(7);
// Convert the object to JSON with no extra spaces.
// Gson's default toJson produces compact JSON.
String s = gson.toJson(obj);
// sign = md5(dateNow + s + g)
String sign = md5(dateNow + s + g);
Map<String, String> result = new HashMap<>();
result.put("time", dateNow);
result.put("sign", sign);
return result;
}
private String sendPost(String endpoint, Object payload, boolean signRequest) throws IOException, InterruptedException {
String url = BASE_URL + endpoint;
String jsonPayload = gson.toJson(payload);
Map<String, String> headers = new HashMap<>(baseHeaders);
headers.put("Content-type", "application/json");
if (signRequest) {
Map<String, String> signature = mexcCrypto(payload);
headers.put("x-mxc-sign", signature.get("sign"));
headers.put("x-mxc-nonce", signature.get("time"));
}
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload));
headers.forEach(builder::header);
HttpRequest request = builder.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String responseTxt = response.body();
return responseTxt;
}
private ServiceResponse sendGet(String endpoint, Object payload, boolean signRequest) throws IOException, InterruptedException {
// For GET, if a payload is provided, assume it is converted to query parameters.
String url = BASE_URL + endpoint;
if (payload != null) {
// Assuming payload is a Map<String, Object>
@SuppressWarnings("unchecked")
Map<String, Object> paramMap = (Map<String, Object>) payload;
if (!paramMap.isEmpty()) {
String query = buildQueryString(paramMap);
url += "?" + query;
}
}
Map<String, String> headers = new HashMap<>(baseHeaders);
headers.put("Content-type", "application/json");
if (signRequest && payload != null) {
Map<String, String> signature = mexcCrypto(payload);
headers.put("x-mxc-sign", signature.get("sign"));
headers.put("x-mxc-nonce", signature.get("time"));
}
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET();
headers.forEach(builder::header);
HttpRequest request = builder.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
ServiceResponse sr = new ServiceResponse(response.statusCode(), response.body());
return sr;
}
private String buildQueryString(Map<String, Object> params) {
StringBuilder sb = new StringBuilder();
params.entrySet().stream().sorted(Map.Entry.comparingByKey())
.forEach(entry -> {
if (sb.length() > 0) sb.append("&");
sb.append(urlEncode(entry.getKey()))
.append("=")
.append(urlEncode(entry.getValue().toString()));
});
return sb.toString();
}
private String urlEncode(String s) {
try {
return URLEncoder.encode(s, StandardCharsets.UTF_8.toString())
.replaceAll("\\+", "%20");
} catch (Exception e) {
throw new RuntimeException("UTF-8 encoding not supported", e);
}
}
private static final String BASE_URL = "https://futures.mexc.com/api/v1/private";
private static final Logger log = LoggerFactory.getLogger(MEXCFuturesPlugin.class);
private String webKey;
private final Map<String, String> baseHeaders;
private final HttpClient httpClient;
private final Gson gson;
}
@@ -0,0 +1,21 @@
package crypto.r35157.cauldron.afets;
public enum MEXCOpenType {
Isolated(1),
Cross(2);
private final int value;
MEXCOpenType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
@@ -0,0 +1,24 @@
package crypto.r35157.cauldron.afets;
public enum MEXCOrderType {
LimitOrder(1),
PostOnlyMaker(2),
CloseOrCancelInstantly(3),
CloseOrCancelCompletely(4),
MarketOrders(5);
private final int value;
MEXCOrderType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
@@ -0,0 +1,23 @@
package crypto.r35157.cauldron.afets;
public enum MEXCPositionSide {
OpenLong(1),
CloseShort(2),
OpenShort(3),
CloseLong(4);
private final int value;
MEXCPositionSide(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
@@ -0,0 +1,114 @@
package crypto.r35157.cauldron.afets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public enum MEXCProtocolErrors {
operationSucceeded(0, "Operate succeed", true),
publicAbnormal(9999, "Public abnormal", false),
internalError(500, "Internal error", false),
systemBusy(501, "System busy", false),
unauthorized(401, "Unauthorized", true),
apiKeyExpired(402, "Api_key expired", true),
IpNotOnWhitelist(406, "Accessed IP is not in the whitelist", true),
unknownSource(506, "Unknown source of request", true),
excessiveRequestFrequency(510, "Excessive frequency of requests", false),
endpointInaccessible(511, "Endpoint inaccessible", true),
invalidRequest(513, "Invalid request(for open api serves time more or less than 10s)", false),
parameterError(600, "Parameter error", true),
dataDecodingError(601, "Data decoding error", true),
verifyFailed(602, "Verify failed", true),
repeatedRequests(603, "Repeated requests", false),
accountReadPermissionRequired(701, "Account read permission is required", true),
accpuntModifyPermissionRequired(702, "Account modify permission is required", true),
tradeInformationReadPermissionRequired(703, "Trade information read permission is required", true),
transactionInformationModifyPermissionRequired(704, "Transaction information modify permission is required", true),
accountDoesNotExist(1000, "Account does not exist", true),
contractDoesNotExist(1001, "Contract does not exist", true),
contractNotActivated(1002, "Contract not activated", true),
errorRiskLimitLevel(1003, "Error in risk limit level", true),
amountError(1004, "Amount error", true),
wrongOrderDirection(2001, "Wrong order direction", true),
wrongOrderType1(2002, "Wrong opening type", true),
overpriced(2003, "Overpriced to pay", true),
lowPriceForSelling(2004, "Low-price for selling", true),
balanceInsufficient(2005, "Balance insufficient", true),
leverageRatioError(2006, "Leverage ratio error", true),
orderPriceError(2007, "Order price error", true),
insufficientQuantity(2008, "The quantity is insufficient", true),
nonexistentPosition(2009, "Positions do not exist or have been closed", true),
orderQuanityError(2011, "Order quantity error", true),
cancelOrdersOverMaxLimit(2013, "Cancel orders over maximum limit", true),
batchQuantityExceedsLimit(2014, "The quantity of batch order exceeds the limit", true),
priceOrQuantityAccuracyError(2015, "Price or quantity accuracy error", true),
triggerVolumeOverMax(2016, "Trigger volume over the maximum", true),
exceedingMaxAvailableMargin(2018, "Exceeding the maximum available margin", true),
activeOpenPosition(2019, "There is an active open position", true),
leverageNotConsistentWithPosition(2021, "The single leverage is not consistent with the existing position leverage", true),
wrongOrderType2(2022, "Wrong position type", true),
positionsOverMaxLeverage(2023, "There are positions over the maximum leverage", true),
ordersOverMaxLeverage(2024, "There are orders with leverage over the maximum", true),
positionsOverMaxAllowable(2025, "The holding positions is over the maximum allowable positions", true),
leverageModificationForCrossNotAllowed(2026, "Modification of leverage is not supported for cross", true),
onlyOneCrossOrIsolatedInSameDirection(2027, "There is only one cross or isolated in the same direction", true),
maxOrderQuantityExceeded(2028, "The maximum order quantity is exceeded", true),
orderTypeError(2029, "Error order type", true),
externalOrderIdTooLong(2030, "External order ID is too long (Max. 32 bits )", true),
positionExceedsRiskLimit(2031, "The allowable holding position exceed the current risk limit", true),
orderPriceLessThanLongPositionForcedLiquidationPrice(2032, "Order price is less than long position force liquidate price", true),
orderPriceMoreThanShortPositionForcedLiquidationPrice(2033, "Order price is more than short position force liquidate price", true),
batchQueryQuantityLimitExceeded(2034, "The batch query quantity limit is exceeded", true),
unsupportedMarketPriceTier(2035, "Unsupported market price tier", true),
triggerPriceTypeError(3001, "Trigger price type error", true),
triggerTypeError(3002, "Trigger type error", true),
executiveCycleError(3003, "Executive cycle error", true),
triggerPriceError(3004, "Trigger price error", true),
unsupportedCurrency(4001, "Unsupported currency", true),
orderLimitReached(2036, "The orders more than the limit, please contact customer service", true),
transactionPostTooFast(2037, "Frequent transactions, please try it later", false),
positionLimitReached(2038, "The maximum allowable position quantity is exceeded, please contact customer service!", true),
takePriceAndStopLossError(5001, "The take-price and the stop-loss price cannot be none at the same time", true),
stopLimitOrderDoNotExist(5002, "The Stop-Limit order does not exist or has closed", true),
takeProfitStopLossPriceSettingError(5003, "Take-profit and stop-loss price setting is wrong", true),
takeProfitStopLossOrderVolumeError(5004, "The take-profit and stop-loss order volume is more than the holding positions can be liquidated", true),
tradeForbidden(6001, "Trading forbidden", true),
openForbidden(6002, "Open forbidden", true),
timeRangeError(6003, "Time range error", true),
tradingPairAndStatusMissing(6004, "The trading pair and status should be fill in", true),
tradingPairNotAvailable(6005, "The trading pair is not available", true);
MEXCProtocolErrors(int errorCode, String description, boolean persistent) {
this.errorCode = errorCode;
this.description = description;
this.persistent = persistent;
}
public int getErrorCode() {
return errorCode;
}
public String getDescription() {
return description;
}
public boolean isPersistent() {
return persistent;
}
@Override
public String toString() {
return errorCode + ": " + description;
}
private static final Map<Integer, MEXCProtocolErrors> ERROR_CODE_MAP =
Arrays.stream(values()).collect(Collectors.toMap(MEXCProtocolErrors::getErrorCode, e -> e));
public static MEXCProtocolErrors fromErrorCode(int code) {
return ERROR_CODE_MAP.get(code);
}
private final int errorCode;
private final String description;
private final boolean persistent;
}
@@ -0,0 +1,141 @@
package crypto.r35157.cauldron.afets;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MEXCSpotPlugin {
public MEXCSpotPlugin(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}
public String getPositions() throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
// For a GET request, if there are query parameters, add them to a map.
// In this example, we assume there are no extra query parameters.
Map<String, String> queryParams = new HashMap<>();
// If needed, add parameters:
// queryParams.put("param1", "value1");
// Create the query string (if there are any parameters)
String requestParam = MEXCSpotPlugin.getRequestParamString(queryParams);
// Build the complete URL (append query parameters if not empty)
String fullUrl = API_OPENPOSITIONS;
if (!requestParam.isEmpty()) {
fullUrl += "?" + requestParam;
}
// Create a request timestamp
String reqTime = String.valueOf(System.currentTimeMillis());
// Create a SignVo for generating the signature.
// For GET requests, the requestParam string is the query string (sorted, URL-encoded).
MEXCSpotPlugin.SignVo signVo = new MEXCSpotPlugin.SignVo(reqTime, accessKey, secretKey, requestParam);
String signature = MEXCSpotPlugin.sign(signVo);
// Build the HttpRequest with the required headers
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(fullUrl))
.header("Content-Type", "application/json")
.header("ApiKey", accessKey)
.header("Request-Time", reqTime)
.header("Signature", signature)
.GET()
.build();
// Send the request and get the response
HttpResponse<String> httpResp = client.send(request, HttpResponse.BodyHandlers.ofString());
String responseJson = httpResp.body();
return responseJson;
}
/**
* Gets the get request parameter string
*
* @param param get/delete Request parameters map
*/
public static String getRequestParamString(Map<String, String> param) {
if (MapUtils.isEmpty(param)) {
return "";
}
StringBuilder sb = new StringBuilder(1024);
SortedMap<String, String> map = new TreeMap<>(param);
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = StringUtils.isBlank(entry.getValue()) ? "" : entry.getValue();
sb.append(key).append('=').append(urlEncode(value)).append('&');
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
public static String urlEncode(String s) {
try {
return URLEncoder.encode(s, "UTF-8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("UTF-8 encoding not supported!");
}
}
public static String sign(SignVo signVo) {
String requestParam = (signVo.requestParam == null) ? "" : signVo.requestParam;
String str = signVo.accessKey + signVo.reqTime + requestParam;
return actualSignature(str, signVo.secretKey);
}
public static String actualSignature(String inputStr, String key) {
Mac hmacSha256;
try {
hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secKey =
new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSha256.init(secKey);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("No such algorithm: " + e.getMessage());
} catch (InvalidKeyException e) {
throw new RuntimeException("Invalid key: " + e.getMessage());
}
byte[] hash = hmacSha256.doFinal(inputStr.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(hash);
}
public record SignVo(
String reqTime,
String accessKey,
String secretKey,
String requestParam // Get the request parameters are sorted in dictionary order, with & concatenated strings, POST should be a JSON string
) {}
private static final Logger log = LoggerFactory.getLogger(MEXCSpotPlugin.class);
private static final String API_BASEURL = "https://contract.mexc.com/api/v1/";
private static final String API_PRIVATGEURL = API_BASEURL + "private/";
private static final String API_OPENPOSITIONS = API_PRIVATGEURL + "position/open_positions";
private final String accessKey;
private final String secretKey;
}
@@ -0,0 +1,25 @@
package crypto.r35157.cauldron.afets;
public enum MEXCType {
PriceLimitOrder(1),
PostOnlyMaker(2),
TransactOrCancelInstantly(3),
TransactCompletelyOrCancelCompletely(4),
MarketOrders(5),
ConvertMarketPriceToCurrentPrice(6);
private final int value;
MEXCType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
@@ -0,0 +1,18 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public record NextCloseableInfo(
Position position,
BigDecimal quantityToTradeTokens,
int quantityToTradeContracts,
PositionType positionType,
BigDecimal earning,
BigDecimal earningPct,
BigDecimal rawScore,
BigDecimal adjustedScore,
BigDecimal balanceTokens,
int balanceContracts,
int leverage
) {
}
@@ -0,0 +1,208 @@
package crypto.r35157.cauldron.afets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import crypto.r35157.cauldron.afets.contractdetails.ContractDetailsItem;
import crypto.r35157.cauldron.afets.contractdetails.MEXCContractDetails;
import crypto.r35157.cauldron.afets.gami.GlobalAssetMetadataIndex;
import crypto.r35157.cauldron.afets.gami.GlobalAssetMetadataIndexItem;
import crypto.r35157.cauldron.afets.ticker.PositionItem;
import crypto.r35157.cauldron.afets.ticker.PositionResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import static crypto.r35157.cauldron.afets.PositionType.marginLong;
import static crypto.r35157.cauldron.afets.PositionType.marginShort;
public class Portfolio {
public Portfolio(
PushNotifier pushNotifier,
String positionsFilename,
GlobalAssetMetadataIndex gami,
MEXCSpotPlugin mexcSpotPlugin,
MEXCContractDetails contractDetails
) {
this.pushNotifier = pushNotifier;
this.positionsFilename = positionsFilename;
this.gami = gami;
this.mexcSpotPlugin = mexcSpotPlugin;
this.contractDetails = contractDetails;
gson = new GsonBuilder().create();
}
public List<Position> getPositions() {
return positions;
}
public void updateAll() throws Exception {
List<PositionItem> mexcPositionList;
// TODO: Super ugly - will fix later
while(true) {
String mexcPositionJson = mexcSpotPlugin.getPositions();
PositionResponse posResponse = gson.fromJson(mexcPositionJson, PositionResponse.class);
mexcPositionList = posResponse.data();
if(mexcPositionList != null && !mexcPositionList.isEmpty()) {
// Everything is OK - break out!
break;
}
String errorMsg = "ERROR: mexcPositionList is null or empty (retry in 1 minute): " + mexcPositionJson;
log.error(errorMsg);
pushNotification(errorMsg);
Thread.sleep(60 * 1000);
}
Map<String, PositionItem> mexcPositionMap = new HashMap<>();
for (PositionItem item : mexcPositionList) { // THIS CAN THROW AN NPE
int positionType = item.positionType();
String typeStr = positionType == 1 ? "long" : "short";
String key = item.symbol() + "/" + typeStr;
if(mexcPositionMap.containsKey(key)) {
log.error("ERROR: " + key + " already in MEXC map! Skipping!");
continue;
}
mexcPositionMap.put(key, item);
}
Map<String, Position> localPositionMap = new HashMap<>();
List<String> lines = Files.readAllLines(Paths.get(positionsFilename));
for(String origLine : lines) {
String line = origLine.trim();
// Skip if the line is empty or is a full comment line.
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
// Remove any trailing comment from the line (everything after a '#' character).
int commentIndex = line.indexOf('#');
if (commentIndex != -1) {
line = line.substring(0, commentIndex).trim();
}
// Skip the line if nothing is left.
if (line.isEmpty()) {
continue;
}
// Split the line on one or more whitespace characters.
String[] parts = line.split("\\s+");
if (parts.length != 6) {
log.error("ERROR: Unparseable line in '" + positionsFilename + "' (" + origLine + ")");
continue;
}
// The file format is:
// Name, Type, StartTime, GAMIId, NumberOfDecimalsInTrade and MaxTotal
boolean quarantined = false;
int index = 0;
String name = parts[index]; // Unused here - just for identification in file
String typeStr = parts[++index];
long startTime = Long.parseLong(parts[++index]);
String gamiIdStr = parts[++index];
int numberOfDecimalsInTrade = Integer.parseInt(parts[++index]);
BigDecimal maxTotal = new BigDecimal(parts[++index]);
UUID gamiId = UUID.fromString(gamiIdStr);
GlobalAssetMetadataIndexItem gamiIdItem = gami.getById(gamiId);
PositionType type = ("long".equalsIgnoreCase(typeStr)) ? marginLong : marginShort;
Position position = new Position(
quarantined,
gamiIdItem,
type,
startTime,
null,
null,
null,
numberOfDecimalsInTrade,
null,
maxTotal
);
String key = position.gamiId().symbolMEXC() + "/" + position.type();
if(localPositionMap.containsKey(key)) {
log.error("ERROR: " + key + " seems to be defined twice in 'Positions.json' - Skipping duplicate one!");
continue;
}
localPositionMap.put(key, position);
}
// Map the two piles of garbage together
positions = new ArrayList<>();
for (Position localPos : localPositionMap.values()) {
String symbol = localPos.gamiId().symbolMEXC();
String key = symbol + "/" + localPos.type();
PositionItem mexcPos = mexcPositionMap.get(key);
if(mexcPos == null) {
log.warn(" WARNING: Cannot find position '" + key + "' from 'Position.json' on MEXC - skipping!");
continue;
}
ContractDetailsItem contractDetailsItem = contractDetails.getContractDetails(symbol);
BigDecimal contractSize = contractDetailsItem.contractSize();
BigDecimal basePrice = new BigDecimal(mexcPos.holdAvgPriceFullyScale());
BigDecimal numOfContracts = new BigDecimal(mexcPos.holdVol());
BigDecimal balance = numOfContracts.multiply(contractSize);
Position mergedPos = new Position(
localPos.quarantined(),
localPos.gamiId(),
localPos.type(),
localPos.startTime(),
basePrice,
balance,
mexcPos.leverage(),
localPos.numberOfDecimalsInTrade(),
contractSize,
localPos.maxTotal()
);
positions.add(mergedPos);
}
// Check for rogue position (positions that exist in MEXC but is not controlled by Evelyn)
for(String key : mexcPositionMap.keySet()) {
if(localPositionMap.containsKey(key) == false) {
log.warn(" WARNING: " + key + " is NOT controlled by Evelyn - Skipping!");
}
}
int a = 0;
}
private void pushNotification(String message) {
if(pushNotifier != null) {
pushNotifier.push(message);
}
}
private final GlobalAssetMetadataIndex gami;
private final MEXCSpotPlugin mexcSpotPlugin;
private final MEXCContractDetails contractDetails;
private final Gson gson;
private final String positionsFilename;
private final PushNotifier pushNotifier;
private static final Logger log = LoggerFactory.getLogger(Portfolio.class);
private List<Position> positions;
}
@@ -0,0 +1,20 @@
package crypto.r35157.cauldron.afets;
import crypto.r35157.cauldron.afets.gami.GlobalAssetMetadataIndexItem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.math.BigDecimal;
public record Position(
boolean quarantined,
@NotNull GlobalAssetMetadataIndexItem gamiId,
@NotNull PositionType type,
long startTime,
@Nullable BigDecimal basePrice,
@Nullable BigDecimal balance,
@Nullable Integer leverage,
int numberOfDecimalsInTrade,
@Nullable BigDecimal tokensPerContract,
@Nullable BigDecimal maxTotal
) {}
@@ -0,0 +1,8 @@
package crypto.r35157.cauldron.afets;
public enum PositionSideAction {
OpenLong,
CloseLong,
OpenShort,
CloseShort
}
@@ -0,0 +1,17 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public record PositionStatistics(
int totalNumberOfLongPositions,
int totalNumberOfShortPositions,
BigDecimal totalLongPositionsValue,
BigDecimal totalShortPositionsValue,
BigDecimal totalLongBaseValue,
BigDecimal totalShortBaseValue,
BigDecimal totalLongMarginValue,
BigDecimal totalShortMarginValue,
BigDecimal totalAvgLongPositionSize,
BigDecimal totalAvgShortPositionSize
) {
}
@@ -0,0 +1,17 @@
package crypto.r35157.cauldron.afets;
public enum PositionType {
marginLong("long"),
marginShort("short");
PositionType(String humanReadableName) {
this.humanReadableName = humanReadableName;
}
@Override
public String toString() {
return humanReadableName;
}
private final String humanReadableName;
}
@@ -0,0 +1,18 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public record PositionValues(
Position position,
BigDecimal currentPrice,
BigDecimal basePrice,
BigDecimal balance,
int leverage,
BigDecimal positionValue,
BigDecimal baseValue,
BigDecimal marginValue
) {
}
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public record ProfitLoss(
BigDecimal pnlOnLongs,
BigDecimal pnlOnShorts,
BigDecimal pnlUnrealized
) {}
@@ -0,0 +1,68 @@
package crypto.r35157.cauldron.afets;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import static java.nio.charset.StandardCharsets.UTF_8;
public class PushNotifier {
public PushNotifier(@NotNull String token, @NotNull String user) {
this.token = token;
this.user = user;
}
public void push(@NotNull String message) {
try {
// Build the POST data string with URL encoding
StringBuilder postData = new StringBuilder();
postData.append("token=").append(URLEncoder.encode(token, UTF_8));
postData.append("&user=").append(URLEncoder.encode(user, UTF_8));
postData.append("&message=").append(URLEncoder.encode(message, UTF_8));
byte[] postDataBytes = postData.toString().getBytes(UTF_8);
// Create the connection to the Pushover API
URL url = new URL(ENDPOINT);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length));
// Write the POST data to the request body
try (OutputStream os = conn.getOutputStream()) {
os.write(postDataBytes);
}
// Read the response from the API
int responseCode = conn.getResponseCode();
InputStream is = (responseCode < HttpURLConnection.HTTP_BAD_REQUEST)
? conn.getInputStream() : conn.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, UTF_8));
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
// Output the response code and body
//System.out.println("Response Code: " + responseCode);
//System.out.println("Response: " + response.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String ENDPOINT = "https://api.pushover.net/1/messages.json";
private final String token;
private final String user;
}
@@ -0,0 +1,16 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
public interface ScoreCalculator {
/**
* Calculates a trade score based on the parameter inputs. Different implementations can use
* different strategies to arrive at a score.
* The only importance is that scores are later used to compare against each other, so scores
* from different implementations cannot be expected to work.
*
* @param input A collection of different values that can be helpful for the score calculation.
* @return The implementation-specific score
*/
BigDecimal calculateScore(ScoreCalculatorInput input);
}
@@ -0,0 +1,42 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
import static crypto.r35157.cauldron.afets.PositionType.marginLong;
import static java.math.RoundingMode.HALF_UP;
public class ScoreCalculatorImpl implements ScoreCalculator {
@Override
public BigDecimal calculateScore(ScoreCalculatorInput input) {
// Guard clause
if(input.futureTokenBaseNotationPrice.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("'futureTokenBaseNotationPrice' must be > 0");
}
if(input.currentTokenNotationPrice.compareTo(BigDecimal.ZERO) == 0) {
throw new ArithmeticException("'currentTokenNotationPrice' cannot be zero");
}
if(input.amountNotionalToSpend.compareTo(BigDecimal.ZERO) == 0) {
throw new ArithmeticException("Notional amount to spend cannot be zero");
}
// Correct calculation of units traded at actual market price
BigDecimal unitsToBeTraded = input.amountNotionalToSpend.divide(input.currentTokenNotationPrice, 20, HALF_UP);
BigDecimal priceDiff = (input.type == marginLong)
? input.currentTokenBaseNotationPrice.subtract(input.futureTokenBaseNotationPrice)
: input.futureTokenBaseNotationPrice.subtract(input.currentTokenBaseNotationPrice);
// Profit improvement in absolute terms
BigDecimal profit = priceDiff.multiply(unitsToBeTraded);
// Relative profit improvement per dollar spent
BigDecimal relativeProfit = profit.divide(input.amountNotionalToSpend, 20, HALF_UP);
// Final intuitive score
BigDecimal score = relativeProfit.add(BigDecimal.ONE);
return score;
}
}
@@ -0,0 +1,57 @@
package crypto.r35157.cauldron.afets;
import java.math.BigDecimal;
/**
* A data transfer object that encapsulates the input parameters required for score calculation. This class provides
* all the necessary data points that different implementations of a ScoreCalculator may use to compute a score.
* Users of this class can represent various aspects of a trading position, such as pricing,
* leverage, position type, and contract specifications.
*/
public class ScoreCalculatorInput {
/**
* Input parameters for the score calculation. Different implementations of ScoreCalculator can choose which of
* them to use or leave out of the score calculation.
*
* @param currentTokenBaseNotationPrice Current average entry notation price per token (before trade)
* @param futureTokenBaseNotationPrice New average entry notation price per token (after trade)
* @param currentTokenNotationPrice Current notation price per token (before trade)
* @param amountNotionalToSpend Notional amount to spend (not margin amount)
* @param totalNumberOfPositions The total number of positions of this type (longs or shorts)
* @param contractSize Number of tokens per contract (can be fractional, e.g., 0.1)
* @param leverage How much leverage is used - set this to ONE if leverage is not used.
* @param type Position type (long or short)
* @param averagePositionValue The average notation value size (= totalValue / totalNumberOfPositions)
*/
public ScoreCalculatorInput(
BigDecimal currentTokenBaseNotationPrice,
BigDecimal futureTokenBaseNotationPrice,
BigDecimal currentTokenNotationPrice,
BigDecimal amountNotionalToSpend,
int totalNumberOfPositions,
BigDecimal contractSize,
BigDecimal leverage,
PositionType type,
BigDecimal averagePositionValue
) {
this.currentTokenNotationPrice = currentTokenNotationPrice;
this.futureTokenBaseNotationPrice = futureTokenBaseNotationPrice;
this.currentTokenBaseNotationPrice = currentTokenBaseNotationPrice;
this.amountNotionalToSpend = amountNotionalToSpend;
this.totalNumberOfPositions = totalNumberOfPositions;
this.contractSize = contractSize;
this.leverage = leverage;
this.type = type;
this.averagePositionValue = averagePositionValue;
}
public BigDecimal currentTokenBaseNotationPrice;
public BigDecimal futureTokenBaseNotationPrice;
public BigDecimal currentTokenNotationPrice;
public BigDecimal amountNotionalToSpend;
public int totalNumberOfPositions;
public BigDecimal contractSize;
public BigDecimal leverage;
public PositionType type;
public BigDecimal averagePositionValue;
}
@@ -0,0 +1,6 @@
package crypto.r35157.cauldron.afets;
public record ServiceResponse(
int code,
String body
) {}
@@ -0,0 +1,14 @@
package crypto.r35157.cauldron.afets;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
public class ThousandsSeparatorFormatter {
public static String formatWithJavaThousandsSeparator(int number) {
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setGroupingSeparator('_');
DecimalFormat formatter = new DecimalFormat("#,###", symbols);
return formatter.format(number);
}
}
@@ -0,0 +1,154 @@
package crypto.r35157.cauldron.afets;
import crypto.r35157.TimeTools;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class TimeOuts {
public TimeOuts(@NotNull String filename, long duration) {
timeoutMap = new HashMap<>();
commentsMap = new HashMap<>();
this.filename = filename;
this.duration = duration;
log.info("Timeouts file: {}", filename);
update();
purge();
log.info(" Timeout: {} (Initialized with {} entries)",
TimeTools.formatTimeFromMillis(duration), timeoutMap.size());
}
private void update() {
File file = new File(filename);
if (!file.exists()) {
timeoutMap.clear();
commentsMap.clear();
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
String comment;
while ((line = reader.readLine()) != null) {
// Skip empty lines or lines starting with #
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
int commentIndex = line.indexOf('#');
if (commentIndex != -1) {
comment = line.substring(commentIndex + 1).trim();
line = line.substring(0, commentIndex).trim();
} else {
comment = null;
}
String[] parts = line.split(" ", 2);
if (parts.length == 2) {
String key = parts[0];
try {
timeoutMap.put(key, Long.parseLong(parts[1]));
if(comment != null && !comment.trim().isEmpty()) {
commentsMap.put(key, comment);
}
} catch (NumberFormatException e) {
log.error("Skipping invalid line: {}", line);
}
}
}
} catch (IOException e) {
log.error("ERROR: Could not update TimeOuts by reading from '" + filename + "' - " + e.getMessage());
}
}
private void persist() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
for (Map.Entry<String, Long> entry : timeoutMap.entrySet()) {
String line = entry.getKey() + " " + entry.getValue();
String comment = commentsMap.get(entry.getKey());
if (comment != null && !comment.trim().isEmpty()) {
line += " #" + comment;
}
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
log.error("ERROR: Could not persist TimeOuts by writing to '" + filename + "' - " + e.getMessage());
}
}
public boolean isInTimeOut(UUID uuid, PositionType positionType) {
purge();
String key = getKey(uuid, positionType);
return timeoutMap.containsKey(key);
}
public void putInTimeout(UUID uuid, PositionType positionType, String comment) {
Long timeout = System.currentTimeMillis() + duration;
String key = getKey(uuid, positionType);
timeoutMap.put(key, timeout);
if (comment != null && !comment.trim().isEmpty()) {
commentsMap.put(key, comment);
}
persist();
purge();
}
public void purge() {
long now = System.currentTimeMillis();
int numberOfElementsBeforePurge = timeoutMap.size();
// Store keys to be removed
Set<String> keysToRemove = timeoutMap.entrySet()
.stream()
.filter(entry -> entry.getValue() <= now)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
// Remove from both maps
keysToRemove.forEach(key -> {
timeoutMap.remove(key);
commentsMap.remove(key);
});
int numberOfElementsAfterPurge = timeoutMap.size();
int numberOfPurges = numberOfElementsBeforePurge - numberOfElementsAfterPurge;
if(numberOfPurges > 0) {
log.info("Purged {} TimeOuts", numberOfPurges);
persist();
}
}
private String getKey(UUID uuid, PositionType positionType) {
return uuid.toString() + "/" + positionType.toString();
}
private static final Logger log = LoggerFactory.getLogger(TimeOuts.class);
private final long duration;
private final String filename;
private final Map<String, Long> timeoutMap;
private final Map<String, String> commentsMap;
}
@@ -0,0 +1,16 @@
package crypto.r35157.cauldron.afets.accountdetails;
import java.math.BigDecimal;
public record AccountDetails(
String currency,
BigDecimal positionMargin,
BigDecimal availableBalance,
BigDecimal cashBalance,
BigDecimal frozenBalance,
BigDecimal equity,
BigDecimal unrealized,
BigDecimal bonus,
BigDecimal availableCash,
BigDecimal availableOpen
) { }
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets.accountdetails;
import java.math.BigDecimal;
public record AccountDetailsResponse(
boolean success,
BigDecimal code,
AccountDetails data
) { }
@@ -0,0 +1,8 @@
package crypto.r35157.cauldron.afets.accountdetails;
import crypto.r35157.cauldron.afets.InvalidAPIKeyException;
import org.jetbrains.annotations.NotNull;
public interface MEXCAccountDetailsService {
@NotNull AccountDetails getDetails() throws InvalidAPIKeyException, Exception;
}
@@ -0,0 +1,39 @@
package crypto.r35157.cauldron.afets.accountdetails;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.ToNumberPolicy;
import crypto.r35157.cauldron.afets.InvalidAPIKeyException;
import crypto.r35157.cauldron.afets.MEXCFuturesPlugin;
import crypto.r35157.cauldron.afets.ServiceResponse;
import org.jetbrains.annotations.NotNull;
public class MEXCAccountDetailsServiceImpl implements MEXCAccountDetailsService {
public MEXCAccountDetailsServiceImpl(MEXCFuturesPlugin mexFuturesPlugin, String symbol) {
this.mexFuturesPlugin = mexFuturesPlugin;
this.symbol = symbol;
gson = new GsonBuilder()
.setObjectToNumberStrategy(ToNumberPolicy.BIG_DECIMAL)
.create();
}
public @NotNull AccountDetails getDetails() throws InvalidAPIKeyException, Exception {
ServiceResponse response = mexFuturesPlugin.getAccountDetails(symbol);
int code = response.code();
if(code == 200) {
String json = response.body();
AccountDetailsResponse adr = gson.fromJson(json, AccountDetailsResponse.class);
return adr.data();
} else if(code == 401) {
throw new InvalidAPIKeyException("API key invalid!");
} else {
throw new Exception("Error getting account details (Error code: " + code + "): " + response.body());
}
}
private final MEXCFuturesPlugin mexFuturesPlugin;
private final Gson gson;
private final String symbol;
}
@@ -0,0 +1,59 @@
package crypto.r35157.cauldron.afets.accountdetails;
import crypto.r35157.cauldron.afets.InvalidAPIKeyException;
import crypto.r35157.cauldron.afets.MEXCFuturesPlugin;
import crypto.r35157.nenjim.EncryptedFileProperties;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RetryingMEXCAccountDetailsServiceDecorator implements MEXCAccountDetailsService {
public RetryingMEXCAccountDetailsServiceDecorator(
MEXCFuturesPlugin mexFuturesPlugin,
MEXCAccountDetailsService accountDetailsService,
int maxNumberOfRetries,
EncryptedFileProperties secrets,
long sleepTime
) {
this.mexFuturesPlugin = mexFuturesPlugin;
this.accountDetailsService = accountDetailsService;
this.maxNumberOfRetires = maxNumberOfRetries;
this.secrets = secrets;
this.sleepTime = sleepTime;
}
public @NotNull AccountDetails getDetails() throws InvalidAPIKeyException, Exception {
boolean keyInvalidKnown = false;
for(int i = 0; i < maxNumberOfRetires - 1; i++) {
try {
return accountDetailsService.getDetails();
} catch(InvalidAPIKeyException e) {
if(keyInvalidKnown) {
throw e;
} else {
keyInvalidKnown = true;
log.error("ERROR: Invalid MEXC API key (" + e.getMessage() + ") - Updating and retrying...");
secrets.refresh();
String mexcWebKey = secrets.getProperty("assetaz_exchange_mexc_webkey");
mexFuturesPlugin.setWebKey(mexcWebKey);
}
} catch(Exception e) {
log.error("ERROR: Getting account details (" + e.getMessage() + ") - Try "
+ (i + 1) + "/" + maxNumberOfRetires + "), retrying in " + sleepTime + "ms...");
}
Thread.sleep(sleepTime);
}
return accountDetailsService.getDetails();
}
private static final Logger log = LoggerFactory.getLogger(RetryingMEXCAccountDetailsServiceDecorator.class);
private final MEXCFuturesPlugin mexFuturesPlugin;
private final MEXCAccountDetailsService accountDetailsService;
private final int maxNumberOfRetires;
private final EncryptedFileProperties secrets;
private final long sleepTime;
}
@@ -0,0 +1,74 @@
package crypto.r35157.cauldron.afets.contractdetails;
import java.math.BigDecimal;
import java.util.List;
public record ContractDetailsItem(
String symbol,
String displayName,
String displayNameEn,
int positionOpenType,
String baseCoin,
String quoteCoin,
String baseCoinName,
String quoteCoinName,
int futureType,
String settleCoin,
BigDecimal contractSize,
int minLeverage,
int maxLeverage,
int countryConfigContractMaxLeverage,
int priceScale,
int volScale,
int amountScale,
double priceUnit,
int volUnit,
long minVol,
long maxVol,
double bidLimitPriceRate,
double askLimitPriceRate,
double takerFeeRate,
double makerFeeRate,
double maintenanceMarginRate,
double initialMarginRate,
long riskBaseVol,
long riskIncrVol,
int riskLongShortSwitch,
BigDecimal riskIncrMmr,
BigDecimal riskIncrImr,
int riskLevelLimit,
double priceCoefficientVariation,
List<String> indexOrigin,
int state,
boolean isNew,
boolean isHot,
boolean isHidden,
List<String> conceptPlate,
List<Integer> conceptPlateId,
String riskLimitType,
List<Integer> maxNumOrders,
int marketOrderMaxLevel,
double marketOrderPriceLimitRate1,
double marketOrderPriceLimitRate2,
double triggerProtect,
int appraisal,
int showAppraisalCountdown,
int automaticDelivery,
boolean apiAllowed,
List<String> depthStepList,
long limitMaxVol,
int threshold,
String baseCoinIconUrl,
int id,
String vid,
String baseCoinId,
long createTime,
long openingTime,
int openingCountdownOption,
boolean showBeforeOpen,
boolean isMaxLeverage,
boolean isZeroFeeRate,
String riskLimitMode,
List<RiskLimitCustom> riskLimitCustom
) {
}
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets.contractdetails;
import java.util.List;
public record ContractDetailsResponse(
boolean success,
int code,
List<ContractDetailsItem> data
) { }
@@ -0,0 +1,59 @@
package crypto.r35157.cauldron.afets.contractdetails;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import crypto.r35157.cauldron.afets.ticker.MEXCTickerItem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MEXCContractDetails {
public MEXCContractDetails() {
map = new HashMap<>();
client = HttpClient.newHttpClient();
gson = new GsonBuilder()
.registerTypeAdapter(MEXCTickerItem.class, new MEXCContractDetailsDeserializer())
.create();
request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.GET()
.build();
}
public void updateAll() throws Exception {
HttpResponse<String> httpResp = client.send(request, HttpResponse.BodyHandlers.ofString());
String json = httpResp.body();
ContractDetailsResponse resp = gson.fromJson(json, ContractDetailsResponse.class);
List<ContractDetailsItem> items = resp.data();
for (ContractDetailsItem item : items) {
map.put(item.symbol(), item);
}
}
public @Nullable ContractDetailsItem getContractDetails(@NotNull String symbol) {
ContractDetailsItem item = map.get(symbol);
return item;
}
public Set<String> getAllSymbols() {
return map.keySet();
}
private static final String API_URL = "https://contract.mexc.com/api/v1/contract/detail";
private final HttpClient client;
private final HttpRequest request;
private final Map<String, ContractDetailsItem> map;
private final Gson gson;
}
@@ -0,0 +1,203 @@
package crypto.r35157.cauldron.afets.contractdetails;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class MEXCContractDetailsDeserializer implements JsonDeserializer<ContractDetailsResponse> {
@Override
public ContractDetailsResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
boolean success = jsonObject.get("success").getAsBoolean();
int code = jsonObject.get("code").getAsInt();
List<ContractDetailsItem> dataItems = new ArrayList<>();
JsonArray dataArray = jsonObject.getAsJsonArray("data");
for (JsonElement dataElement : dataArray) {
JsonObject itemObj = dataElement.getAsJsonObject();
String symbol = itemObj.get("symbol").getAsString();
String displayName = itemObj.get("displayName").getAsString();
String displayNameEn = itemObj.get("displayNameEn").getAsString();
int positionOpenType = itemObj.get("positionOpenType").getAsInt();
String baseCoin = itemObj.get("baseCoin").getAsString();
String quoteCoin = itemObj.get("quoteCoin").getAsString();
String baseCoinName = itemObj.get("baseCoinName").getAsString();
String quoteCoinName = itemObj.get("quoteCoinName").getAsString();
int futureType = itemObj.get("futureType").getAsInt();
String settleCoin = itemObj.get("settleCoin").getAsString();
BigDecimal contractSize = jsonObject.get("contractSize").getAsBigDecimal();
int minLeverage = itemObj.get("minLeverage").getAsInt();
int maxLeverage = itemObj.get("maxLeverage").getAsInt();
int countryConfigContractMaxLeverage = itemObj.get("countryConfigContractMaxLeverage").getAsInt();
int priceScale = itemObj.get("priceScale").getAsInt();
int volScale = itemObj.get("volScale").getAsInt();
int amountScale = itemObj.get("amountScale").getAsInt();
double priceUnit = itemObj.get("priceUnit").getAsDouble();
int volUnit = itemObj.get("volUnit").getAsInt();
long minVol = itemObj.get("minVol").getAsLong();
long maxVol = itemObj.get("maxVol").getAsLong();
double bidLimitPriceRate = itemObj.get("bidLimitPriceRate").getAsDouble();
double askLimitPriceRate = itemObj.get("askLimitPriceRate").getAsDouble();
double takerFeeRate = itemObj.get("takerFeeRate").getAsDouble();
double makerFeeRate = itemObj.get("makerFeeRate").getAsDouble();
double maintenanceMarginRate = itemObj.get("maintenanceMarginRate").getAsDouble();
double initialMarginRate = itemObj.get("initialMarginRate").getAsDouble();
long riskBaseVol = itemObj.get("riskBaseVol").getAsLong();
long riskIncrVol = itemObj.get("riskIncrVol").getAsLong();
int riskLongShortSwitch = itemObj.get("riskLongShortSwitch").getAsInt();
BigDecimal riskIncrMmr = itemObj.get("riskIncrMmr").getAsBigDecimal();
BigDecimal riskIncrImr = itemObj.get("riskIncrImr").getAsBigDecimal();
int riskLevelLimit = itemObj.get("riskLevelLimit").getAsInt();
double priceCoefficientVariation = itemObj.get("priceCoefficientVariation").getAsDouble();
List<String> indexOrigin = new ArrayList<>();
JsonArray indexOriginArray = itemObj.getAsJsonArray("indexOrigin");
for (JsonElement element : indexOriginArray) {
indexOrigin.add(element.getAsString());
}
int state = itemObj.get("state").getAsInt();
boolean isNew = itemObj.get("isNew").getAsBoolean();
boolean isHot = itemObj.get("isHot").getAsBoolean();
boolean isHidden = itemObj.get("isHidden").getAsBoolean();
List<String> conceptPlate = new ArrayList<>();
JsonArray conceptPlateArray = itemObj.getAsJsonArray("conceptPlate");
for (JsonElement element : conceptPlateArray) {
conceptPlate.add(element.getAsString());
}
List<Integer> conceptPlateId = new ArrayList<>();
JsonArray conceptPlateIdArray = itemObj.getAsJsonArray("conceptPlateId");
for (JsonElement element : conceptPlateIdArray) {
conceptPlateId.add(element.getAsInt());
}
String riskLimitType = itemObj.get("riskLimitType").getAsString();
List<Integer> maxNumOrders = new ArrayList<>();
JsonArray maxNumOrdersArray = itemObj.getAsJsonArray("maxNumOrders");
for (JsonElement element : maxNumOrdersArray) {
maxNumOrders.add(element.getAsInt());
}
int marketOrderMaxLevel = itemObj.get("marketOrderMaxLevel").getAsInt();
double marketOrderPriceLimitRate1 = itemObj.get("marketOrderPriceLimitRate1").getAsDouble();
double marketOrderPriceLimitRate2 = itemObj.get("marketOrderPriceLimitRate2").getAsDouble();
double triggerProtect = itemObj.get("triggerProtect").getAsDouble();
int appraisal = itemObj.get("appraisal").getAsInt();
int showAppraisalCountdown = itemObj.get("showAppraisalCountdown").getAsInt();
int automaticDelivery = itemObj.get("automaticDelivery").getAsInt();
boolean apiAllowed = itemObj.get("apiAllowed").getAsBoolean();
List<String> depthStepList = new ArrayList<>();
JsonArray depthStepListArray = itemObj.getAsJsonArray("depthStepList");
for (JsonElement element : depthStepListArray) {
depthStepList.add(element.getAsString());
}
int limitMaxVol = itemObj.get("limitMaxVol").getAsInt();
int threshold = itemObj.get("threshold").getAsInt();
String baseCoinIconUrl = itemObj.get("baseCoinIconUrl").getAsString();
int id = itemObj.get("id").getAsInt();
String vid = itemObj.get("vid").getAsString();
String baseCoinId = itemObj.get("baseCoinId").getAsString();
long createTime = itemObj.get("createTime").getAsLong();
long openingTime = itemObj.get("openingTime").getAsLong();
int openingCountdownOption = itemObj.get("openingCountdownOption").getAsInt();
boolean showBeforeOpen = itemObj.get("showBeforeOpen").getAsBoolean();
boolean isMaxLeverage = itemObj.get("isMaxLeverage").getAsBoolean();
boolean isZeroFeeRate = itemObj.get("isZeroFeeRate").getAsBoolean();
String riskLimitMode = itemObj.get("riskLimitMode").getAsString();
List<RiskLimitCustom> riskLimitCustomList = new ArrayList<>();
JsonArray riskLimitCustomArray = itemObj.getAsJsonArray("riskLimitCustom");
for (JsonElement element : riskLimitCustomArray) {
JsonObject riskObj = element.getAsJsonObject();
int level = riskObj.get("level").getAsInt();
int maxVolVal = riskObj.get("maxVol").getAsInt();
double mmr = riskObj.get("mmr").getAsDouble();
double imr = riskObj.get("imr").getAsDouble();
int maxLeverageVal = riskObj.get("maxLeverage").getAsInt();
riskLimitCustomList.add(new RiskLimitCustom(level, maxVolVal, mmr, imr, maxLeverageVal));
}
ContractDetailsItem item = new ContractDetailsItem(
symbol,
displayName,
displayNameEn,
positionOpenType,
baseCoin,
quoteCoin,
baseCoinName,
quoteCoinName,
futureType,
settleCoin,
contractSize,
minLeverage,
maxLeverage,
countryConfigContractMaxLeverage,
priceScale,
volScale,
amountScale,
priceUnit,
volUnit,
minVol,
maxVol,
bidLimitPriceRate,
askLimitPriceRate,
takerFeeRate,
makerFeeRate,
maintenanceMarginRate,
initialMarginRate,
riskBaseVol,
riskIncrVol,
riskLongShortSwitch,
riskIncrMmr,
riskIncrImr,
riskLevelLimit,
priceCoefficientVariation,
indexOrigin,
state,
isNew,
isHot,
isHidden,
conceptPlate,
conceptPlateId,
riskLimitType,
maxNumOrders,
marketOrderMaxLevel,
marketOrderPriceLimitRate1,
marketOrderPriceLimitRate2,
triggerProtect,
appraisal,
showAppraisalCountdown,
automaticDelivery,
apiAllowed,
depthStepList,
limitMaxVol,
threshold,
baseCoinIconUrl,
id,
vid,
baseCoinId,
createTime,
openingTime,
openingCountdownOption,
showBeforeOpen,
isMaxLeverage,
isZeroFeeRate,
riskLimitMode,
riskLimitCustomList
);
dataItems.add(item);
}
return new ContractDetailsResponse(success, code, dataItems);
}
}
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets.contractdetails;
public record RiskLimitCustom(
int level,
int maxVol,
double mmr,
double imr,
int maxLeverage
) { }
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets.fundingrate;
import java.util.List;
public record FundingRatesResponse(
boolean success,
int code,
List<MEXCFundingRateItem> data
) { }
@@ -0,0 +1,13 @@
package crypto.r35157.cauldron.afets.fundingrate;
import java.math.BigDecimal;
public record MEXCFundingRateItem(
String symbol,
BigDecimal fundingRate,
BigDecimal maxFundingRate,
BigDecimal minFundingRate,
int collectCycle,
long nextSettleTime,
long timestamp
){}
@@ -0,0 +1,73 @@
package crypto.r35157.cauldron.afets.fundingrate;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MEXCFundingRates {
public MEXCFundingRates() {
fundingRateMap = new HashMap<>();
client = HttpClient.newHttpClient();
gson = new GsonBuilder()
.registerTypeAdapter(MEXCFundingRateItem.class, new MEXCFundingRatesDeserializer())
.create();
request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.GET()
.build();
}
public void updateAll() throws Exception {
HttpResponse<String> httpResp = client.send(request, HttpResponse.BodyHandlers.ofString());
String fundingFeeJson = httpResp.body();
List<MEXCFundingRateItem> fundingRateItems = mapFundingRates(fundingFeeJson);
refreshLocalFundingRateMap(fundingRateItems);
}
private void refreshLocalFundingRateMap(List<MEXCFundingRateItem> fundingRateItems) {
fundingRateMap.clear();
for(MEXCFundingRateItem item : fundingRateItems) {
fundingRateMap.put(item.symbol(), item);
}
}
private List<MEXCFundingRateItem> mapFundingRates(String json) {
final FundingRatesResponse response;
final List<MEXCFundingRateItem> items;
try {
response = gson.fromJson(json, FundingRatesResponse.class);
items = response.data();
} catch (Exception e) {
throw e;
}
return items;
}
public @Nullable MEXCFundingRateItem getFundingRate(@NotNull String localSymbol) {
MEXCFundingRateItem item = fundingRateMap.get(localSymbol);
return item;
}
private static final String API_URL = "https://contract.mexc.com/api/v1/contract/funding_rate";
private final HttpClient client;
private final HttpRequest request;
private final Map<String, MEXCFundingRateItem> fundingRateMap;
private final Gson gson;
}
@@ -0,0 +1,38 @@
package crypto.r35157.cauldron.afets.fundingrate;
import com.google.gson.*;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Type;
import java.math.BigDecimal;
public class MEXCFundingRatesDeserializer implements JsonDeserializer<MEXCFundingRateItem> {
@Override
public MEXCFundingRateItem deserialize(
@NotNull JsonElement json,
@NotNull Type typeOfT,
@NotNull JsonDeserializationContext context
) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String symbol = jsonObject.get("symbol").getAsString();
BigDecimal fundingRate = jsonObject.get("fundingRate").getAsBigDecimal();
BigDecimal maxFundingRate = jsonObject.get("maxFundingRate").getAsBigDecimal();
BigDecimal minFundingRate = jsonObject.get("minFundingRate").getAsBigDecimal();
int collectCycle = jsonObject.get("collectCycle").getAsInt();
long nextSettleTime = jsonObject.get("nextSettleTime").getAsLong();
long timestamp = jsonObject.get("timestamp").getAsLong();
MEXCFundingRateItem item = new MEXCFundingRateItem(
symbol,
fundingRate,
maxFundingRate,
minFundingRate,
collectCycle,
nextSettleTime,
timestamp
);
return item;
}
}
@@ -0,0 +1,94 @@
package crypto.r35157.cauldron.afets.gami;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class GlobalAssetMetadataIndex {
public GlobalAssetMetadataIndex(String configFilename) {
this.configFilename = configFilename;
}
public @Nullable GlobalAssetMetadataIndexItem getByMEXCSymbol(@NotNull String localSymbol) {
// TODO: Currently hardcoded to work with MEXC only
for(GlobalAssetMetadataIndexItem item : items) {
if(item.symbolMEXC().equals(localSymbol)) {
return item;
}
}
return null;
}
public @Nullable GlobalAssetMetadataIndexItem getById(@NotNull UUID uuid) {
for(GlobalAssetMetadataIndexItem item : items) {
if(item.gamiId().equals(uuid)) {
return item;
}
}
return null;
}
public void updateAll() throws Exception {
List<GlobalAssetMetadataIndexItem> items = new ArrayList<>();
List<String> lines = Files.readAllLines(Paths.get(configFilename));
for(String origLine : lines) {
String line = origLine.trim();
// Skip if the line is empty or is a full comment line.
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
// Remove any trailing comment from the line (everything after a '#' character).
int commentIndex = line.indexOf('#');
if (commentIndex != -1) {
line = line.substring(0, commentIndex).trim();
}
// Skip the line if nothing is left.
if (line.isEmpty()) {
continue;
}
// Split the line on one or more whitespace characters.
String[] parts = line.split("\\s+");
// Expecting at least 3 parts: Name, GamiId, and MexcSymbol.
if (parts.length != 3) {
log.error("ERROR: Unparseable line in '" + configFilename + "' (" + origLine + ")");
continue;
}
// The file format is:
// Name, GamiId, MexcSymbol
String name = parts[0];
String gamiIdStr = parts[1];
String mexcSymbol = parts[2];
GlobalAssetMetadataIndexItem item = new GlobalAssetMetadataIndexItem(
UUID.fromString(gamiIdStr),
name,
mexcSymbol
);
items.add(item);
}
this.items = items;
// TODO: This is ugly - but lets just drop all those entries that do not exist on MEXC
items.removeIf(item -> item.symbolMEXC() == null);
}
private static final Logger log = LoggerFactory.getLogger(GlobalAssetMetadataIndex.class);
private final String configFilename;
private List<GlobalAssetMetadataIndexItem> items;
}
@@ -0,0 +1,12 @@
package crypto.r35157.cauldron.afets.gami;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public record GlobalAssetMetadataIndexItem(
@NotNull UUID gamiId,
@NotNull String name,
@Nullable String symbolMEXC
) {}
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets.leverage;
import java.util.List;
public record LeverageInfoResponse(
boolean success,
int code,
List<MEXCLeverageInfoItem> data
) { }
@@ -0,0 +1,43 @@
package crypto.r35157.cauldron.afets.leverage;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import crypto.r35157.cauldron.afets.MEXCFuturesPlugin;
import crypto.r35157.cauldron.afets.ServiceResponse;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.List;
public class MEXCLeverageInfo {
public MEXCLeverageInfo(MEXCFuturesPlugin mexFuturesPlugin) {
this.mexFuturesPlugin = mexFuturesPlugin;
gson = new GsonBuilder()
.registerTypeAdapter(MEXCLeverageInfoItem.class, new MEXCLeverageInfoDeserializer())
.create();
}
public @Nullable MEXCLeverageInfoItem getLeverageInfo(@NotNull String symbol, int positionType)
throws IOException, InterruptedException {
ServiceResponse rs = mexFuturesPlugin.getLeverage(symbol);
String json = rs.body();
LeverageInfoResponse leverageInfoResponse = gson.fromJson(json, LeverageInfoResponse.class);
List<MEXCLeverageInfoItem> items = leverageInfoResponse.data();
MEXCLeverageInfoItem result = null;
for(MEXCLeverageInfoItem item : items) {
if(positionType == item.positionType()) {
result = item;
break;
}
}
return result;
}
private final MEXCFuturesPlugin mexFuturesPlugin;
private final Gson gson;
}
@@ -0,0 +1,42 @@
package crypto.r35157.cauldron.afets.leverage;
import com.google.gson.*;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Type;
import java.math.BigDecimal;
public class MEXCLeverageInfoDeserializer implements JsonDeserializer<MEXCLeverageInfoItem> {
@Override
public MEXCLeverageInfoItem deserialize(
@NotNull JsonElement json,
@NotNull Type typeOfT,
@NotNull JsonDeserializationContext context
) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
int level = jsonObject.get("level").getAsInt();
int maxVol = jsonObject.get("maxVol").getAsInt();
BigDecimal mmr = jsonObject.get("mmr").getAsBigDecimal();
BigDecimal imr = jsonObject.get("imr").getAsBigDecimal();
int positionType = jsonObject.get("positionType").getAsInt();
int openType = jsonObject.get("openType").getAsInt();
int leverage = jsonObject.get("leverage").getAsInt();
boolean limitBySys = jsonObject.get("limitBySys").getAsBoolean();
BigDecimal currentMmr = jsonObject.get("currentMmr").getAsBigDecimal();
MEXCLeverageInfoItem item = new MEXCLeverageInfoItem(
level,
maxVol,
mmr,
imr,
positionType,
openType,
leverage,
limitBySys,
currentMmr
);
return item;
}
}
@@ -0,0 +1,15 @@
package crypto.r35157.cauldron.afets.leverage;
import java.math.BigDecimal;
public record MEXCLeverageInfoItem(
int level,
int maxVol,
BigDecimal mmr,
BigDecimal imr,
int positionType,
int openType,
int leverage,
boolean limitBySys,
BigDecimal currentMmr
) {}
@@ -0,0 +1,188 @@
package crypto.r35157.cauldron.afets.nenjim;
import crypto.r35157.cauldron.afets.*;
import crypto.r35157.cauldron.afets.accountdetails.MEXCAccountDetailsService;
import crypto.r35157.cauldron.afets.accountdetails.MEXCAccountDetailsServiceImpl;
import crypto.r35157.cauldron.afets.accountdetails.RetryingMEXCAccountDetailsServiceDecorator;
import crypto.r35157.cauldron.afets.contractdetails.MEXCContractDetails;
import crypto.r35157.cauldron.afets.fundingrate.MEXCFundingRates;
import crypto.r35157.cauldron.afets.gami.GlobalAssetMetadataIndex;
import crypto.r35157.cauldron.afets.leverage.MEXCLeverageInfo;
import crypto.r35157.cauldron.afets.ticker.MEXCTicker;
import crypto.r35157.nenjim.DockerSwarmSecrets;
import crypto.r35157.nenjim.ImmutableProperties;
import crypto.r35157.nenjim.EncryptedFileProperties;
import crypto.r35157.nenjim.SystemEnvironmentProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class NenjimObjectRepository {
public NenjimObjectRepository() throws Exception {
ImmutableProperties sysEnvProperties = new SystemEnvironmentProperties();
evelynHomeDir = sysEnvProperties.getProperty("EVELYN_HOME");
log.info("Evelyn home directory: " + evelynHomeDir);
if(evelynHomeDir == null) {
log.error("ERROR: EVELYN_HOME environment variable is empty - set it to the directory where the evelyn configuration files are located!");
System.exit(1); // TODO: This is incorrect as it takes down the whole Nenjim system!!!
}
evelynConfigDir = evelynHomeDir + "/conf";
evelynGamiFilename = evelynConfigDir + "/GlobalAssetMetadataIndex.conf";
evelynConfigFilename = evelynConfigDir + "/Evelyn.conf";
evelynTmpDir = evelynHomeDir + "/tmp";
evelynTimeoutFilename = evelynTmpDir + "/Timeouts.tmp";
ImmutableProperties dockerSwarmSecrets = new DockerSwarmSecrets();
String nenjimMasterPassword = null;
try {
nenjimMasterPassword = dockerSwarmSecrets.getProperty("nenjim-secrets-password");
if(nenjimMasterPassword == null) {
log.error("ERROR: Nenjim master password NOT set in Docker Swarm (key: 'nenjim-secrets-password')!");
System.exit(1); // TODO: This is incorrect as it takes down the whole Nenjim system!!!
}
} catch (IOException e) {
log.error("ERROR: " + e.getMessage());
System.exit(1);
}
String secretsFile = evelynHomeDir + "/conf/secrets.enc";
log.info("Secrets file: " + secretsFile);
secrets = new EncryptedFileProperties(secretsFile, nenjimMasterPassword);
log.info("Entries in secret file found: " + secrets.getSize());
String pushOverTokenEvelyn = secrets.getProperty("notifier_pushover_token_evelyn");
String pushOverUser = secrets.getProperty("notifier_pushover_user");
pushNotifier = new PushNotifier(pushOverTokenEvelyn, pushOverUser);
gami = new GlobalAssetMetadataIndex(evelynGamiFilename);
ticker = new MEXCTicker();
contractDetails = new MEXCContractDetails();
String mexcAccessKey = secrets.getProperty("assetaz_exchange_mexc_access_key");
String mexcSecretKey = secrets.getProperty("assetaz_exchange_mexc_secret_key");
mexcSpotPlugin = new MEXCSpotPlugin(mexcAccessKey, mexcSecretKey);
configuration = new Configuration(evelynConfigFilename);
configuration.update();
timeOuts = new TimeOuts(evelynTimeoutFilename, configuration.getTimeOut());
String positionsFilename = evelynHomeDir + "/conf/Positions.conf";
portfolio = new Portfolio(pushNotifier, positionsFilename, gami, mexcSpotPlugin, contractDetails);
String mexcUserAgent = secrets.getProperty("assetaz_exchange_mexc_useragent");
String mexcWebKey = secrets.getProperty("assetaz_exchange_mexc_webkey");
mexcFuturesPlugin = new MEXCFuturesPlugin(mexcWebKey, mexcUserAgent);
mexcFundingRates = new MEXCFundingRates();
mexcLeverageInfo = new MEXCLeverageInfo(mexcFuturesPlugin);
scoreCalculator = new ScoreCalculatorImpl();
earningsStatistics = new EarningsStatistics();
MEXCAccountDetailsService realAccountDetailsService = new MEXCAccountDetailsServiceImpl(mexcFuturesPlugin, "USDT");
accountDetails = new RetryingMEXCAccountDetailsServiceDecorator(
mexcFuturesPlugin,
realAccountDetailsService,
3,
secrets,
3000
);
try {
eventWatcher = new EventWatcher(Thread.currentThread(), evelynTmpDir);
eventWatcher.start();
} catch(Exception e) {
log.error("ERROR: " + e.getMessage());
System.exit(1);
}
}
public Configuration getConfiguration() {
return configuration;
}
public MEXCAccountDetailsService getAccountDetails() {
return accountDetails;
}
public EarningsStatistics getEarningsStatistics() {
return earningsStatistics;
}
public GlobalAssetMetadataIndex getGlobalAssetMetadataIndex() {
return gami;
}
public MEXCTicker getTicker() {
return ticker;
}
public MEXCContractDetails getContractDetails() {
return contractDetails;
}
public MEXCSpotPlugin getMEXCSpotPlugin() {
return mexcSpotPlugin;
}
public TimeOuts getTimeOuts() {
return timeOuts;
}
public Portfolio getPortfolio() {
return portfolio;
}
public MEXCFuturesPlugin getMexcFuturesPlugin() {
return mexcFuturesPlugin;
}
public MEXCFundingRates getMexcFundingRates() {
return mexcFundingRates;
}
public MEXCLeverageInfo getMexcLeverageInfo() {
return mexcLeverageInfo;
}
public ScoreCalculator getScoreCalculator() {
return scoreCalculator;
}
public PushNotifier getPushNotifier() {
return pushNotifier;
}
public EncryptedFileProperties getSecrets() {
return secrets;
}
private static final Logger log = LoggerFactory.getLogger(NenjimObjectRepository.class);
private final EncryptedFileProperties secrets;
private final GlobalAssetMetadataIndex gami;
private final MEXCTicker ticker;
private final MEXCContractDetails contractDetails;
private final MEXCSpotPlugin mexcSpotPlugin;
private final TimeOuts timeOuts;
private final Portfolio portfolio;
private final MEXCFuturesPlugin mexcFuturesPlugin;
private final MEXCFundingRates mexcFundingRates;
private final MEXCLeverageInfo mexcLeverageInfo;
private final ScoreCalculator scoreCalculator;
private final PushNotifier pushNotifier;
private final EarningsStatistics earningsStatistics;
private final MEXCAccountDetailsService accountDetails;
private final Configuration configuration;
private final String evelynHomeDir;
private final String evelynConfigDir;
private final String evelynGamiFilename;
private final String evelynConfigFilename;
private final String evelynTimeoutFilename;
private final String evelynTmpDir;
private EventWatcher eventWatcher = null;
}
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets.ticker;
import java.util.List;
public record FuturesTickerResponse(
boolean success,
int code,
List<MEXCTickerItem> data
) { }
@@ -0,0 +1,58 @@
package crypto.r35157.cauldron.afets.ticker;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MEXCTicker {
public MEXCTicker() {
tickerMap = new HashMap<>();
client = HttpClient.newHttpClient();
gson = new GsonBuilder()
.registerTypeAdapter(MEXCTickerItem.class, new MEXCTickerDeserializer())
.create();
request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.GET()
.build();
}
public void updateAll() throws Exception {
HttpResponse<String> httpResp = client.send(request, HttpResponse.BodyHandlers.ofString());
String tickerJson = httpResp.body();
FuturesTickerResponse tickerResp = gson.fromJson(tickerJson, FuturesTickerResponse.class);
List<MEXCTickerItem> tickerItems = tickerResp.data();
for (MEXCTickerItem item : tickerItems) {
tickerMap.put(item.symbol(), item);
}
}
public @Nullable MEXCTickerItem getPrice(@NotNull String symbol) {
MEXCTickerItem item = tickerMap.get(symbol);
return item;
}
public Set<String> getAllSymbols() {
return tickerMap.keySet();
}
private static final String API_URL = "https://contract.mexc.com/api/v1/contract/ticker";
private final HttpClient client;
private final HttpRequest request;
private final Map<String, MEXCTickerItem> tickerMap;
private final Gson gson;
}
@@ -0,0 +1,31 @@
package crypto.r35157.cauldron.afets.ticker;
import com.google.gson.*;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Type;
import java.math.BigDecimal;
public class MEXCTickerDeserializer implements JsonDeserializer<MEXCTickerItem> {
@Override
public MEXCTickerItem deserialize(
@NotNull JsonElement json,
@NotNull Type typeOfT,
@NotNull JsonDeserializationContext context
) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String symbol = jsonObject.get("symbol").getAsString();
BigDecimal currentPrice = jsonObject.get("lastPrice").getAsBigDecimal();
long now = System.currentTimeMillis();
MEXCTickerItem ticker = new MEXCTickerItem(
symbol,
currentPrice,
now
);
return ticker;
}
}
@@ -0,0 +1,11 @@
package crypto.r35157.cauldron.afets.ticker;
import org.jetbrains.annotations.NotNull;
import java.math.BigDecimal;
public record MEXCTickerItem(
@NotNull String symbol,
@NotNull BigDecimal lastPrice,
long timestamp
) {}
@@ -0,0 +1,36 @@
package crypto.r35157.cauldron.afets.ticker;
import java.util.List;
public record PositionItem(
long positionId,
String symbol,
int positionType, // 1=Long, 2=Short
int openType,
int state,
double holdVol,
double frozenVol,
double closeVol,
double holdAvgPrice,
String holdAvgPriceFullyScale,
double openAvgPrice,
String openAvgPriceFullyScale,
double closeAvgPrice,
double liquidatePrice,
double oim,
double im,
double holdFee,
double realised,
int leverage,
double marginRatio,
long createTime,
long updateTime,
boolean autoAddIm,
int version,
double profitRatio,
double newOpenAvgPrice,
double newCloseAvgPrice,
double closeProfitLoss,
double fee,
List<Object> deductFeeList
) {}
@@ -0,0 +1,9 @@
package crypto.r35157.cauldron.afets.ticker;
import java.util.List;
public record PositionResponse(
boolean success,
int code,
List<PositionItem> data
) {}
@@ -0,0 +1,41 @@
package crypto.r35157.nenjim;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class DirectoryClassLoader extends ClassLoader {
private final Path rootDir;
private final Map<String, Class<?>> loadedClasses = new HashMap<>();
public DirectoryClassLoader(Path rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (loadedClasses.containsKey(name)) {
return loadedClasses.get(name);
}
String relativePath = name.replace('.', '/') + ".class";
File classFile = rootDir.resolve(relativePath).toFile();
if (!classFile.exists()) {
throw new ClassNotFoundException("Class file not found: " + classFile.getAbsolutePath());
}
try (FileInputStream fis = new FileInputStream(classFile)) {
byte[] bytes = fis.readAllBytes();
Class<?> clazz = defineClass(name, bytes, 0, bytes.length);
loadedClasses.put(name, clazz);
return clazz;
} catch (IOException e) {
throw new ClassNotFoundException("Failed to load class: " + name, e);
}
}
}
@@ -0,0 +1,56 @@
package crypto.r35157.nenjim;
import java.io.IOException;
import java.nio.file.*;
public class DockerSwarmSecrets implements ImmutableProperties {
public DockerSwarmSecrets() {
refresh();
}
@Override
public String getProperty(String key) throws IOException {
Path dir = Paths.get(BASE_DIR);
if(!Files.exists(dir)) {
throw new IOException(ERROR_DIR_PREFIX + "do not exist!");
}
if(!Files.isDirectory(dir)) {
throw new IOException(ERROR_DIR_PREFIX + "is not a directory!");
}
if(!Files.isReadable(dir)) {
throw new IOException(ERROR_DIR_PREFIX + "is not readable!");
}
Path file = Paths.get(BASE_DIR, key);
if(!Files.exists(file)) {
throw new IOException(ERROR_FILE_PREFIX + file + "' do not exist!");
}
if(Files.isDirectory(file)) {
throw new IOException(ERROR_FILE_PREFIX + file + "' is not a file!");
}
if(!Files.isReadable(file)) {
throw new IOException(ERROR_FILE_PREFIX + file + "' is not readable!");
}
String value = Files.readString(file).trim();
return value;
}
@Override
public void refresh() {}
@Override
public int getSize() {
return -1;
}
private static final String BASE_DIR = "/run/secrets/";
private static final String ERROR_DIR_PREFIX = "Docker Swarm Secrets directory '" + BASE_DIR + "' ";
private static final String ERROR_FILE_PREFIX = "Docker Swarm Secrets file '";
}
@@ -0,0 +1,60 @@
package crypto.r35157.nenjim;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
public class EncryptedFileProperties implements ImmutableProperties {
public EncryptedFileProperties(String encFilePath, String password) throws IOException {
this.encFilePath = encFilePath;
this.password = password;
refresh();
}
@Override
public String getProperty(String key) {
return secretsMap.get(key);
}
@Override
public void refresh() throws IOException {
log.info("Refreshing encrypted secrets file (" + encFilePath + ")...");
// TODO: This is not too pretty as the execution results in some warnings which will also get included in the map!
ProcessBuilder pb = new ProcessBuilder(
"openssl", "enc", "-d", "-aes-256-cbc", "-in", encFilePath, "-pass", "pass:" + password
);
pb.redirectErrorStream(true);
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(":", 2);
if (parts.length == 2) {
secretsMap.put(parts[0].trim(), parts[1].trim());
}
}
}
try {
process.waitFor();
} catch (Exception e) {
log.error("ERROR refreshing secrets file: " + e.getMessage());
throw new IOException(e);
}
log.info("Finished refreshing secrets file! Found " + secretsMap.size() + " entries.");
}
@Override
public int getSize() {
return secretsMap.size();
}
private static final Logger log = LoggerFactory.getLogger(EncryptedFileProperties.class);
private Map<String, String> secretsMap = new HashMap<>();
private String encFilePath;
private String password;
}
@@ -0,0 +1,9 @@
package crypto.r35157.nenjim;
import java.io.IOException;
public interface ImmutableProperties {
String getProperty(String key) throws IOException;
void refresh() throws IOException;
int getSize();
}
@@ -0,0 +1,5 @@
package crypto.r35157.nenjim;
public class NenjimCLIAdminClient {
private NenjimHubClient hubClient;
}
@@ -0,0 +1,4 @@
package crypto.r35157.nenjim;
public interface NenjimContext {
}
@@ -0,0 +1,25 @@
package crypto.r35157.nenjim;
import java.util.HashMap;
public interface NenjimHub {
/**
* Starts a process based on the provided fully qualified interface name.
* The actual implementation that is run is binded according to the Context.
*
* @param fqInterfaceName the fully qualified interface name of the process to start. The interface must extend the {@code NenjimProcess} interface.
*/
void startProcess(String fqInterfaceName);
/**
* Provides a map of all running processes managed by the NenjimHub, keyed by the processId.
*
* @return a map of {@code NenjimProcess} running processes
*/
HashMap<Integer, NenjimProcess> getRunningProcesses();
/**
* A no-operation (noop). This method does nothing.
*/
void noop();
}
@@ -0,0 +1,4 @@
package crypto.r35157.nenjim;
public interface NenjimHubClient {
}
@@ -0,0 +1,93 @@
package crypto.r35157.nenjim;
import java.util.HashMap;
//import java.util.concurrent.StructuredTaskScope;
public class NenjimHubImpl implements NenjimHub {
public static void main() throws Exception {
new NenjimHubImpl();
}
public NenjimHubImpl() throws Exception {
System.out.println("Initializing NenjimHub...");
nextProcessId = 1;
//processesScope = new StructuredTaskScope.ShutdownOnFailure();
processes = new HashMap<>();
System.out.println("Starting autorun processes:");
startAutoRunProcesses();
waitForAndShutdown();
}
private void waitForAndShutdown() {
System.out.println("Done - Now online!");
/*
try {
processesScope.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
processesScope.close();
*/
System.out.println("Nenjim is now shutdown (stopped all processes)!");
}
private void startAutoRunProcesses() throws Exception {
String[] processesToAutoStart = {
"crypto.r35157.nenjim.NenjimHubSocketAdminAdapter",
"crypto.r35157.nenjim.NenjimHubRestAdminAdapter",
"crypto.r35157.nenjim.NenjimHubRPCAdminAdapter",
"crypto.r35157.nenjim.SuwimoHub",
"crypto.r35157.nenjim.SodaTaskManager",
"crypto.r35157.assetaz.hub.AssetAZHub"
};
for (String processInterfaceName : processesToAutoStart) {
startProcess(processInterfaceName);
}
}
@Override
public void startProcess(String className) {
ClassLoader loader = ClassLoader.getSystemClassLoader();
/*processesScope.fork(() -> {
Class<?> clazz = loader.loadClass(className);
if (!NenjimProcess.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("'" + className + "' does NOT implement NenjimProcess");
}
NenjimProcess proc = (NenjimProcess) clazz.getDeclaredConstructor().newInstance();
processes.put(nextProcessId, proc);
nextProcessId++;
NenjimContext context = new NenjimContext() { };
proc.setNenjimProcessContext(context);
proc.setNenjimHub(this);
Thread.currentThread().setName(proc.getProcessName());
System.out.println(" Starter Nenjim process '" + proc.getProcessName() + "'");
proc.run();
return null;
});*/
}
@Override
public HashMap<Integer, NenjimProcess> getRunningProcesses() {
return processes;
}
@Override
public void noop() {
System.out.println("NenjimHub command: 'noop'");
}
private HashMap<Integer, NenjimProcess> processes;
//private StructuredTaskScope.ShutdownOnFailure processesScope;
private int nextProcessId;
}
@@ -0,0 +1,24 @@
package crypto.r35157.nenjim;
public class NenjimHubRPCAdminAdapter implements NenjimProcess {
@Override
public void run() throws Exception {
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub hub) {
}
@Override
public String getProcessName() {
return "NenjimHub RPC Admin Adapter";
}
private NenjimContext nenjimContext;
}
@@ -0,0 +1,4 @@
package crypto.r35157.nenjim;
public class NenjimHubRPCAdminBridge {
}
@@ -0,0 +1,23 @@
package crypto.r35157.nenjim;
public class NenjimHubRestAdminAdapter implements NenjimProcess {
@Override
public void run() throws Exception {
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub hub) {
}
@Override
public String getProcessName() {
return "NenjimHub Rest Admin Adapter";
}
private NenjimContext nenjimContext;
}
@@ -0,0 +1,4 @@
package crypto.r35157.nenjim;
public class NenjimHubRestAdminBridge {
}
@@ -0,0 +1,116 @@
package crypto.r35157.nenjim;
import crypto.r35157.RingBuffer;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.List;
public class NenjimHubSocketAdminAdapter implements NenjimProcess {
public NenjimHubSocketAdminAdapter() {
this.portNumber = SOCKET_ADMIN_PORT;
}
@Override
public void run() throws Exception {
try (ServerSocket server = new ServerSocket(portNumber)) {
System.out.println("NenjimHub Socket Admin Client ready and listening on port: " + portNumber);
while (true) {
try (
Socket client = server.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(
client.getOutputStream(), true) // autoFlush = true
) {
boolean doContinue = true;
System.out.println("Connection received from: "
+ client.getRemoteSocketAddress());
out.println("NenjimHub 0.1 r35157_reference_implementation 0.0.0.1");
out.println("OK");
out.flush();
while (doContinue) {
String line = in.readLine();
if (line.trim().isEmpty()) {
out.println("OK");
} else {
long start = System.currentTimeMillis();
String command = line.split(" ")[0].toUpperCase();
switch (command) {
case "START":
String fqInterfaceName = line.split(" ")[1];
nenjimHub.startProcess(fqInterfaceName);
break;
case "PS":
HashMap<Integer, NenjimProcess> processesMap = nenjimHub.getRunningProcesses();
for(int processId : processesMap.keySet()) {
NenjimProcess process = processesMap.get(processId);
out.println(
processId
+ " '" + process.getProcessName() + "'"
+ " " + process.getClass().getName()
);
}
break;
case "NOOP":
nenjimHub.noop();
break;
case "QUIT":
doContinue = false;
System.out.println("Connection closed by client");
break;
case "HELP":
out.println("START <fqInterfaceName> - Start a new process using the given fully qualified interface name");
out.println("PS - Show running processes");
out.println("NOOP - No Operation");
out.println("QUIT - Quit the session");
out.println("HELP - This list of commands");
break;
default:
out.println("Error: 400 - Unknown command");
break;
}
out.println("OK (" + (System.currentTimeMillis() - start) + "ms)");
}
}
}
System.out.println("Session ended!");
}
}
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub nenjimHub) {
this.nenjimHub = nenjimHub;
}
@Override
public String getProcessName() {
return "NenjimHub Socket Admin Adapter";
}
private static final int SOCKET_ADMIN_PORT = 38010;
private static final int RING_BUFFER_CAPACITY = 25;
private NenjimContext nenjimContext;
private NenjimHub nenjimHub;
private final int portNumber;
}
@@ -0,0 +1,4 @@
package crypto.r35157.nenjim;
public class NenjimHubSocketAdminBridge {
}
@@ -0,0 +1,74 @@
package crypto.r35157.nenjim;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class NenjimHubSocketAdminClient implements NenjimProcess {
public NenjimHubSocketAdminClient() {
this.portNumber = SOCKET_ADMIN_PORT;
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub hub) {
}
@Override
public String getProcessName() {
return "NenjimHub Socket Admin Client";
}
@Override
public void run() throws Exception {
try (ServerSocket server = new ServerSocket(portNumber)) {
System.out.println("NenjimHub Socket Admin Client ready and listening on port: " + portNumber);
while (true) {
try (
Socket client = server.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(
client.getOutputStream(), true) // autoFlush = true
) {
System.out.println("Connection received from: "
+ client.getRemoteSocketAddress());
out.println("Protocol: NenjimAdmin-0.1");
out.println("OK");
out.flush();
while(true) {
String line = in.readLine();
if(line.trim().isEmpty()) {
out.println("OK");
} else if (line.toUpperCase().startsWith("NOOP")) {
long start = System.currentTimeMillis();
//nenjimContext.noOp();
out.println("OK (" + (System.currentTimeMillis() - start) + "ms)");
} else if ("QUIT".equalsIgnoreCase(line)) {
out.println("OK");
System.out.println("Connection closed by client");
break;
} else {
out.println("Error: 400 - Unknown command");
}
}
System.out.println("Session ended!");
}
}
}
}
private static final int SOCKET_ADMIN_PORT = 38010;
private final int portNumber;
private NenjimContext nenjimContext;
}
@@ -0,0 +1,10 @@
package crypto.r35157.nenjim;
public interface NenjimProcess {
void run() throws Exception;
void setNenjimProcessContext(NenjimContext context);
void setNenjimHub(NenjimHub hub);
String getProcessName();
}
@@ -0,0 +1,245 @@
package crypto.r35157.nenjim;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
//import org.junit.jupiter.api.Disabled;
//import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public class NenjimTestTool extends Application implements NenjimProcess {
@Override
public void run() throws Exception {
launch();
}
public static class FileNode {
private final SimpleStringProperty name;
private final SimpleLongProperty size;
private final SimpleStringProperty type;
private final BooleanProperty selected = new SimpleBooleanProperty(false);
public FileNode(String name, long size, String type) {
this.name = new SimpleStringProperty(name);
this.size = new SimpleLongProperty(size);
this.type = new SimpleStringProperty(type);
}
public SimpleStringProperty nameProperty() { return name; }
public SimpleLongProperty sizeProperty() { return size; }
public SimpleStringProperty typeProperty() { return type; }
public BooleanProperty selectedProperty() { return selected; }
}
@Override
public void stop() throws Exception {
super.stop();
}
@Override
public void start(Stage primaryStage) {
// When the user clicks the window “X”, tear down JavaFX only:
primaryStage.setOnCloseRequest(event -> {
// triggers Application.stop() and shuts down FX machinery
Platform.exit();
// → no System.exit(): the rest of your Nenjin VM keeps running
});
// Load icons
Image folderIcon = new Image(getClass().getResourceAsStream("/icons/folder.png"));
Image fileIcon = new Image(getClass().getResourceAsStream("/icons/file.png"));
// Build tree items
TreeItem<FileNode> root = new TreeItem<>(new FileNode("Projekt", 0, "Folder"));
root.setExpanded(true);
TreeItem<FileNode> src = new TreeItem<>(new FileNode("src", 0, "Folder"));
src.getChildren().add(new TreeItem<>(new FileNode("Main.java", 15000, "File")));
src.getChildren().add(new TreeItem<>(new FileNode("Utils.java", 8200, "File")));
root.getChildren().add(src);
TreeItem<FileNode> res = new TreeItem<>(new FileNode("resources", 0, "Folder"));
res.getChildren().add(new TreeItem<>(new FileNode("icon.png", 2500, "File")));
root.getChildren().add(res);
// Create TreeTableView
TreeTableView<FileNode> treeTable = new TreeTableView<>(root);
treeTable.setShowRoot(true);
// Name column with tri-state checkbox and icon
TreeTableColumn<FileNode, String> nameCol = new TreeTableColumn<>("Name");
nameCol.setCellValueFactory(param -> param.getValue().getValue().nameProperty());
nameCol.setPrefWidth(300);
nameCol.setCellFactory(col -> new TreeTableCell<FileNode, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
TreeItem<FileNode> treeItem = getTreeTableRow().getTreeItem();
FileNode node = treeItem.getValue();
// Create checkbox
CheckBox checkBox = new CheckBox();
checkBox.setAllowIndeterminate(true);
// Bind selected
checkBox.selectedProperty().bindBidirectional(node.selectedProperty());
// Bind indeterminate to partial selection of children
Observable[] childProps = treeItem.getChildren().stream()
.map(child -> (Observable) child.getValue().selectedProperty())
.toArray(Observable[]::new);
checkBox.indeterminateProperty().bind(Bindings.createBooleanBinding(() -> {
boolean any = treeItem.getChildren().stream()
.anyMatch(child -> child.getValue().selectedProperty().get());
boolean all = treeItem.getChildren().stream()
.allMatch(child -> child.getValue().selectedProperty().get());
return any && !all;
}, childProps));
// Propagate clicks to children
checkBox.addEventHandler(ActionEvent.ACTION, e -> {
if (!checkBox.isIndeterminate()) {
treeItem.getChildren().forEach(child ->
child.getValue().selectedProperty().set(checkBox.isSelected())
);
}
});
// Create icon
ImageView iv = new ImageView(
node.typeProperty().get().equals("Folder") ? folderIcon : fileIcon
);
iv.setFitHeight(16);
iv.setFitWidth(16);
// Layout
HBox container = new HBox(5, checkBox, iv);
setText(item);
setGraphic(container);
}
}
});
// Size column
TreeTableColumn<FileNode, Number> sizeCol = new TreeTableColumn<>("Size (bytes)");
sizeCol.setCellValueFactory(param -> param.getValue().getValue().sizeProperty());
sizeCol.setPrefWidth(120);
// Type column
TreeTableColumn<FileNode, String> typeCol = new TreeTableColumn<>("Type");
typeCol.setCellValueFactory(param -> param.getValue().getValue().typeProperty());
typeCol.setPrefWidth(100);
treeTable.getColumns().setAll(nameCol, sizeCol, typeCol);
Button runButton = new Button("Execute Tests");
runButton.setOnAction(event -> runTests());
// Show scene
StackPane stackPane = new StackPane(treeTable);
BorderPane rootPane = new BorderPane();
rootPane.setTop(stackPane);
rootPane.setBottom(runButton);
Scene scene = new Scene(rootPane, 700, 450);
primaryStage.setTitle("Nenjim Test Tool");
primaryStage.setScene(scene);
primaryStage.show();
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub hub) {
}
@Override
public String getProcessName() {
return "Nenjim Test Tool";
}
private void runTests() {
// Map over projekter og deres konfiguration
Map<String, ProjectConfig> projects = Map.of(
"NenjimHub", new ProjectConfig(
Path.of("/home/minimons/projects/nenjim_testtool-tests/build/classes/java/test"),
List.of(
"crypto.r35157.nenjim.testtool.TestsTOC"
)
)
);
for (Map.Entry<String, ProjectConfig> entry : projects.entrySet()) {
String projectName = entry.getKey();
ProjectConfig config = entry.getValue();
System.out.println("===> Projekt: " + projectName);
DirectoryClassLoader loader = new DirectoryClassLoader(config.classDir);
for (String className : config.testClasses) {
System.out.println("--> Tester klasse: " + className);
try {
Class<?> clazz = loader.loadClass(className);
// TODO: Renable when import is fixed
/*Disabled disabledClass = clazz.getAnnotation(Disabled.class);
if (disabledClass != null) {
String reason = disabledClass.value();
System.out.println("\uD83D\uDEAB --> Klasse '" + className + "' disabled! (Reason: " + reason + ")");
continue;
}*/
Object instance = clazz.getDeclaredConstructor().newInstance();
// TODO: Renable when import is fixed
/*
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Test.class)) {
Disabled disabledTest = method.getAnnotation(Disabled.class);
String methodName = method.getName();
if(disabledTest != null) {
String reason = disabledTest.value();
System.out.println("\uD83D\uDEAB --> Test '" + methodName + "' disabled! (Reason: " + reason + ")");
continue;
}
System.out.print(" Kører test: '" + methodName + "' ... ");
try {
method.setAccessible(true);
method.invoke(instance);
System.out.println("✅");
} catch (Throwable t) {
System.out.println("❌ Fejl: " + t.getCause());
}
}
}*/
} catch (Exception e) {
System.out.println("🚫 Kunne ikke loade klasse: " + e.getMessage());
e.printStackTrace();
}
}
}
}
private NenjimContext nenjimContext;
}
@@ -0,0 +1,14 @@
package crypto.r35157.nenjim;
import java.nio.file.Path;
import java.util.List;
public class ProjectConfig {
public final Path classDir;
public final List<String> testClasses;
public ProjectConfig(Path classDir, List<String> testClasses) {
this.classDir = classDir;
this.testClasses = testClasses;
}
}
@@ -0,0 +1,25 @@
package crypto.r35157.nenjim;
import java.util.concurrent.CountDownLatch;
public class SodaTaskManager implements NenjimProcess {
@Override
public void run() throws Exception {
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub hub) {
}
@Override
public String getProcessName() {
return "Soda Task Manager";
}
private NenjimContext nenjimContext;
}
@@ -0,0 +1,23 @@
package crypto.r35157.nenjim;
public class SuwimoHub implements NenjimProcess {
@Override
public void run() throws Exception {
}
@Override
public void setNenjimProcessContext(NenjimContext context) {
this.nenjimContext = context;
}
@Override
public void setNenjimHub(NenjimHub hub) {
}
@Override
public String getProcessName() {
return "Suwimo Hub";
}
private NenjimContext nenjimContext;
}
@@ -0,0 +1,27 @@
package crypto.r35157.nenjim;
public class SystemEnvironmentProperties implements ImmutableProperties {
public SystemEnvironmentProperties() {
refresh();
}
@Override
public String getProperty(String key) {
String value = System.getenv(key);
if(value == null) {
return null;
}
value = value.trim();
return value;
}
@Override
public int getSize() {
return -1;
}
@Override
public void refresh() {}
}
@@ -0,0 +1,209 @@
package crypto.r35157.nenjim.scorevisualizer;
import crypto.r35157.cauldron.afets.PositionType;
import crypto.r35157.cauldron.afets.ScoreCalculator;
import crypto.r35157.cauldron.afets.ScoreCalculatorImpl;
import crypto.r35157.cauldron.afets.ScoreCalculatorInput;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class ScoreVisualizerApp extends Application {
public ScoreCalculator scoreCalculator = new ScoreCalculatorImpl();
@Override
public void start(Stage stage) {
// CHART (top)
NumberAxis xAxis = new NumberAxis();
xAxis.setLabel("Point");
NumberAxis yAxis = new NumberAxis();
// xAxis name set dynamically later
LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
chart.setTitle("Evelyn Score Visualization");
chart.setLegendVisible(false);
XYChart.Series<Number, Number> scoreSeries = new XYChart.Series<>();
chart.getData().add(scoreSeries);
// CONTROLS (bottom)
GridPane controls = new GridPane();
controls.setHgap(10);
controls.setVgap(8);
controls.setPadding(new Insets(10, 10, 10, 10));
Label pointsLabel = new Label("Points");
TextField pointsField = new TextField("30");
Label currentTokenBaseNotationPriceLabel = new Label("CurrentTokenBaseNotationPrice");
TextField currentTokenBaseNotationPriceFrom = new TextField("0.01");
TextField currentTokenBaseNotationPriceTo = new TextField("0.1");
Label futureTokenBaseNotationPriceLabel = new Label("FutureTokenBaseNotationPrice");
TextField futureTokenBaseNotationPriceFrom = new TextField("0.1");
TextField futureTokenBaseNotationPriceTo = new TextField("2.0");
Label currentTokenNotationPriceLabel = new Label("CurrentTokenNotationPrice");
TextField currentTokenNotationPriceFrom = new TextField("0.01");
TextField currentTokenNotationPriceTo = new TextField("0.1");
Label amountNotationalToSpendLabel = new Label("AmountNotationalToSpend");
TextField amountNotationalToSpendFrom = new TextField("0");
TextField amountNotationalToSpendTo = new TextField("100");
Label totalNumberOfPositionsLabel = new Label("TotalNumberOfPositions");
TextField totalNumberOfPositionsFrom = new TextField("0.01");
TextField totalNumberOfPositionsTo = new TextField("2.0");
Label contractSizeLabel = new Label("ContractSize");
TextField contractSizeFrom = new TextField("1.0");
TextField contractSizeTo = new TextField("2.0");
Label leverageLabel = new Label("Leverage");
TextField leverageFrom = new TextField("1.0");
TextField leverageTo = new TextField("2.0");
Label averagePositionValueLabel = new Label("AveragePositionValue");
TextField averagePositionValueFrom = new TextField("1.0");
TextField averagePositionValueTo = new TextField("2.0");
Label tradeTypeLabel = new Label("TradeType");
ComboBox<PositionType> tradeTypeCombo = new ComboBox<>();
tradeTypeCombo.getItems().addAll(PositionType.marginLong, PositionType.marginShort);
tradeTypeCombo.setValue(PositionType.marginLong);
Label seriesLabel = new Label("DataSeries");
ComboBox<String> seriesCombo = new ComboBox<>();
seriesCombo.getItems().addAll(
"Score",
currentTokenBaseNotationPriceLabel.getText(),
futureTokenBaseNotationPriceLabel.getText(),
currentTokenNotationPriceLabel.getText(),
amountNotationalToSpendLabel.getText(),
totalNumberOfPositionsLabel.getText(),
contractSizeLabel.getText(),
leverageLabel.getText(),
averagePositionValueLabel.getText()
);
Button updateBtn = new Button("Update Graph");
int row = 0;
controls.add(seriesLabel, 0, row); controls.add(seriesCombo, 1, row); controls.add(updateBtn, 2, row++);
controls.add(pointsLabel, 0, row); controls.add(pointsField, 1, row++);
controls.add(currentTokenBaseNotationPriceLabel, 0, row); controls.add(currentTokenBaseNotationPriceFrom, 1, row); controls.add(currentTokenBaseNotationPriceTo, 2, row++);
controls.add(futureTokenBaseNotationPriceLabel, 0, row); controls.add(futureTokenBaseNotationPriceFrom, 1, row); controls.add(futureTokenBaseNotationPriceTo, 2, row++);
controls.add(currentTokenNotationPriceLabel, 0, row); controls.add(currentTokenNotationPriceFrom, 1, row); controls.add(currentTokenNotationPriceTo, 2, row++);
controls.add(amountNotationalToSpendLabel, 0, row); controls.add(amountNotationalToSpendFrom, 1, row); controls.add(amountNotationalToSpendTo, 2, row++);
controls.add(totalNumberOfPositionsLabel, 0, row); controls.add(totalNumberOfPositionsFrom, 1, row); controls.add(totalNumberOfPositionsTo, 2, row++);
controls.add(contractSizeLabel, 0, row); controls.add(contractSizeFrom, 1, row); controls.add(contractSizeTo, 2, row++);
controls.add(leverageLabel, 0, row); controls.add(leverageFrom, 1, row); controls.add(leverageTo, 2, row++);
controls.add(averagePositionValueLabel, 0, row); controls.add(averagePositionValueFrom, 1, row); controls.add(averagePositionValueTo, 2, row++);
controls.add(tradeTypeLabel, 0, row); controls.add(tradeTypeCombo, 1, row++);
// Layout: VBox (graph on top, controls below)
VBox root = new VBox(chart, controls);
// Action: Update Graph
updateBtn.setOnAction(e -> {
yAxis.setLabel(seriesCombo.getValue());
scoreSeries.getData().clear();
try {
int points = Integer.parseInt(pointsField.getText().trim());
if (points < 2) { points = 2; } // At least two points
BigDecimal currentTokenBaseNotationPriceFromVal = new BigDecimal(currentTokenBaseNotationPriceFrom.getText().trim());
BigDecimal currentTokenBaseNotationPriceToVal = new BigDecimal(currentTokenBaseNotationPriceTo.getText().trim());
BigDecimal futureTokenBaseNotationPriceFromVal = new BigDecimal(futureTokenBaseNotationPriceFrom.getText().trim());
BigDecimal futureTokenBaseNotationPriceToVal = new BigDecimal(futureTokenBaseNotationPriceTo.getText().trim());
BigDecimal currentTokenNotationPriceFromVal = new BigDecimal(currentTokenNotationPriceFrom.getText().trim());
BigDecimal currentTokenNotationPriceToVal = new BigDecimal(currentTokenNotationPriceTo.getText().trim());
BigDecimal amountNotationalToSpendFromVal = new BigDecimal(amountNotationalToSpendFrom.getText().trim());
BigDecimal amountNotationalToSpendToVal = new BigDecimal(amountNotationalToSpendTo.getText().trim());
BigDecimal totalNumberOfPositionsFromVal = new BigDecimal(totalNumberOfPositionsFrom.getText().trim());
BigDecimal totalNumberOfPositionsToVal = new BigDecimal(totalNumberOfPositionsTo.getText().trim());
BigDecimal contractSizeFromVal = new BigDecimal(contractSizeFrom.getText().trim());
BigDecimal contractSizeToVal = new BigDecimal(contractSizeTo.getText().trim());
BigDecimal leverageFromVal = new BigDecimal(leverageFrom.getText().trim());
BigDecimal leverageToVal = new BigDecimal(leverageTo.getText().trim());
BigDecimal averagePositionValueFromVal = new BigDecimal(averagePositionValueFrom.getText().trim());
BigDecimal averagePositionValueToVal = new BigDecimal(averagePositionValueTo.getText().trim());
PositionType type = tradeTypeCombo.getValue();
for (int i = 0; i < points; i++) {
BigDecimal ratio = (points == 1)
? BigDecimal.ZERO
: new BigDecimal(i).divide(new BigDecimal(points - 1), 10, RoundingMode.HALF_UP);
BigDecimal currentTokenBaseNotationPriceVal = interpolate(currentTokenBaseNotationPriceFromVal, currentTokenBaseNotationPriceToVal, ratio);
BigDecimal futureTokenBaseNotationPriceVal = interpolate(futureTokenBaseNotationPriceFromVal, futureTokenBaseNotationPriceToVal, ratio);
BigDecimal currentTokenNotationPriceVal = interpolate(currentTokenNotationPriceFromVal, currentTokenNotationPriceToVal, ratio);
BigDecimal amountNotationalToSpendVal = interpolate(amountNotationalToSpendFromVal, amountNotationalToSpendToVal, ratio);
BigDecimal totalNumberOfPositionsVal = interpolate(totalNumberOfPositionsFromVal, totalNumberOfPositionsToVal, ratio);
BigDecimal contractSizeVal = interpolate(contractSizeFromVal, contractSizeToVal, ratio);
BigDecimal leverageVal = interpolate(leverageFromVal, leverageToVal, ratio);
BigDecimal averagePositionValueVal = interpolate(averagePositionValueFromVal, averagePositionValueToVal, ratio);
try {
double point = switch(seriesCombo.getValue()) {
case "CurrentTokenBaseNotationPrice" -> currentTokenBaseNotationPriceVal.doubleValue();
case "FutureTokenBaseNotationPrice" -> futureTokenBaseNotationPriceVal.doubleValue();
case "CurrentTokenNotationPrice" -> currentTokenNotationPriceVal.doubleValue();
case "AmountNotationalToSpend" -> amountNotationalToSpendVal.doubleValue();
case "TotalNumberOfPositions" -> totalNumberOfPositionsVal.doubleValue();
case "ContractSize" -> contractSizeVal.doubleValue();
case "Leverage" -> leverageVal.doubleValue();
case "AveragePositionValue" -> averagePositionValueVal.doubleValue();
case "Score" -> {
ScoreCalculatorInput input = new ScoreCalculatorInput(
currentTokenBaseNotationPriceVal,
futureTokenBaseNotationPriceVal,
currentTokenNotationPriceVal,
amountNotationalToSpendVal,
totalNumberOfPositionsVal.intValue(),
contractSizeVal,
leverageVal,
type,
averagePositionValueVal);
yield scoreCalculator.calculateScore(input).doubleValue();
}
default -> throw new IllegalStateException("Unexpected value: " + seriesCombo.getValue());
};
scoreSeries.getData().add(new XYChart.Data<>(i + 1, point));
} catch (Exception ex) {
// Skip points with invalid input (e.g., divide by zero)
}
}
} catch (Exception ex) {
// You could show a dialog here if desired
ex.printStackTrace();
}
});
// Window setup
stage.setScene(new Scene(root, 900, 800));
stage.setTitle("Evelyn Score Visualizer");
stage.show();
}
// Helper for linear interpolation between from and to
private static BigDecimal interpolate(BigDecimal from, BigDecimal to, BigDecimal ratio) {
return from.add(to.subtract(from).multiply(ratio));
}
public static void main(String[] args) {
launch(args);
}
}