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 {
|
application {
|
||||||
mainClass.set("com.r35157.nenjim.hubd.impl.ref.Main")
|
mainClass.set("com.r35157.nenjim.hubd.impl.ref.Main")
|
||||||
|
applicationDefaultJvmArgs = listOf("--enable-preview")
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -43,9 +44,13 @@ dependencies {
|
|||||||
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
|
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
|
||||||
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
|
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
|
||||||
|
|
||||||
implementation("org.slf4j:slf4j-api:2.0.18")
|
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
|
||||||
implementation("com.fazecast:jSerialComm:2.11.4")
|
implementation("com.fazecast:jSerialComm:2.11.4")
|
||||||
|
implementation("com.google.code.gson:gson:2.14.0")
|
||||||
|
implementation("commons-codec:commons-codec:1.22.0")
|
||||||
|
implementation("org.apache.commons:commons-collections4:4.5.0")
|
||||||
|
implementation("org.apache.commons:commons-lang3:3.20.0")
|
||||||
|
implementation("org.slf4j:slf4j-api:2.0.18")
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -54,7 +59,15 @@ java {
|
|||||||
|
|
||||||
tasks.withType<JavaCompile>().configureEach {
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
options.release.set(25)
|
options.release.set(25)
|
||||||
options.compilerArgs.addAll(listOf("-Xmaxerrs", "1"))
|
|
||||||
|
options.compilerArgs.addAll(
|
||||||
|
listOf(
|
||||||
|
"--enable-preview",
|
||||||
|
//"-Xlint:deprecation",
|
||||||
|
//"-Xlint:unchecked",
|
||||||
|
"-Xmaxerrs", "1"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val generatedDetagMain = layout.buildDirectory.dir("generated/sources/detag/main/java")
|
val generatedDetagMain = layout.buildDirectory.dir("generated/sources/detag/main/java")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ DEBUG_SUSPEND="${DEBUG_SUSPEND:-y}"
|
|||||||
echo "Starting NenjimHub in DEBUG mode on port ${DEBUG_PORT} (suspend=${DEBUG_SUSPEND})"
|
echo "Starting NenjimHub in DEBUG mode on port ${DEBUG_PORT} (suspend=${DEBUG_SUSPEND})"
|
||||||
|
|
||||||
exec java \
|
exec java \
|
||||||
|
--enable-preview \
|
||||||
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=*:${DEBUG_PORT}" \
|
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=${DEBUG_SUSPEND},address=*:${DEBUG_PORT}" \
|
||||||
-Dlog4j.configurationFile=conf/log4j2.xml \
|
-Dlog4j.configurationFile=conf/log4j2.xml \
|
||||||
-cp "$CLASSPATH" \
|
-cp "$CLASSPATH" \
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ fi
|
|||||||
CLASSPATH=$(IFS=:; echo "${jars[*]}")
|
CLASSPATH=$(IFS=:; echo "${jars[*]}")
|
||||||
|
|
||||||
exec java \
|
exec java \
|
||||||
|
--enable-preview \
|
||||||
-Dlog4j.configurationFile=conf/log4j2.xml \
|
-Dlog4j.configurationFile=conf/log4j2.xml \
|
||||||
-cp "$CLASSPATH" \
|
-cp "$CLASSPATH" \
|
||||||
com.r35157.nenjim.hubd.impl.ref.Main
|
com.r35157.nenjim.hubd.impl.ref.Main
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package crypto.r35157;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RingBuffer {
|
||||||
|
public RingBuffer(int capacity) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.buffer = new ArrayDeque<>(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(String command) {
|
||||||
|
if (getSize() == capacity) {
|
||||||
|
buffer.removeFirst();
|
||||||
|
}
|
||||||
|
buffer.addLast(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAll() {
|
||||||
|
return new ArrayList<>(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return buffer.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCapacity() {
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int capacity;
|
||||||
|
private final Deque<String> buffer;
|
||||||
|
}
|
||||||
@@ -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