set flag Reparse. replace real with double. replace Real with Double. replace price with float. // Persistence: // MPM3.BotInAction is persistable which in turn means // MPM3, Position, Juicer and other classes are also persistable. // We don't persist Position.ticker for space reasons though. persistable sclass MPM3 { // MARKET (trading platform) // How much percentual slippage we have for each position on // average plus the fees for the position. // Obviously dependent on the market (trading site) we are using. settable real marketAdversity = 0.12 + 0.08; // SELF-IMPOSED LIMITS // How long a position can be held at the longest // - only for backtesting! gettable real maxPositionDuration = minutesToMS(60); real minimalMaxDuration() { ret minutesToMS(1); } // correct user if they are stupid selfType maxPositionDuration(real whatUserWants) { maxPositionDuration = max(whatUserWants, minimalMaxDuration()); this; } // POSITIONS // An open(ed) position persistable record Position(transient TickerSequence ticker, real openingTime, real direction) { // Set this to a non-null value if this is a real position on a real market settable O irlInfo; // Eye that opened this position settable Eye eye; real openingPrice() { ret ticker.priceAtTimestamp(openingTime); } real profitAtTime(real time) { ret (ticker.priceAtTimestamp(time)/openingPrice()*100-100)*direction - marketAdversity; } real openingTime() { ret openingTime; } S directionString() { ret direction > 0 ? "long" : direction < 0 ? "short" : ""; } void restoreTicker(TickerSequence ticker) { this.ticker = ticker; } TickerPoint openingTickerPoint() { ret new TickerPoint(ticker, lround(openingTime)); } } macro forwardToPosition { TickerSequence ticker() { ret p.ticker; } void restoreTicker(TickerSequence ticker) { p.restoreTicker(ticker); } real direction() { ret p.direction; } S directionString() { ret p.directionString(); } real openingTime() { ret p.openingTime; } real openingPrice() { ret p.openingPrice(); } Eye eye() { ret p.eye(); } } persistable record LivePosition(Position p) { settable Juicer juicer; // highest profit this position has reached settable real crest = -infinity(); settable real time; // time we last updated this position settable real profit; // expected profit at that time forwardToPosition // return true if any change bool update(real time) { if (time <= this.time) false; this.time = time; profit = p.profitAtTime(time); crest = max(crest, profit); true; } transient simplyCached real maxClosingTime() { ret p.ticker.live() ? infinity() : min(p.openingTime+maxPositionDuration, p.ticker.endTime()); } bool shouldClose() { ret time >= maxClosingTime() || profit < (profit < 0 ? -juicer.lossTolerance : crest-juicer.pullback); } // >= 1 to close // otherwise keep open real closeSignal() { CloseSignal strongestSignal = closeSignalWithDescription(); ret strongestSignal == null ? 0 : strongestSignal.trigger; } CloseSignal closeSignalWithDescription() { ret highestBy(closeSignals(), signal -> signal.trigger); } // trigger = 0 to 1 record noeq CloseSignal(S reason, real trigger) {} L closeSignals() { new L signals; // How close are we to the max closing time? if (maxClosingTime() < infinity()) signals.add(new CloseSignal("Age", transformToZeroToOne(time, p.openingTime(), maxClosingTime()))); // How close are we to our loss limit? if (profit < 0) signals.add(new CloseSignal("Loss", doubleRatio(-profit, juicer.lossTolerance)); else // How close are we to the pullback limit? signals.add(new CloseSignal("Profit", transformToZeroToOne(profit, crest, crest-juicer.pullback)); ret signals; } } record ClosedPosition(Position p, real closingTime) { real profit = Double.NaN; MPM3 mpm() { ret MPM3.this; } forwardToPosition real closingTime() { ret closingTime; } real closingPrice() { ret ticker().priceAtTimestamp(closingTime); } real duration() { ret closingTime()-openingTime(); } real profit() { if (isNaN(profit)) profit = p.profitAtTime(closingTime); ret profit; } } // Wrappers for L and L // (maybe not needed?) sclass LivePositions { L positions = syncL(); } sclass ClosedPositions { L positions = syncL(); } // BOT (Eye + Juicer) persistable srecord Juicer(real lossTolerance, real pullback) {} abstract sclass Eye { // returns -1 to open short, 1 to open long // and anything in between to do nothing abstract real adviseDirection(TickerPoint tickerPoint); } // time = ms to look back srecord SimpleEye(long lookbackTime, real minMove) extends Eye { // 0 to 1 settable real relativeVerificationTime/* = 0.33*/; // 0 to 1 (roughly) settable real verificationThreshold/* = 0.5*/; // in ms real verificationTime() { ret lookbackTime*relativeVerificationTime; } // in price percent real verificationMoveThreshold() { ret minMove*relativeVerificationTime*verificationThreshold; } // in price percent real verificationMove(TickerPoint tickerPoint) { ret calculateMove(tickerPoint, verificationTime()); } // in price percent, scaled up to lookbackTime real relativeVerificationMove(TickerPoint tickerPoint) { ret doubleRatio(verificationMove(tickerPoint)*lookbackTime, verificationTime()); } real verificationSignal(TickerPoint tickerPoint) { ret doubleRatio(verificationMove(tickerPoint), verificationMoveThreshold()); } bool verificationEnabled() { ret relativeVerificationTime != 0; } real adviseDirection(TickerPoint tickerPoint) { real move = calculateMove(tickerPoint); if (isNaN(move)) ret 0; real relativeMove = doubleRatio(move, minMove); int sign = sign(relativeMove); real absRelativeMove = abs(relativeMove); if (verificationEnabled()) { real verification = max(0, sign*verificationSignal(tickerPoint)); absRelativeMove = min(absRelativeMove, verification); } // No more clamping - allow trade signals > 100% ret sign*absRelativeMove; } real calculateMove(TickerPoint tickerPoint, real lookbackTime default this.lookbackTime) { if (!tickerPoint.canLookback(lround(lookbackTime))) ret Double.NaN; real currentPrice = tickerPoint.currentPrice(); real before = tickerPoint.lookback(lround(lookbackTime)); ret currentPrice/before*100-100; } } /*persistable srecord TwoEyes extends Eye { settable Eye eye1; settable Eye eye2; *(Eye *eye1, Eye *eye2) {} real adviseDirection(TickerPoint tickerPoint) { } }*/ srecord TradingBot(Eye eye, Juicer juicer) { *(real lookbackMinutes, real minMove, real maxLoss, real pullback) { eye = new SimpleEye(minutesToMS(lookbackMinutes), minMove); juicer = new Juicer(maxLoss, pullback); } } class BotInAction extends MetaWithChangeListeners { settable TradingBot botConfiguration; transient TickerSequence ticker; // Both open and closed positions may be purely simulated or // mirrors of positions actually made IRL (see Position.irlInfo). new ListWithChangeListeners openPositions; new ListWithChangeListeners closedPositions; LivePosition openPosition(TickerPoint tickerPoint, real direction) { var p = new Position(tickerPoint.ticker(), tickerPoint.time(), direction); var livePosition = new LivePosition(p).juicer(botConfiguration.juicer); livePosition.update(tickerPoint.time()); openPositions.add(livePosition); change(); ret livePosition; } void closePosition(LivePosition position) { if (openPositions.remove(position)) { // TODO: closedPositions.add... change(); } } void restoreTicker(TickerSequence ticker) { if (ticker == this.ticker) ret; this.ticker = ticker; ticker.onPricePointAdded(time -> updatePositions(time)); for (p : openPositions) p.restoreTicker(ticker); for (p : closedPositions) p.restoreTicker(ticker); } void updatePositions(real time) { bool anyChange; for (p : openPositions) anyChange |= p.update(time); if (anyChange) change(); } } ClosedPosition runJuicer(Position p, Juicer j) { TickerSequence ticker = p.ticker; long time = lround(p.openingTime); double maxClosingTime = min(time+maxPositionDuration, ticker.endTime()); real crest = -infinity(); while (time < maxClosingTime) { time = ticker.nextTimestamp(time); real profit = p.profitAtTime(time); crest = max(crest, profit); if (profit < (profit < 0 ? -j.lossTolerance : crest-j.pullback)) break; } ret new ClosedPosition(p, time); } record noeq Backtest(TickerSequence ticker, TradingBot bot) extends Convergent { new L closedPositions; long time; double latestAllowedOpeningTime() { ret ticker.endTime()-maxPositionDuration; } void step { if (time == 0) time = ticker.startTime(); if (time > latestAllowedOpeningTime()) ret with done = true; TickerPoint tickerPoint = new(ticker, time); real direction = bot.eye.adviseDirection(tickerPoint); if (abs(direction) < 1) // No position to open, move to next timestamp time = ticker.nextTimestamp(time); else { // We have a position to open var position = new Position(ticker, time, direction) .eye(bot.eye); // Ask juicer when to close position var closedPosition = runJuicer(position, bot.juicer); // Add to record of positions made closedPositions.add(closedPosition); // Move on to next timestamp time = ticker.nextTimestamp(closedPosition.closingTime); } } } }