sclass MPM { gettable new PositionSet imaginaryPositions; gettable new PositionSet realPositions; settable double cryptoPrice; new DoubleBuffer cryptoPriceLog; // -1, 0 [initial value only] or 1 settable int cryptoDirection; event cryptoDirectionChanged; settable double currentTime; settable double maxAllowedLoss = 1; // in percent Position newShort() { ret new Position().direction(-1); } Position newLong() { ret new Position().direction(1); } abstract class CloseReason {} class LossClose > CloseReason {} class HappyClose > CloseReason {} class RegularClose > CloseReason {} // pullback close class KillClose > CloseReason {} // hard-closed for any reason settable double fees = 0.12; settable double expectedSlippage = .1; // in percent before leverage class Position { settable bool real; // -1 (short) or 1 (long) settable int direction; settable double openingTime; settable double openingPrice; settable double lastPrice; settable double closingTime; settable double closingPrice; simplyCached double openingPriceWithFeesAndSlippage() { ret openingPrice*(1+(fees+expectedSlippage)/100*direction); } settable double relativeValue; settable double maxRelativeValue = negativeInfinity(); settable double minRelativeValue = infinity(); settable double pullbackThreshold; // null when position is open, not null when position was closed settable CloseReason closeReason; PositionSet motherList() { ret real ? realPositions : imaginaryPositions; } void close(CloseReason reason) { closingTime(time()); closingPrice(cryptoPrice); closeReason(reason); motherList().wasClosed(this); } void imagine { real(false); launch(); } void launch { update(cryptoPrice); motherList().add(this); } void update(double cryptoPrice) { lastPrice(cryptoPrice); relativeValue((cryptoPrice/openingPriceWithFeesAndSlippage()-1)*direction*100); maxRelativeValue(max(maxRelativeValue, relativeValue)); minRelativeValue(min(minRelativeValue, relativeValue)); pullbackThreshold(maxRelativeValue-pullback(this)); } toString { ret renderVars(+direction, +openingPrice, +openingPriceWithFeesAndSlippage(), +lastPrice, +relativeValue, +maxRelativeValue); } void autoClose { if (relativeValue < -maxAllowedLoss) ret with close(new LossClose); if (relativeValue >= 0 && relativeValue < pullbackThreshold) ret with close(new RegularClose); } bool isOpen() { ret closeReason == null; } } // end of Position class PositionSet { new LinkedHashSet positionsByDate; new LinkedHashSet openPositions; new LinkedHashSet closedPositions; void add(Position p) { positionsByDate.add(p); (p.isOpen() ? openPositions : closedPositions).add(p); } void remove(Position p) { positionsByDate.remove(p); openPositions.remove(p); closedPositions.remove(p); } void wasClosed(Position p) { openPositions.remove(p); closedPositions.add(p); } } swappable double pullback(Position p) { ret 0.5; } double time() { ret currentTime; } void addTickerSequence(TickerSequence ts) { int n = l(ts); for i to n: newCryptoPrice(ts.prices.get(i), ts.timestamps.get(i)); } void newCryptoPrice(double price) { newCryptoPrice(price, currentTime+1); } void newCryptoPrice(double price, double time) { currentTime(time); // don't store non-changes if (price == cryptoPrice) ret; int direction = sign(cryptoPrice-price); cryptoPriceLog.add(price); cryptoPrice(price); for (p : allOpenPositions()) { p.update(cryptoPrice); p.autoClose(); } if (direction != cryptoDirection) { cryptoDirection(direction); cryptoDirectionChanged(); } } L allPositions() { ret concatLists(realPositions().positionsByDate, imaginaryPositions().positionsByDate); } L allOpenPositions() { ret concatLists(realPositions().openPositions, imaginaryPositions().openPositions); } { init(); } void init { onCryptoDirectionChanged(-> { for (p : ll(newLong(), newShort())) p.openingTime(time()).openingPrice(cryptoPrice).imagine(); }); } void printStatus { print(toString()); } toString { ret combineWithSeparator(" + ", n2(realPositions.positionsByDate, "real position"), n2(imaginaryPositions.positionsByDate, "imaginary position")); } }