longPositionsAtOrBelowDigitizedPrice(double openingPrice) {
ret filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() <= openingPrice);
}
PriceCells cells aka priceCells() {
ret digitizer?.cells;
}
S formatPriceX(double price) {
if (isNaN(price)) ret "-";
S s = formatPrice(price);
if (cells() == null) ret s;
double num = cells().priceToCellNumber(price);
ret s + " (C" + formatDouble2(num) + ")";
}
abstract void price aka currentPrice(double price);
bool inCooldown() {
if (cooldownMinutes <= 0) false;
if (nempty(openPositions)) true;
var lastClosed = last(closedPositions);
if (lastClosed == null) false;
var minutes = toMinutes(currentTime()-lastClosed.closingTime);
ret minutes < cooldownMinutes;
}
void assureCanOpen(Position p) {
if (cooldownMinutes > 0) {
if (nempty(openPositions))
fail("Already have an open position");
var lastClosed = last(closedPositions);
if (lastClosed != null) {
var minutes = toMinutes(currentTime()-lastClosed.closingTime);
if (minutes < cooldownMinutes)
fail("Last position closed " + formatMinutes(fromMinutes(minutes)) + " ago, cooldown is " + cooldownMinutes);
}
}
}
P openPosition(P p, int direction, O openReason default null) {
try {
assureCanOpen(p);
p.openReason(openReason);
var price = digitizedPrice();
var realPrice = currentPrice();
logVars("openPosition", +realPrice, +price, +digitizer);
if ((isNaN(price) || price == 0) && digitizer != null) {
price = digitizer.digitizeIndividually(currentPrice());
print("digitized individually: " + price);
}
p.openingPrice(realPrice);
p.direction(direction);
p.digitizedOpeningPrice(price);
// Calculate quantity (cryptoAmount) from margin
// unless cryptoAmount is already set
if (p.cryptoAmount == 0)
p.cryptoAmount = p.marginToUse/realPrice*leverage;
// Round cryptoAmount to the allowed increments
p.cryptoAmount = roundTo(cryptoStep, p.cryptoAmount);
// Clamp to minimum/maximum order
p.cryptoAmount = clamp(p.cryptoAmount, minCrypto, maxCrypto);
log(renderVars("openPosition", +marginPerPosition, +realPrice, +leverage, cryptoAmount := p.cryptoAmount, +cryptoStep));
p.open();
//print("Opening " + p);
//printPositions();
p.openOnMarket();
ret p;
} on fail e {
log(e);
}
}
LS status() {
double mulProf = multiplicativeProfit();
ret llNonNulls(
empty(comment) ? null : "Comment: " + comment,
"Profit: " + marginCoin + " " + plusMinusFix(formatMarginPrice(coinProfit())),
"Realized profit: " + marginCoin + " " + formatMarginProfit(realizedCoinProfit) + " from " + n2(closedPositions, "closed position")
+ " (" + formatMarginProfit(realizedCoinWins) + " from " + n2(realizedWins, "win")
+ ", " + formatMarginProfit(realizedCoinLosses) + " from " + n2(realizedLosses, "loss", "losses") + ")",
"Unrealized profit: " + marginCoin + " " + formatMarginProfit(unrealizedCoinProfit()) + " in " + n2(openPositions, "open position"),
isNaN(mulProf) ? null : "Multiplicative profit: " + formatProfit(mulProf) + "%",
//baseToString(),
!primed() ? null : "Primed",
!started() ? null : "Started. current price: " + formatPriceX(currentPrice)
+ (isNaN(digitizedPrice()) ? "" : ", digitized: " + formatPriceX(digitizedPrice())),
(riskPerTrade == 0 ? "" : "Risk per trade: " + formatDouble1(riskPerTrade) + "%. ")
+ "Position size: " + marginCoin + " " + formatPrice(marginPerPosition) + "x" + formatDouble1(leverage) + " = " + marginCoin + " " + formatPrice(positionSize()),
!usingCells() ? null : "Cell size: " + formatCellSize(cellSize),
spaceCombine("Step " + n2(stepCount), renderStepSince()),
//"Debt: " + marginCoin + " " + formatMarginProfit(debt()) + " (max seen: " + formatMarginProfit(maxDebt) + ")",
"Investment used: " + marginCoin + " " + formatMarginPrice(maxInvestment()),
strategyJuicer == null ?: "Strategy juicer: " + strategyJuicer,
//"Drift: " + cryptoCoin + " " + plusMinusFix(formatCryptoAmount(drift())),
);
}
S renderStepSince() {
if (stepSince == 0) ret "";
ret "since " + (active()
? formatHoursMinutesColonSeconds(currentTime()-stepSince)
: formatLocalDateWithSeconds(stepSince));
}
LS fullStatus() {
ret listCombine(
tradingAccount,
status(),
"",
n2(openPositions, "open position") + ":",
reversed(openPositions),
"",
n2(closedPositions, "closed position")
+ " (" + (showClosedPositionsBackwards ? "latest first" : "oldest first") + "):",
showClosedPositionsBackwards ? reversed(closedPositions) : closedPositions,
);
}
void feed(PricePoint pricePoint) {
if (!active()) ret;
setTime(pricePoint.timestamp);
price(pricePoint.price);
}
void feed(TickerSequence ts) {
if (!active()) ret;
if (ts == null) ret;
for (pricePoint : ts.pricePoints())
feed(pricePoint);
}
public int compareTo(G22TradingStrategy s) {
ret s == null ? 1 : cmp(coinProfit(), s.coinProfit());
}
// Returns closed positions
L closeAllPositions(O reason default "User close") {
var positions = openPositions();
closePositions(positions, reason);
ret (L) positions;
}
void closeMyself() {
closedItself(currentTime());
closeAllPositionsAndDeactivate();
}
void closeAllPositionsAndDeactivate {
deactivate();
closeAllPositions();
}
void deactivate {
market(null);
if (!active) ret;
active(false);
deactivated(currentTime());
log("Strategy deactivated.");
}
void reset aka reset_G22TradingStrategy() {
resetFields(this, fieldsToReset());
change();
}
selfType emptyClone aka emptyClone_G22TradingStrategy() {
var clone = shallowCloneToUnlistedConcept(this);
clone.reset();
ret clone;
}
L allPositions() {
ret concatLists(openPositions, closedPositions);
}
L sortedPositions() {
var allPositions = allPositions();
ret sortedByCalculatedField(allPositions, -> .openingTime());
}
bool positionsAreNonOverlapping() {
for (a, b : unpair overlappingPairs(sortedPositions()))
if (b.openingTime() < a.closingTime())
false;
true;
}
// Profit when applying all positions (somewhat theoretical because you
// might go below platform limits)
// Also only works when positions are linear
double multiplicativeProfit() {
if (!positionsAreNonOverlapping()) ret Double.NaN;
double profit = 1;
for (p : sortedPositions())
profit *= 1+p.profit()/100;
ret (profit-1)*100;
}
bool haveBackData() { ret backDataHoursWanted == 0 | backDataFed; }
bool didRealTrades() {
ret any(allPositions(), p -> p.openedOnMarket() || p.closedOnMarket());
}
S formatCellSize(double cellSize) {
ret formatPercentage(cellSize, 3);
}
S areaDesc() {
if (eq(area, "Candidates")) ret "Candidate";
ret nempty(area) ? area : archived ? "Archived" : "";
}
selfType setTime aka currentTime(long time) {
int age = ifloor(ageInHours());
long lastMod = mod(currentTime()-startTime, hoursToMS(1));
currentTime = -> time;
if (ifloor(ageInHours()) > age)
log("Hourly profit log: " + formatMarginProfit(coinProfit()));
this;
}
double ageInHours() { ret startTime == 0 ? 0 : msToHours(currentTime()-startTime); }
// only use near start of strategy, untested otherwise
selfType changeCellSize(double newCellSize) {
double oldCellSize = cellSize();
if (oldCellSize == newCellSize) this;
cellSize(newCellSize);
if (digitizer != null)
digitizer.swapPriceCells(makePriceCells(priceCells().basePrice()));
log("Changed cell size from " + oldCellSize + " to " + newCellSize);
this;
}
bool hasClosedItself() { ret closedItself != 0; }
public double juiceValue() { ret coinProfit(); }
// drift is our cumulative delta (=sum of all signed position
// quantities)
double drift() {
double drift = 0;
for (p : openPositions())
drift += p.cryptoAmount()*p.direction();
ret drift;
}
S formatCryptoAmount(double amount) {
ret formatDouble3(amount);
}
Position openShort() { ret openPosition(-1); }
Position openLong() { ret openPosition(1); }
Position openPosition(int direction, O openReason default null) {
new Position p;
p.marginToUse = marginToUseForNewPosition();
ret openPosition(p, direction, openReason);
}
// Open or close positions until the drift (delta) is
// equal to targetDrift (or as close as the platform's
// restrictions allow).
// Returns the list of positions opened or closed
// (Function is in dev.)
L adjustDrift(double targetDrift, O reason default null) {
new L changeList;
// target 0? close all
if (targetDrift == 0)
ret closeAllPositions(reason);
double drift = drift();
// target already reached? done
if (drift == targetDrift) ret changeList;
int direction = sign(targetDrift);
// Are we changing direction? Then close everything first.
if (sign(drift) != direction)
changeList.addAll(closeAllPositions(reason));
// Now we know targetDrift and drift have the same sign.
double diff = abs(targetDrift)-abs(drift);
// Round to allow increments
diff = roundTo(cryptoStep, diff);
if (diff > 0) {
// We need to open a new position - that's easy
new Position p;
p.cryptoAmount = diff;
changeList.add(openPosition(p, direction, reason));
} else {
double toClose = -diff;
// We need to close positions - that's a bit more tricky
// Filter by direction to be sure in case there is
// hedging (you shouldn't do hedging, really, though)
var positions = filter(openPositions(), p -> p.direction() == direction);
// Let's look for an exact size match first.
// We rounded to cryptoStep, so using == is ok.
var _toClose = toClose;
var exactMatch = firstThat(positions, p -> p.cryptoAmount == _toClose);
if (exactMatch != null) {
exactMatch.close(reason);
changeList.add(exactMatch);
ret changeList;
}
// No exact match. Go through positions starting with
// the oldest one.
for (p : positions) {
toClose = roundTo(cryptoStep, toClose);
if (toClose == 0) break;
if (toClose >= p.cryptoAmount) {
// Need to close the whole position
toClose -= p.cryptoAmount;
p.close(reason);
changeList.add(p);
} else {
// Need a partial close.
changeList.add(p);
var remainderPosition = p.partialClose(toClose, reason);
changeList.add(remainderPosition);
}
}
}
ret changeList;
}
L winners() { ret filter(closedPositions(), p -> p.coinProfit() > 0); }
L losers() { ret filter(closedPositions(), p -> p.coinProfit() < 0); }
L extends Position> openPositions() { ret cloneList(openPositions); }
double winRate() {
ret l(winners()) * 100.0 / l(closedPositions);
}
bool usingCells() { true; }
double positionSize() {
ret marginToUseForNewPosition()*leverage;
}
double marginToUseForNewPosition() {
ret scaleMargin(marginPerPosition);
}
// to simulate compounding in backtest
double scaleMargin(double margin) {
// Live? Then no scaling.
if (usingLiveData) ret margin;
ret margin*compoundingFactor();
}
double compoundingFactor() {
// No compounding selected?
if (compoundingBaseEquity == 0) ret 1;
ret max(0, remainingEquity()/compoundingBaseEquity);
}
// only for backtesting
double remainingEquity() {
ret compoundingBaseEquity+realizedCoinProfit();
}
class RiskToMargin {
// in percent
settable double riskForTrade = riskPerTrade;
settable double stopLossPercent;
settable double price = currentPrice();
settable double fullEquity;
double riskedEquity() {
ret fullEquity*riskForTrade/100;
}
double qty() {
ret riskedEquity()/(price*stopLossPercent/100);
}
double margin aka get() {
ret qty()*price/leverage();
}
}
void fixRealizedStats {
realizedProfit(0);
realizedCoinProfit(0);
realizedWins(0);
realizedCoinWins(0);
realizedLosses(0);
realizedCoinLosses(0);
for (p : closedPositions()) p.addToRealizedStats();
}
// p must not be open
bool deletePosition(Position p) {
if (closedPositions.remove(p) || positionsThatFailedToOpen.remove(p)) {
fixRealizedStats();
true;
}
false;
}
bool hasRiskPerTrade() { ret riskPerTrade != 0; }
Position addFakePosition(double coinProfit) {
new Position p;
p.makeFake(coinProfit);
ret p;
}
}