// Design notes. // // NG stands for "Non-Garbage" // // !Design notes end sclass NGSStrategy2 extends G22CandleBasedStrategyWithTrailingStop is IntegrityCheckable { settable int period1 = 100; // the "slow" MA settable int period2 = 5; // the fast MA (for opening) settable double maMinMove = 0.07; // MA trend threshold settable double maMoveHysteresis = 0.01; // MA trend non-oscillation threshold settable Side longSide = new(1); settable Side shortSide = new(-1); // false = only manual openLong() and openShort(), // not based on indicators. // true = auto-open positions indefinitely depending on // indicators. settable bool autoPositions = true; // temporal fields follow S fieldsToReset() { ret lineCombine(super.fieldsToReset(), [[ ma1 ma2 lastMA1 maMove movingAverage1_cache movingAverage2_cache maMoveHistory maDirection maDirectionHistory ]]); } settable double ma1; settable double ma2; settable double lastMA1 = Double.NaN; settable double maMove; settable int maDirection; // Too big, disabling this by default now gettable TickerSequence maMoveHistory/* = new TickerSequence("maMove")*/; gettable TickerSequence maDirectionHistory = new TickerSequence("maDirection"); settable transient O visualizer; selfType emptyClone() { selfType clone = cast super.emptyClone(); clone.longSide(longSide.emptyClone(clone)); clone.shortSide(shortSide.emptyClone(clone)); ret clone; } // Side = logic for one side (long/short = direction 1/-1) record noeq Side(int direction) { // Our condition is: // Price has to be in a band close to the MA first (band 1) // and then move into a band further away from the MA (band 2) // Are we allowed to open positions in this direction at all? settable bool enabled = true; // First band (close to MA) settable DoubleRange band1 = new(0.2, 0.6); // Second band settable DoubleRange band2 = new(0.6, 1.0); // Percentage to close the position at (below band/above band) settable double closeLevel = 0; // Do we require the MA to go in our direction? // (If no, we just require it to not go the other direction.) settable bool needMADirection = true; // temporal fields follow selfType emptyClone(NGSStrategy2 stratClone) { ret copyFields(this, stratClone.new Side(), identifiers("direction enabled band1 band2 closeLevel")); } // Has price visited band1 recently? settable bool wasInBand1; // convert percent band to current price range DoubleRange instantiateBand(DoubleRange band, double ma1 default ma1) { ret doubleRange( maPlusPercent(ma1, direction*band.start), maPlusPercent(ma1, direction*band.end)).sort(); } double priceToPercent(double price, double ma1 default ma1) { ret (price/ma1-1)*direction*100; } double relativePrice() { ret priceToPercent(currentPrice()); } void step { var relativePrice = relativePrice(); // close if too close to band/beyond band if (relativePrice < closeLevel && sign(drift()) == direction) { closeAllPositions(direction > 0 ? "Below band" : "Above band"); } // price on wrong side => reset if (relativePrice < 0) wasInBand1(false); // Above band2 => reset if (relativePrice > band2.end) wasInBand1(false); // We hit band 1 if (band1.contains(relativePrice)) wasInBand1(true); if (autoPositions && enabled && wasInBand1 && band2.contains(relativePrice) && ma2*direction < currentPrice()*direction && (needMADirection ? maDirection == direction : maDirection != -direction)) { log("Trigger " + this); wasInBand1(false); if (drift() == 0 && !inCooldown()) openPosition(direction); } } // "short" or "long" toString { ret trading_directionToPositionType(direction); } LS status() { ret listCombine( enabled ? null : "disabled", "Relative price: "+ formatDouble1(relativePrice()), !wasInBand1 ? null : "Was in band 1", "Band 1: " + band1, "Band 2: " + band2, "Close level: " + closeLevel ); } } // end of Side void ngsStep aka ngsLogic() { //double close = currentPrice(); //double drift = drift(); for (side : sides()) side.step(); change(); } simplyCached MAIndicator movingAverage1() { ret new MAIndicator(period1); } simplyCached MAIndicator movingAverage2() { ret new MAIndicator(period2); } L indicators() { ret ll(movingAverage1(), movingAverage2()); } bool maRising() { ret maDirection > 0; } bool maFalling() { ret maDirection < 0; } double moveSignal() { ret maMove/maMinMove*100; } double maPlusPercent(dbl ma1 default ma1, double percent) { ret ma1*(1+percent/100); } double maMinusPercent(dbl ma1 default ma1, double percent) { ret ma1*(1-percent/100); } { granularity(30); onNewPrice(price -> ngsStep()); onCandleCompleted(candle -> { lastMA1(ma1); ma1(movingAverage1()!); ma2(movingAverage2()!); maMove((ma1/lastMA1-1)*100); maMoveHistory?.addIfPriceChanged(maMove, currentTime()); if (maMove >= maMinMove) maDirection(1); else if (maMove <= -maMinMove) maDirection(-1); else if (maDirection < 0 && maMove >= -maMinMove2() || maDirection > 0 && maMove <= maMinMove2()) maDirection(0); maDirectionHistory?.addIfPriceChanged(maDirection, currentTime()); ngsStep(); }); } double maMinMove2() { ret maMinMove-maMoveHysteresis; } LS status() { ret listCombine( "Min distance, MA min move: " + maMinMove + " (current move: " + formatDouble(maMove, 4) + ", direction: " + maDirection + ")", "MA periods: " + period1 + " " + period2, "Do: " + (!autoPositions ? "Only manual positions" : commaCombine(stringIf(doLongs(), "Longs"), stringIf(doShorts(), "Shorts"))), "Moving averages: " + formatDouble_significant(ma1, 4) + ", " + formatDouble_significant(ma2, 4), map(sides(), side -> firstToUpper(str(side)) + " side: " + commaCombine(side.status())), super.status() ); } void afterStep :: before { } bool doShorts() { ret shortSide.enabled; } bool doLongs() { ret longSide.enabled; } L sides() { ret ll(longSide, shortSide); } int candlesNeededBeforeOperational() { ret period1; } bool usingCells() { false; } public void integrityCheck { movingAverage1().integrityCheck(); movingAverage2().integrityCheck(); } }