concept Chaser extends G22TradingStrategy { // Juicer parameters // Maximum duration of a position settable double maxPositionHours = 4; // Max loss per position before leverage settable double maxLoss = 0.6; // Ordinary pullback (fixed price percentage) settable double pullback = 0.4; // Relative pullback (percentage of profit) settable double relativePullback; // e.g. 20 settable double minPullback; // e.g. 0.1 settable double maxPullback; // e.g. 4 settable Opener opener; // internal new RunLengthCounter rlc; OpenSignal openSignal; // trigger = 0 to 1 record noeq CloseSignal(S reason, double trigger) { double trigger() { ret trigger; } toString { ret "Close signal " + reason + " (" + iround(trigger*100) + "%)"; } } // trigger = 0 to 1 record noeq OpenSignal(S reason, double trigger, int direction) { double trigger() { ret trigger; } toString { ret "Open signal " + reason + " (" + iround(trigger*100) + "%)"; } } persistable record Juicer(double maxLoss, double pullback) { L closeSignals(Position p) { new L signals; double profit = p.profitBeforeLeverage(); // How close are we to the max closing time? if (p.maxClosingTime() < infinity()) signals.add(new CloseSignal("Age", transformToZeroToOne(currentTime(), p.openingTime(), p.maxClosingTime()))); // How close are we to our loss limit? if (profit < 0) signals.add(new CloseSignal("Loss", doubleRatio(-profit, maxLoss)); else // How close are we to the pullback limit? signals.add(new CloseSignal("Profit", transformToZeroToOne(profit, p.crest, p.crest-pullback)); ret signals; } } abstract sclass Opener { abstract OpenSignal openSignal(); abstract Opener cloneInto(Chaser strat); } record PreRunOpener(int minRunUp, int minRunDown) extends Opener { Opener cloneInto(Chaser strat) { ret strat.new PreRunOpener(minRunUp, minRunDown); } OpenSignal openSignal() { int direction = Chaser.this.direction; if (direction == 0) null; int preRun = direction > 1 ? minRunUp : minRunDown; int runLength = toInt(rlc.runLength()); printConcat( "[", formatLocalDateWithSeconds(currentTime()), "] ", "Runlength ", runLength, " price: ", formatPriceX(currentPrice()) ); // TODO: even more precise signal if (runLength >= preRun) print("Pre-run signal!"); ret new OpenSignal("RunLength " + runLength, doubleRatio(runLength, preRun), direction); } toString { ret "PreRunOpener up " + minRunUp + ", down " + minRunDown; } } persistable class Position extends G22TradingStrategy.Position { settable OpenSignal openSignal; settable Juicer juicer; // highest profit this position has reached settable double crest = -infinity(); double maxClosingTime() { ret openingTime()+hoursToMS(maxPositionHours); } L closeSignals() { ret juicer == null ?: juicer.closeSignals(this); } // >= 1 to close // otherwise keep open double closeSignal() { CloseSignal strongestSignal = closeSignalWithDescription(); ret strongestSignal == null ? 0 : strongestSignal.trigger; } CloseSignal closeSignalWithDescription() { ret highestBy(closeSignals(), signal -> signal.trigger); } void update { crest = max(crest, profitBeforeLeverage()); } toString { var closeSignal = closed() ? null : closeSignalWithDescription(); ret commaCombine(super.toString(), closeSignal ); } } LS status() { ret listCombine( "Max loss: " + formatDouble3(maxLoss) + "%" + ", pullback: " + formatDouble3(pullback) + "%", "Opener: " + opener, "Run length: " + signToUpDown(unnull(rlc.value())) + " " + rlc.runLength(), openSignal, super.status() ); } Position openPosition(OpenSignal openSignal) { int direction = openSignal.direction; if (l(positionsInDirection(direction)) > 0) { log("Not opening a second position in direction " + direction); null; } nextStep(); new Position p; p.openSignal(openSignal); p.juicer(new Juicer(maxLoss, pullback)); ret openPosition(p, direction); } void start { if (started()) fail("Already started"); if (currentPrice() == 0) ret with primed(true); // Save starting price, create price cells & digitizer startingPrice = currentPrice(); var priceCells = makePriceCells(startingPrice); digitizer = new PriceDigitizer2(priceCells); digitizer.verbose(verbose); log("Starting CHASER at " + startingPrice + " +/- " + cellSize); } void price(double price) { if (currentPrice == price) ret; currentPrice = price; if (!started()) { if (primed) { primed(false); start(); nextStep(); beforeStep(); afterStep(); } ret; } digitizer.digitize(price); direction = sign(digitizedPrice()-lastDigitizedPrice()); if (direction != 0) lastDirection = direction; if (started()) step(); } swappable void step { // Update positions with new price for (p : openPositions()) { cast p to Position; p.update(); } // Find positions to close L toClose = new L; for (p : openPositions()) { cast p to Position; CloseSignal signal = p.closeSignalWithDescription(); if (signal != null && signal.trigger() >= 1) { p.closeReason(signal); log("Closing position because " + signal + ": " + p); toClose.add(p); } } if (nempty(toClose)) { nextStep(); closePositions(toClose); afterStep(); } beforeStep(); double p1 = lastDigitizedPrice(); double p2 = digitizedPrice(); direction = sign(p2-p1); if (direction != 0) rlc.add(direction); openSignal = makeOpenSignal(); if (openSignal != null && openSignal.trigger >= 1) openPosition(openSignal); afterStep(); } swappable OpenSignal makeOpenSignal() { ret opener == null ?: opener.openSignal(); } swappable void beforeStep() {} S baseToString() { ret colonCombine( _conceptID() == 0 ? "" : "Strategy " + _conceptID(), "Chaser profit " + formatMarginProfit(coinProfit())); } void reset :: before { resetFields(this, "rlc openSignal"); } Chaser emptyClone() { Chaser strat = cast super.emptyClone(); ret strat.opener(opener?.cloneInto(strat)); } // Chaser.toString toString { ret baseToString(); } selfType preRunOpener(int minRunUp, int minRunDown) { ret opener(new PreRunOpener(minRunUp, minRunDown)); } }