Added classes from old Cauldron (NON TESTED / BROKEN!)
This commit is contained in:
+15
-2
@@ -12,6 +12,7 @@ if (version == "UNSET" && gradle.startParameter.taskNames.any { it.startsWith("p
|
||||
|
||||
application {
|
||||
mainClass.set("com.r35157.nenjim.hubd.impl.ref.Main")
|
||||
applicationDefaultJvmArgs = listOf("--enable-preview")
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -43,9 +44,13 @@ dependencies {
|
||||
runtimeOnly("org.apache.logging.log4j:log4j-core: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.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 {
|
||||
@@ -54,7 +59,15 @@ java {
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
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")
|
||||
|
||||
@@ -17,6 +17,7 @@ DEBUG_SUSPEND="${DEBUG_SUSPEND:-y}"
|
||||
echo "Starting NenjimHub in DEBUG mode on port ${DEBUG_PORT} (suspend=${DEBUG_SUSPEND})"
|
||||
|
||||
exec java \
|
||||
--enable-preview \
|
||||
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=*:${DEBUG_PORT}" \
|
||||
-Dlog4j.configurationFile=conf/log4j2.xml \
|
||||
-cp "$CLASSPATH" \
|
||||
|
||||
@@ -12,6 +12,7 @@ fi
|
||||
CLASSPATH=$(IFS=:; echo "${jars[*]}")
|
||||
|
||||
exec java \
|
||||
--enable-preview \
|
||||
-Dlog4j.configurationFile=conf/log4j2.xml \
|
||||
-cp "$CLASSPATH" \
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
) { }
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package crypto.r35157.cauldron.afets.accountdetails;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record AccountDetailsResponse(
|
||||
boolean success,
|
||||
BigDecimal code,
|
||||
AccountDetails data
|
||||
) { }
|
||||
+8
@@ -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;
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
+59
@@ -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
|
||||
) {
|
||||
}
|
||||
+9
@@ -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;
|
||||
}
|
||||
+203
@@ -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;
|
||||
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user