concept Chaser extends G22TradingStrategy { // Juicer parameters settable double maxLoss = 0.6; settable double pullback = 0.4; // internal // trigger = 0 to 1 record noeq CloseSignal(S reason, double trigger) { double trigger() { ret trigger; } } persistable record Juicer(double maxLoss, double pullback) { L closeSignals(Position p) { new L signals; double profit = p.profit(); // 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; } } persistable class Position extends G22TradingStrategy.Position { 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()); } } Position openPosition(int direction) { if (nempty(openPositions())) { log("Not opening a second position"); null; } new Position p; 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(); } ret; } digitizer.digitize(price); direction = sign(digitizedPrice()-lastDigitizedPrice()); nextStep(); if (started()) step(); print(this); } 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 = strongestSignal(); if (signal != null && signal.trigger() >= 1) { p.closeReason(signal); log("Closing position because " + signal + ": " + p); toClose.add(p); } } if (nempty(toClose)) { closePositions(toClose); afterStep(); } double p1 = lastDigitizedPrice(); double p2 = digitizedPrice(); direction = sign(p2-p1); if (direction == 0) ret; // TODO: open new position afterStep(); } S baseToString() { ret colonCombine( _conceptID() == 0 ? "" : "Strategy " + _conceptID(), "Chaser"); } // Chaser.toString toString { ret baseToString(); } Position openShort() { ret openPosition(-1); } Position openLong() { ret openPosition(1); } }