concept Chaser extends G22TradingStrategy { // Juicer parameters settable double maxLoss = 0.6; settable double pullback = 0.4; settable Opener opener; // internal new RunLengthCounter rlc; // trigger = 0 to 1 record noeq CloseSignal(S reason, double trigger) { double trigger() { ret trigger; } } // trigger = 0 to 1 record noeq OpenSignal(S reason, double trigger, int direction) { double trigger() { ret trigger; } } persistable record Juicer(double maxLoss, double pullback) { L closeSignals(Position p) { new L signals; double profit = p.profit()/p.leverage; // 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 { abstract Opener cloneInto(Chaser strat) { ret strat.new PreRunOpener(minRunUp, minRunDown); } OpenSignal openSignal() { if (direction == 0) null; int preRun = direction > 1 ? minRunUp : minRunDown; int runLength = toInt(rlc.runLength()); printConcat( "[", formatLocalDateWithSeconds(currentTime()), "] ", "Runlength ", runLength, " price: ", formatPriceX(currentPrice()) ); if (runLength >= preRun) { print("Pre-run signal!"); ret new OpenSignal("RunLength " + runLength, 1, direction); } null; } } 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 infinity(); } 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, profit()); } } LS status() { ret listCombine( "Max loss: " + formatDouble3(maxLoss) + "%" + ", pullback: " + formatDouble3(pullback) + "%", 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 (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 openSignal = openSignal(); if (openSignal != null && openSignal.trigger >= 1) openPosition(openSignal); afterStep(); } swappable OpenSignal openSignal() { 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"); } Chaser emptyClone() { Chaser strat = cast super.emptyClone(); ret super.opener(opener?.cloneInto(strat)); } // Chaser.toString toString { ret baseToString(); } }