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 settableWithVar int lastDirection; 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 class BaseJuicer { settable double maxLoss; 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)); ret signals; } } // Juicer with fixed pullback persistable class Juicer extends BaseJuicer { settable double pullback; *(double *maxLoss, double *pullback) {} toString { ret renderRecord("Juicer", +maxLoss, +pullback); } L closeSignals(Position p) { L signals = super.closeSignals(p); double profit = p.profitBeforeLeverage(); // How close are we to the pullback limit? if (profit >= 0) signals.add(new CloseSignal("Profit", transformToZeroToOne(profit, p.crest, p.crest-pullback)); ret signals; } } // Juicer with relative pullback persistable class RelativePullbackJuicer extends BaseJuicer { settable double relativePullback; // e.g. 20 settable double minPullback; // e.g. 0.1 settable double maxPullback; // e.g. 4 toString { ret renderRecord("RelativePullbackJuicer", +maxLoss, +relativePullback, +minPullback, +maxPullback); } L closeSignals(Position p) { L signals = super.closeSignals(p); double profit = p.profitBeforeLeverage(); // How close are we to the pullback limit? if (profit >= 0) signals.add(new CloseSignal("Profit", transformToZeroToOne(profit, p.crest, p.crest-calculatePullback(p.crest))); ret signals; } double calculatePullback(double crest) { ret clamp(relativePullback/100*crest, minPullback, maxPullback); } } 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 BaseJuicer juicer; // highest profit this position has reached double crest = -infinity(); double maxClosingTime() { ret usingLiveData ? infinity() : 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( commaCombine( "Max loss: " + formatDouble3(maxLoss) + "%", relativePullback == 0 ? "pullback: " + formatDouble3(pullback) + "%" : commaCombine( "relative pullback: " + formatDouble3(relativePullback) + "%", "min pullback: " + formatDouble3(minPullback) + "%", "max pullback: " + formatDouble3(maxPullback) + "%", ) ), "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(makeJuicer()); ret openPosition(p, direction); } BaseJuicer makeJuicer() { if (relativePullback != 0) ret new RelativePullbackJuicer() .relativePullback(relativePullback) .minPullback(minPullback) .maxPullback(maxPullback) .maxLoss(maxLoss); else ret new Juicer(maxLoss, pullback); } 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); // Don't open anything while we're feeding back data if (haveBackData()) { 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())); } S fieldsToReset() { ret lines(ll(super.fieldsToReset(), [[lastDirection 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)); } }