longPositionsAtOrBelowDigitizedPrice(double openingPrice) {
    return filter(openPositions, p -> p.isLong() && p.digitizedOpeningPrice() <= openingPrice);
  }
  
  final PriceCells priceCells(){ return cells(); }
PriceCells cells() {
    return digitizer == null ? null : digitizer.cells;
  }
  
  String formatPriceX(double price) {
    if (isNaN(price)) return "-";
    String s = formatPrice(price);
    if (cells() == null) return s;
    double num = cells().priceToCellNumber(price);
    return s + " (C" + formatDouble2(num) + ")";
  }
  
  final  void currentPrice(double price){ price(price); }
abstract void price(double price);
  
   P openPosition(P p, int direction) { return openPosition(p, direction, null); }
 P openPosition(P p, int direction, Object openReason) {
    p.openReason(openReason);
    var price = digitizedPrice();
    var realPrice = currentPrice();
    logVars("openPosition", "realPrice", realPrice, "price", price, "digitizer", 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);
    double cryptoAmount = p.marginToUse/realPrice*leverage;
    cryptoAmount = roundTo(cryptoStep, cryptoAmount);
    log(renderVars("openPosition", "marginPerPosition", marginPerPosition, "realPrice", realPrice, "leverage", leverage, "cryptoAmount", cryptoAmount, "cryptoStep", cryptoStep));
    p.cryptoAmount = max(minCrypto, cryptoAmount);
    p.open();
    //print("Opening " + p);
    //printPositions();
    p.openOnMarket();
    return p;
  }
  
  List status() {
    double mulProf = multiplicativeProfit();
    return 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())),
      "Leverage: " + leverage + ", margin per position: " + marginCoin + " " + formatPrice(marginPerPosition),
      "Cell size: " + formatCellSize(cellSize),
      spaceCombine("Step " + n2(stepCount), renderStepSince()),
      //"Debt: " + marginCoin + " " + formatMarginProfit(debt()) + " (max seen: " + formatMarginProfit(maxDebt) + ")",
      "Investment used: " + marginCoin + " " + formatMarginPrice(maxInvestment()),
      strategyJuicer == null ? null : "Strategy juicer: " + strategyJuicer,
      "Drift: " + cryptoCoin + " " + plusMinusFix(formatCryptoAmount(drift())));
  }
  
  String renderStepSince() {
    if (stepSince == 0) return "";
    return "since " + (active()
      ? formatHoursMinutesColonSeconds(currentTime()-stepSince)
      : formatLocalDateWithSeconds(stepSince));
  }
  
  List fullStatus() {
    return listCombine(
      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()) return;
    setTime(pricePoint.timestamp);
    price(pricePoint.price);
  }
  
  void feed(TickerSequence ts) {
    if (!active()) return;
    if (ts == null) return;
    for (var pricePoint : ts.pricePoints())
      feed(pricePoint);
  }
  
  public int compareTo(G22TradingStrategy s) {
    return s == null ? 1 : cmp(coinProfit(), s.coinProfit());
  }
  
  void closeAllPositions() { closeAllPositions("User close"); }
void closeAllPositions(Object reason) {
    closePositions(openPositions(), reason);
  }
  
  void closeMyself() {
    closedItself(currentTime());
    closeAllPositionsAndDeactivate();
  }
  
  void closeAllPositionsAndDeactivate() {
    deactivate();
    closeAllPositions();
  }
  
  void deactivate() {
    if (!active) return;
    active(false);
    deactivated(currentTime());
    log("Strategy deactivated.");
  }
  
  final void reset_G22TradingStrategy(){ reset(); }
void reset() {
    resetFields(this, fieldsToReset());
    change();
  }
  
  final G22TradingStrategy emptyClone_G22TradingStrategy(){ return emptyClone(); }
G22TradingStrategy emptyClone() {
    var clone = shallowCloneToUnlistedConcept(this);
    clone.reset();
    return clone;
  }
  
  List allPositions() {
    return concatLists(openPositions, closedPositions);
  }
  
  List sortedPositions() {
    var allPositions = allPositions();
    return sortedByCalculatedField(allPositions, __6 -> __6.openingTime());
  }
  
  boolean positionsAreNonOverlapping() {
    for (var __0: overlappingPairs(sortedPositions()))
      { var a = pairA(__0); var b = pairB(__0);  if (b.openingTime() < a.closingTime())
        return false; }
    return 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()) return Double.NaN;
    double profit = 1;
    for (var p : sortedPositions())
      profit *= 1+p.profit()/100;
    return (profit-1)*100;
  }
  
  boolean haveBackData() { return backDataHoursWanted == 0 | backDataFed; }
  
  boolean didRealTrades() {
    return any(allPositions(), p -> p.openedOnMarket() || p.closedOnMarket());
  }
  
  String formatCellSize(double cellSize) {
    return formatPercentage(cellSize, 3);
  }
  
  String areaDesc() {
    if (eq(area, "Candidates")) return "Candidate";
    return nempty(area) ? area : archived ? "Archived" : "";
  }
  
  final G22TradingStrategy currentTime(long time){ return setTime(time); }
G22TradingStrategy setTime(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()));
    return this;
  }
  
  double ageInHours() { return startTime == 0 ? 0 : msToHours(currentTime()-startTime); }
  
  // only use near start of strategy, untested otherwise
  G22TradingStrategy changeCellSize(double newCellSize) {
    double oldCellSize = cellSize();
    if (oldCellSize == newCellSize) return this;
    cellSize(newCellSize);
    if (digitizer != null)
      digitizer.swapPriceCells(makePriceCells(priceCells().basePrice()));
    log("Changed cell size from " + oldCellSize + " to " + newCellSize);
    return this;
  }
  
  boolean hasClosedItself() { return closedItself != 0; }
  
  public double juiceValue() { return coinProfit(); }
  
  double drift() {
    double drift = 0;
    for (var p : openPositions())
      drift += p.cryptoAmount()*p.direction();
    return drift;
  }
  
  String formatCryptoAmount(double amount) {
    return formatDouble3(amount);
  }
  
  Position openShort() { return openPosition(-1); }
  Position openLong() { return openPosition(1); }
  
  Position openPosition(int direction) { return openPosition(direction, null); }
Position openPosition(int direction, Object openReason) {
    Position p = new Position();
    p.marginToUse = marginPerPosition;
    return openPosition(p, direction, openReason);
  }
  
  List winners() { return filter(closedPositions(), p -> p.coinProfit() > 0); }
  List losers() { return filter(closedPositions(), p -> p.coinProfit() < 0); }
  
  List extends Position> openPositions() { return cloneList(openPositions); }
}
interface IFuturesMarket {
  // all open/close orders are market right now (not limit)
  
  static class OpenOrder {
     final public OpenOrder setHoldSide(HoldSide holdSide){ return holdSide(holdSide); }
public OpenOrder holdSide(HoldSide holdSide) { this.holdSide = holdSide; return this; }  final public HoldSide getHoldSide(){ return holdSide(); }
public HoldSide holdSide() { return holdSide; }
 HoldSide holdSide;
     final public OpenOrder setCryptoAmount(double cryptoAmount){ return cryptoAmount(cryptoAmount); }
public OpenOrder cryptoAmount(double cryptoAmount) { this.cryptoAmount = cryptoAmount; return this; }  final public double getCryptoAmount(){ return cryptoAmount(); }
public double cryptoAmount() { return cryptoAmount; }
 double cryptoAmount;
     final public OpenOrder setLeverage(double leverage){ return leverage(leverage); }
public OpenOrder leverage(double leverage) { this.leverage = leverage; return this; }  final public double getLeverage(){ return leverage(); }
public double leverage() { return leverage; }
 double leverage = 1;
     final public OpenOrder setIsCross(boolean isCross){ return isCross(isCross); }
public OpenOrder isCross(boolean isCross) { this.isCross = isCross; return this; }  final public boolean getIsCross(){ return isCross(); }
public boolean isCross() { return isCross; }
 boolean isCross = false;
  }
  
  static class CloseOrder {
     final public CloseOrder setHoldSide(HoldSide holdSide){ return holdSide(holdSide); }
public CloseOrder holdSide(HoldSide holdSide) { this.holdSide = holdSide; return this; }  final public HoldSide getHoldSide(){ return holdSide(); }
public HoldSide holdSide() { return holdSide; }
 HoldSide holdSide;
     final public CloseOrder setCryptoAmount(double cryptoAmount){ return cryptoAmount(cryptoAmount); }
public CloseOrder cryptoAmount(double cryptoAmount) { this.cryptoAmount = cryptoAmount; return this; }  final public double getCryptoAmount(){ return cryptoAmount(); }
public double cryptoAmount() { return cryptoAmount; }
 double cryptoAmount;
  }
  
  // throws exception if order failed
  void openPosition(OpenOrder order);
  
  // throws exception if order failed
  void closePosition(CloseOrder order);
  
  static class SwappableImplementation implements IFuturesMarket {
    public transient  IVF1 openPosition;
public void openPosition(OpenOrder order) { if (openPosition != null) openPosition.get(order); else openPosition_base(order); }
final public void openPosition_fallback(IVF1 _f, OpenOrder order) { if (_f != null) _f.get(order); else openPosition_base(order); }
public void openPosition_base(OpenOrder order) {}
    public transient  IVF1 closePosition;
public void closePosition(CloseOrder order) { if (closePosition != null) closePosition.get(order); else closePosition_base(order); }
final public void closePosition_fallback(IVF1 _f, CloseOrder order) { if (_f != null) _f.get(order); else closePosition_base(order); }
public void closePosition_base(CloseOrder order) {}
    public transient  IF0 drift;
public double drift() { return drift != null ? drift.get() : drift_base(); }
final public double drift_fallback(IF0 _f) { return _f != null ? _f.get() : drift_base(); }
public double drift_base() { throw unimplemented(); }
    public FutureCoinParameters getCoinParameters() { throw unimplemented(); }
  }
  
  // Get current drift (sum of longs+shorts in crypto units)
  double drift();
  
  FutureCoinParameters getCoinParameters();
}
static class SynchronizedLongBuffer implements ILongBuffer {
  long[] data;
  int size;
  
  SynchronizedLongBuffer() {}
  SynchronizedLongBuffer(int size) { if (size != 0) data = new long[size]; }
  SynchronizedLongBuffer(Iterable l) {
    if (l instanceof Collection) allocate(((Collection) l).size());
    addAll(l);
  }
  
  public synchronized void add(long i) {
    if (size >= lLongArray(data)) {
      data = resizeLongArray(data, Math.max(1, toInt(Math.min(maximumSafeArraySize(), lLongArray(data)*2L))));
      if (size >= data.length) throw fail("LongBuffer too large: " + size);
    }
    data[size++] = i;
  }
  
  synchronized void allocate(int n) {
    data = resizeLongArray(data, max(n, size()));
  }
  
  public synchronized void addAll(Iterable l) {
    if (l != null) for (long i : l) add(i);
  }
  
  public synchronized long[] toArray() {
    return size == 0 ? null : resizeLongArray(data, size);
  }
  
  synchronized List toList() {
    return longArrayToList(data, 0, size);
  }
  
  public synchronized List asVirtualList() {
    return listFromFunction(__47 -> get(__47), size);
  }
  
  synchronized void reset() { size = 0; }
  void clear() { reset(); }
  
  synchronized public int size() { return size; }
  public synchronized boolean isEmpty() { return size == 0; }
  
  public synchronized long get(int idx) {
    if (idx >= size) throw fail("Index out of range: " + idx + "/" + size);
    return data[idx];
  }
  
  synchronized void set(int idx, long value) {
    if (idx >= size) throw fail("Index out of range: " + idx + "/" + size);
    data[idx] = value;
  }
  
  public synchronized long popLast() {
    if (size == 0) throw fail("empty buffer");
    return data[--size];
  }
  
  public synchronized long last() { return data[size-1]; }
  synchronized long nextToLast() { return data[size-2]; }
  
  public String toString() { return squareBracket(joinWithSpace(toList())); }
  
  public synchronized Iterator iterator() {
    return new IterableIterator() {
      int i = 0;
      
      public boolean hasNext() { return i < size(); }
      public Long next() {
        synchronized(SynchronizedLongBuffer.this) {
          if (!hasNext()) throw fail("Index out of bounds: " + i);
          return data[i++];
        }
      }
    };
  }
  
  public synchronized void trimToSize() {
    data = resizeLongArray(data, size);
  }
  
  synchronized void remove(int idx) {
    arraycopy(data, idx+1, data, idx, size-1-idx);
    --size;
  }
  
  // don't rely on return value if buffer is empty
  public synchronized long poll() { 
    return size == 0 ? -1 : data[--size];
  }
  
  public synchronized void insertAt(int idx, long[] l) {
    int n = l(l);
    if (n == 0) return;
    long[] newData = new long[size+n];
    arraycopy(data, 0, newData, 0, idx);
    arraycopy(l, 0, newData, idx, n);
    arraycopy(data, idx, newData, idx+n, size-idx);
    data = newData;
    size = newData.length;
  }
}
static class SynchronizedFloatBufferPresentingAsDoubles implements IDoubleBuffer {
  float[] data;
  int size;
  
  SynchronizedFloatBufferPresentingAsDoubles() {}
  SynchronizedFloatBufferPresentingAsDoubles(int size) { if (size != 0) data = new float[size]; }
  SynchronizedFloatBufferPresentingAsDoubles(Iterable l) { addAll(l); }
  SynchronizedFloatBufferPresentingAsDoubles(Collection l) { this(l(l)); addAll(l); }
  SynchronizedFloatBufferPresentingAsDoubles(double... data) { this.data = doubleArrayToFloatArray(data); size = l(data); }
  
  public synchronized void add(double i) {
    if (size >= lFloatArray(data)) {
      data = resizeFloatArray(data, Math.max(1, toInt(Math.min(maximumSafeArraySize(), lFloatArray(data)*2L))));
      if (size >= data.length) throw fail(shortClassName(this) + " too large: " + size);
    }
    data[size++] = (float) i;
  }
  
  public synchronized void addAll(Iterable l) {
    if (l != null) for (double i : l) add(i);
  }
  
  public synchronized double[] toArray() {
    return size == 0 ? null : takeFirstFromFloatArrayAsDoubleArray(data, size);
  }
  
  double[] toArrayNonNull() {
    return unnull(toArray());
  }
  
  synchronized List toList() {
    return floatArrayToDoubleList(data, 0, size);
  }
  synchronized List asVirtualList() {
    return new RandomAccessAbstractList() {
      public int size() { return SynchronizedFloatBufferPresentingAsDoubles.this.size(); }
      public Double get(int i) { return SynchronizedFloatBufferPresentingAsDoubles.this.get(i); }
      public Double set(int i, Double val) {
        synchronized(SynchronizedFloatBufferPresentingAsDoubles.this) {
          Double a = get(i);
          data[i] = val.floatValue();
          return a;
        }
      }
    };
  }
  
  synchronized void reset() { size = 0; }
  void clear() { reset(); }
  
  public synchronized int size() { return size; }
  public synchronized boolean isEmpty() { return size == 0; }
  
  public synchronized double get(int idx) {
    if (idx >= size) throw fail("Index out of range: " + idx + "/" + size);
    return data[idx];
  }
  
  synchronized void set(int idx, double value) {
    if (idx >= size) throw fail("Index out of range: " + idx + "/" + size);
    data[idx] = (float) value;
  }
  
  public synchronized double popLast() {
    if (size == 0) throw fail("empty buffer");
    return data[--size];
  }
  
  public synchronized double first() { return data[0]; }
  public synchronized double last() { return data[size-1]; }
  synchronized double nextToLast() { return data[size-2]; }
  
  public String toString() { return squareBracket(joinWithSpace(toList())); }
  
  public Iterator iterator() {
    return new IterableIterator() {
      int i = 0;
      
      public boolean hasNext() { return i < size(); }
      public Double next() {
        synchronized(SynchronizedFloatBufferPresentingAsDoubles.this) {
          //if (!hasNext()) fail("Index out of bounds: " + i);
          return (double) data[i++];
        }
      }
    };
  }
  
  /*public DoubleIterator doubleIterator() {
    ret new DoubleIterator {
      int i = 0;
      
      public bool hasNext() { ret i < size; }
      public int next() {
        //if (!hasNext()) fail("Index out of bounds: " + i);
        ret data[i++];
      }
      toString { ret "Iterator@" + i + " over " + DoubleBuffer.this; }
    };
  }*/
  
  public synchronized void trimToSize() {
    data = resizeFloatArray(data, size);
  }
  
  synchronized int indexOf(double b) {
    for (int i = 0; i < size; i++)
      if (data[i] == b)
        return i;
    return -1;
  }
  
  synchronized double[] subArray(int start, int end) {
    return subFloatArrayAsDoubleArray(data, start, min(end, size));
  }
  
  public synchronized void insertAt(int idx, double[] l) {
    int n = l(l);
    if (n == 0) return;
    float[] newData = new float[size+n];
    arraycopy(data, 0, newData, 0, idx);
    for (int i = 0; i < n; i++)
      newData[idx+i] = (float) l[i];
    arraycopy(data, idx, newData, idx+n, size-idx);
    data = newData;
    size = newData.length;
  }
}
enum HoldSide {
  SHORT, LONG;
  static HoldSide fromInt(double direction) {
    if (direction > 0) return LONG;
    if (direction < 0) return SHORT;
    throw fail("direction 0");
  }
  
  boolean isLong() { return this == LONG; }
  boolean isShort() { return this == SHORT; }
}
static class Q implements AutoCloseable {
  String name = "Unnamed Queue";
  List q = synchroLinkedList();
  ReliableSingleThread rst = new ReliableSingleThread(new Runnable() {  public void run() { try {  _run() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "_run()"; }});
   final public boolean getRetired(){ return retired(); }
public boolean retired() { return retired; }
volatile boolean retired = false;
   final public Runnable getCurrentJob(){ return currentJob(); }
public Runnable currentJob() { return currentJob; }
volatile Runnable currentJob;
  AtomicLong jobsDone = new AtomicLong();
  
  Q() {}
  Q(String name) {
  this.name = name;}
  
  void add(Runnable r) {
    assertNotRetired();
    q.add(r);
    _trigger();
  }
  
  void addInFront(Runnable r) {
    assertNotRetired();
    q.add(0, r);
    _trigger();
  }
  
  void _trigger() {
    rst.name = name;
    rst.go();
  }
  
  void add(Object r) {
    add(toRunnable(r));
  }
  
  void _run() {
    Runnable r;
    while (licensed() && !retired && (r = syncPopFirst(q)) != null) {
      currentJob = r;
      inc(jobsDone);
      try { r.run(); } catch (Throwable __e) { pcallFail(__e); }
      currentJob = null;
    }
    onIdle();
  }
  
  public void close() { retired = true; } // TODO: interrupt thread
  void done() {} // legacy function
  
  boolean isEmpty() { return q.isEmpty(); }
  int size() { return q.size(); }
  
  Object mutex() { return q; } // clients can synchronize on this
  List snapshot() { return cloneList(q); }
  
  // override this
  void onIdle() {}
  
  boolean busy() { return currentJob != null; }
  
  void assertNotRetired() {
    assertFalse("Queue is retired", retired());
  }
  
  boolean hasThread() { return rst.hasThread(); }
  
  long nJobsDone() { return jobsDone.get(); }
  
  public String toString() {
    return (retired ? "Retired " : "") + "Q " + systemHashCodeHex(this)
      + " (" + (isEmpty() ? "empty" : nEntries(size()) + ", current job: " + currentJob) + ")";
  }
}
interface ILongBuffer extends IntSize, Iterable, ILongQueue {
  void add(long d);
  void addAll(Iterable l);
  long[] toArray();
  long get(int idx);
  void trimToSize();
  long popLast();
  long last();
  boolean isEmpty();
  List asVirtualList();
  void insertAt(int idx, long[] l);
}
static class ConceptWithChangeListeners extends Concept implements IHasChangeListeners, ChangeTriggerable {
   transient Set onChange;
public  ConceptWithChangeListeners onChange(Runnable r) { onChange = createOrAddToSyncLinkedHashSet(onChange, r); return this; }
public  ConceptWithChangeListeners removeChangeListener(Runnable r) { main.remove(onChange, r); return this; }
public   void fireChange() {  if (onChange != null) for (var listener : onChange) pcallF_typed(listener); }
  
  void _onChange() { super._onChange();
    fireChange();
  }
  
  public void change() { super.change(); }
}
interface IDoubleBuffer extends IntSize, Iterable {
  void add(double d);
  void addAll(Iterable l);
  double[] toArray();
  double get(int idx);
  void trimToSize();
  double popLast();
  double first();
  double last();
  boolean isEmpty();
  void insertAt(int idx, double[] l);
}
interface IntSize {
  int size();
  
  default boolean isEmpty() { return size() == 0; }
}
interface Juiceable {
  // e.g. a position's profit before leverage
  double juiceValue();
}
static class FieldVar extends VarWithNotify {
  IHasChangeListeners containingObject;
  String fieldName;
  IF0 getter;
  IVF1 setter;
  FieldVar(IHasChangeListeners containingObject,
    String fieldName, IF0 getter, IVF1 setter) {
  this.setter = setter;
  this.getter = getter;
  this.fieldName = fieldName;
  this.containingObject = containingObject;
    containingObject.onChangeAndNow(() -> _updateFromObject());
  }
  
  void _updateFromObject() {
    set(getter.get());
  }
  
  public void fireChange() {
    setter.get(get());
  super.fireChange(); }
  
  public FieldVar onChange(IVF1 r) {
    if (r != null) onChange(() -> r.get(get()));
    return this;
  }
}
static class GeometricPriceCells implements PriceCells {
  GeometricPriceCells() {}
  // cell size in percent
   final public GeometricPriceCells setCellSizeInPercent(double cellSizeInPercent){ return cellSizeInPercent(cellSizeInPercent); }
public GeometricPriceCells cellSizeInPercent(double cellSizeInPercent) { this.cellSizeInPercent = cellSizeInPercent; return this; }  final public double getCellSizeInPercent(){ return cellSizeInPercent(); }
public double cellSizeInPercent() { return cellSizeInPercent; }
 double cellSizeInPercent;
  
  // one of the cell limits
   final public GeometricPriceCells setBasePrice(double basePrice){ return basePrice(basePrice); }
public GeometricPriceCells basePrice(double basePrice) { this.basePrice = basePrice; return this; }  final public double getBasePrice(){ return basePrice(); }
public double basePrice() { return basePrice; }
 double basePrice = 1000;
  
  GeometricPriceCells(double cellSizeInPercent) {
  this.cellSizeInPercent = cellSizeInPercent;}
  GeometricPriceCells(double basePrice, double cellSizeInPercent) {
  this.cellSizeInPercent = cellSizeInPercent;
  this.basePrice = basePrice;}
  
  double ratio() {
    return 1+cellSizeInPercent/100;
  }
  double toLogScale(double price) {
    return log(price, ratio());
  }
  
  double fromLogScale(double logPrice) {
    return pow(ratio(), logPrice);
  }
  double logBasePrice() {
    return toLogScale(basePrice);
  }
  double remainder(double price) {
    return remainder(toLogScale(price));
  }
  double remainderLog(double logPrice) {
    return frac(logPrice-logBasePrice());
  }
  // TODO: fix this logic's rounding problems
  
  public boolean isCellLimit(double price) {
    return remainder(price) == 0;
  }
  
  public double nextCellLimitLog(double logPrice) {
    double r = remainderLog(logPrice);
    return logPrice + 1-r;
  }
  
  public double nextCellLimit(double price) {
    return fromLogScale(nextCellLimitLog(toLogScale(price)));
  }
  
  public double previousCellLimitLog(double logPrice) {
    double r = remainderLog(logPrice);
    return logPrice - (r == 0 ? 1 : r);
  }
  
  public double previousCellLimit(double price) {
    return fromLogScale(previousCellLimitLog(toLogScale(price)));
  }
  
  public double nCellLimitsDown(double price, int n) {
    double logPrice = toLogScale(price);
    logPrice = previousCellLimitLog(logPrice)-(n-1);
    return fromLogScale(logPrice);
  }
  
  public double nCellLimitsUp(double price, int n) {
    double logPrice = toLogScale(price);
    logPrice = nextCellLimitLog(logPrice)+n;
    return fromLogScale(logPrice);
  }
  
  public double priceToCellNumber(double price) {
    return toLogScale(price)-logBasePrice();
  } 
  
  public double cellNumberToPrice(double cellNumber) {
    return fromLogScale(cellNumber+logBasePrice());
  }
  
  public String toString() {
    return formatDouble2(cellSizeInPercent) + "% cells with C0=" + formatPrice(basePrice);
  }
}
static class PriceDigitizer2 implements IFieldsToList{
  PriceCells cells;
  PriceDigitizer2() {}
  PriceDigitizer2(PriceCells cells) {
  this.cells = cells;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + cells + ")"; }public Object[] _fieldsToList() { return new Object[] {cells}; }
   final public PriceDigitizer2 setCellNumber(int cellNumber){ return cellNumber(cellNumber); }
public PriceDigitizer2 cellNumber(int cellNumber) { this.cellNumber = cellNumber; return this; }  final public int getCellNumber(){ return cellNumber(); }
public int cellNumber() { return cellNumber; }
 int cellNumber = Integer.MIN_VALUE;
   final public PriceDigitizer2 setLastCellNumber(int lastCellNumber){ return lastCellNumber(lastCellNumber); }
public PriceDigitizer2 lastCellNumber(int lastCellNumber) { this.lastCellNumber = lastCellNumber; return this; }  final public int getLastCellNumber(){ return lastCellNumber(); }
public int lastCellNumber() { return lastCellNumber; }
 int lastCellNumber = Integer.MIN_VALUE;
   final public PriceDigitizer2 setVerbose(boolean verbose){ return verbose(verbose); }
public PriceDigitizer2 verbose(boolean verbose) { this.verbose = verbose; return this; }  final public boolean getVerbose(){ return verbose(); }
public boolean verbose() { return verbose; }
 boolean verbose = false;
   final public PriceDigitizer2 setUpDownSequence(UpDownSequence upDownSequence){ return upDownSequence(upDownSequence); }
public PriceDigitizer2 upDownSequence(UpDownSequence upDownSequence) { this.upDownSequence = upDownSequence; return this; }  final public UpDownSequence getUpDownSequence(){ return upDownSequence(); }
public UpDownSequence upDownSequence() { return upDownSequence; }
 UpDownSequence upDownSequence = new UpDownSequence();
  
  // returns new digitized price
  double digitize(double price) {
    double cn = cells.toCellNumber(price);
    if (cellNumber == Integer.MIN_VALUE) {
      cellNumber = lastCellNumber = iround(cn);
    } else {
      lastCellNumber = cellNumber;
      cellNumber = iroundTowardsWithOutwardEpsilon(cn, cellNumber, epsilon());
      
      // TODO: assuming there are only 1-steps
      if (cellNumber > lastCellNumber)
        { if (upDownSequence != null) upDownSequence.addUp(); }
      else if (cellNumber < lastCellNumber)
        { if (upDownSequence != null) upDownSequence.addDown(); }
    }
    return digitizedPrice();
  }
  
  double epsilon() { return 1e-4; }
  
  double digitizedPrice() { return cells.fromCellNumber(cellNumber); }
  double lastDigitizedPrice() { return cells.fromCellNumber(lastCellNumber); }
  
  // digitize price without looking at history
  double digitizeIndividually(double price) {
    double cn = cells.toCellNumber(price);
    return cells.fromCellNumber(round(cn));
  }
  
  PriceCells priceCells() { return cells; }
  
  // brave
  void swapPriceCells(PriceCells newPriceCells) {
    cells = newPriceCells;
  }
}
static class DoubleBuffer implements Iterable, IntSize {
  double[] data;
  int size;
  
  DoubleBuffer() {}
  DoubleBuffer(int size) { if (size != 0) data = new double[size]; }
  DoubleBuffer(Iterable l) { addAll(l); }
  DoubleBuffer(Collection l) { this(l(l)); addAll(l); }
  DoubleBuffer(double... data) { this.data = data; size = l(data); }
  
  void add(double i) {
    if (size >= lDoubleArray(data)) {
      data = resizeDoubleArray(data, Math.max(1, toInt(Math.min(maximumSafeArraySize(), lDoubleArray(data)*2L))));
      if (size >= data.length) throw fail("DoubleBuffer too large: " + size);
    }
    data[size++] = i;
  }
  
  void addAll(Iterable l) {
    if (l != null) for (double i : l) add(i);
  }
  
  double[] toArray() {
    return size == 0 ? null : resizeDoubleArray(data, size);
  }
  
  double[] toArrayNonNull() {
    return unnull(toArray());
  }
  
  List toList() {
    return doubleArrayToList(data, 0, size);
  }
  List asVirtualList() {
    return new RandomAccessAbstractList() {
      public int size() { return size; }
      public Double get(int i) { return DoubleBuffer.this.get(i); }
      public Double set(int i, Double val) {
        Double a = get(i);
        data[i] = val;
        return a;
      }
    };
  }
  
  void reset() { size = 0; }
  void clear() { reset(); }
  
  public int size() { return size; }
  double get(int idx) {
    if (idx >= size) throw fail("Index out of range: " + idx + "/" + size);
    return data[idx];
  }
  
  void set(int idx, double value) {
    if (idx >= size) throw fail("Index out of range: " + idx + "/" + size);
    data[idx] = value;
  }
  
  double popLast() {
    if (size == 0) throw fail("empty buffer");
    return data[--size];
  }
  
  double last() { return data[size-1]; }
  double nextToLast() { return data[size-2]; }
  
  public String toString() { return squareBracket(joinWithSpace(toList())); }
  
  public Iterator iterator() {
    return new IterableIterator() {
      int i = 0;
      
      public boolean hasNext() { return i < size; }
      public Double next() {
        //if (!hasNext()) fail("Index out of bounds: " + i);
        return data[i++];
      }
    };
  }
  
  /*public DoubleIterator doubleIterator() {
    ret new DoubleIterator {
      int i = 0;
      
      public bool hasNext() { ret i < size; }
      public int next() {
        //if (!hasNext()) fail("Index out of bounds: " + i);
        ret data[i++];
      }
      toString { ret "Iterator@" + i + " over " + DoubleBuffer.this; }
    };
  }*/
  
  void trimToSize() {
    data = resizeDoubleArray(data, size);
  }
  
  int indexOf(double b) {
    for (int i = 0; i < size; i++)
      if (data[i] == b)
        return i;
    return -1;
  }
  
  double[] subArray(int start, int end) {
    return subDoubleArray(data, start, min(end, size));
  }
}
static class PricePoint implements IFieldsToList{
  static final String _fieldOrder = "timestamp price";
  long timestamp;
  double price;
  PricePoint() {}
  PricePoint(long timestamp, double price) {
  this.price = price;
  this.timestamp = timestamp;}
public boolean equals(Object o) {
if (!(o instanceof PricePoint)) return false;
    PricePoint __1 =  (PricePoint) o;
    return timestamp == __1.timestamp && price == __1.price;
}
  public int hashCode() {
    int h = 516290023;
    h = boostHashCombine(h, _hashCode(timestamp));
    h = boostHashCombine(h, _hashCode(price));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {timestamp, price}; }
  public String toString() {
    return formatPrice(price) + " @ " + formatLocalDateWithSeconds(timestamp);
  }
  
  final long timestamp(){ return time(); }
long time() { return timestamp; }
  double price() { return price; }
}
interface PriceCells {
  boolean isCellLimit(double price);
  
  // next-higher cell limit or NaN if no more cells
  double nextCellLimit(double price);
  
  // next-lower cell limit or NaN if no more cells
  double previousCellLimit(double price);
  
  // go n steps down
  double nCellLimitsDown(double price, int n);
  
  // go n steps up
  double nCellLimitsUp(double price, int n);
  
  // convert to "cell space" (0=first cell limit, 1=next cell limit, -1=previous cell limit etc)
  default double toCellNumber(double price){ return priceToCellNumber(price); }
double priceToCellNumber(double price);
  
  default double fromCellNumber(double cellNumber){ return cellNumberToPrice(cellNumber); }
double cellNumberToPrice(double cellNumber);
  
  default double basePrice() { return cellNumberToPrice(0); }
}
// We use big-endian as DataOutputStream does
static class ByteHead /*is DataOutput*/ implements AutoCloseable {
   final public ByteHead setReadMode(boolean readMode){ return readMode(readMode); }
public ByteHead readMode(boolean readMode) { this.readMode = readMode; return this; }  final public boolean getReadMode(){ return readMode(); }
public boolean readMode() { return readMode; }
 boolean readMode = false;
   final public ByteHead setWriteMode(boolean writeMode){ return writeMode(writeMode); }
public ByteHead writeMode(boolean writeMode) { this.writeMode = writeMode; return this; }  final public boolean getWriteMode(){ return writeMode(); }
public boolean writeMode() { return writeMode; }
 boolean writeMode = false;
   final public InputStream getInputStream(){ return inputStream(); }
public InputStream inputStream() { return inputStream; }
 InputStream inputStream;
   final public OutputStream getOutputStream(){ return outputStream(); }
public OutputStream outputStream() { return outputStream; }
 OutputStream outputStream;
   final public ByteHead setByteCounter(long byteCounter){ return byteCounter(byteCounter); }
public ByteHead byteCounter(long byteCounter) { this.byteCounter = byteCounter; return this; }  final public long getByteCounter(){ return byteCounter(); }
public long byteCounter() { return byteCounter; }
 long byteCounter;
   final public boolean getEof(){ return eof(); }
public boolean eof() { return eof; }
 boolean eof = false;
  
  ByteHead() {}
  ByteHead(InputStream inputStream) { inputStream(inputStream); }
  ByteHead(OutputStream outputStream) { outputStream(outputStream); }
  
  ByteHead inputStream(InputStream inputStream) { this.inputStream = inputStream; readMode(true); return this; }
  ByteHead outputStream(OutputStream outputStream) { this.outputStream = outputStream; writeMode(true); return this; }
  
  void write(byte[] data) { try {
    ensureWriteMode();
    { if (outputStream != null) outputStream.write(data); }
    byteCounter += data.length;
  } catch (Exception __e) { throw rethrow(__e); } }
  
  void writeFloat(float f) {
    writeInt(Float.floatToIntBits(f));
  }
  
  void writeLong(long l) {
    writeInt((int) (l >> 32));
    writeInt((int) l);
  }
  
  void writeInt(int i) {
    write(i >> 24);
    write(i >> 16);
    write(i >> 8);
    write(i);
  }
  
  void writeShort(int i) {
    write(i >> 8);
    write(i);
  }
  
  final void write(int i){ writeByte(i); }
void writeByte(int i) { try {
    ensureWriteMode();
    { if (outputStream != null) outputStream.write(i); }
    byteCounter++;
  } catch (Exception __e) { throw rethrow(__e); } }
  
  void writeASCII(char c) {
    write(toASCII(c));
  }
  
  void writeASCII(String s) {
    write(toASCII(s));
  }
  
  // write/verify constant ASCII text
  void exchangeASCII(String s) {
    exchangeConstantBytes(toASCII(s));
  }
  
  void exchangeConstantBytes(byte[] data) {
    for (int i = 0; i < l(data); i++)
      exchangeByte(data[i]);
  }
  
  long readLong() {
    long i = ((long) readInt()) << 32;
    return i | (readInt() & 0xFFFFFFFFL);
  }
  
  float readFloat() {
    return Float.intBitsToFloat(readInt());
  }
  
  int readInt() {
    int i = read() << 24;
    i |= read() << 16;
    i |= read() << 8;
    return i | read();
  }
  
  short readShort() {
    int i = read() << 8;
    return (short) (i | read());
  }
  
  byte[] readBytes(int n) {
    byte[] data = new byte[n];
    for (int i = 0; i < n; i++) {
      int b = read();
      if (b < 0)
        throw fail("EOF");
      data[i] = (byte) b; 
    }
    return data;
  }
  
  String readString() {
    int n = readInt();
    if (eof()) return null;
    return fromUtf8(readBytes(n));
  }
  
  // null is written as empty string
  // writes UTF8 length (4 bytes) plus string as UTF8
  void writeString(String s) {
    byte[] utf8 = toUtf8(unnull(s));
    writeInt(l(utf8));
    write(utf8);
  }
  
  // -1 for EOF
  final int read(){ return readByte(); }
int readByte() { try {
    ensureReadMode();
    ++byteCounter;
    int b = inputStream.read();
    if (b < 0) eof = true;
    return b;
  } catch (Exception __e) { throw rethrow(__e); } }
  
  void ensureReadMode() {
    if (!readMode) throw fail("Not in read mode");
  }
  
  void ensureWriteMode() {
    if (!writeMode) throw fail("Not in write mode");
  }
  
  // exchange = read or write depending on mode
  
  void exchangeByte(byte getter, IVF1 setter) {
    exchangeByte(() -> getter, setter);
  }
  
  void exchangeByte(IF0 getter, IVF1 setter) {
    if (writeMode())
      writeByte(getter.get());
    
    if (readMode())
      setter.get(toUByte(readByte()));
  }
  
  void exchangeShort(IF0 getter, IVF1 setter) {
    if (writeMode())
      writeShort(getter.get());
    
    if (readMode())
      setter.get(readShort());
  }
  
  void exchangeLong(IVar var) {
    exchangeLong(var.getter(), var.setter());
  }
  
  void exchangeLong(IF0 getter, IVF1 setter) {
    if (writeMode())
      writeLong(getter.get());
    if (readMode())
      setter.get(readLong());
  }
  
  void exchangeByte(byte i) {
    exchangeByte(() -> i, j -> assertEquals(i, j));
  }
  
  void exchangeInt(int i) {
    exchangeInt(() -> i, j -> assertEquals(i, j));
  }
  
  void exchangeInt(IF0 getter, IVF1 setter) {
    if (writeMode())
      writeInt(getter.get());
    if (readMode())
      setter.get(readInt());
  }
  
  void exchange(ByteIO writable) {
    if (writable != null) writable.readWrite(this);
  }
  
  void exchangeAll(Iterable extends ByteIO> writables) {  
    if (writables != null)
      for (var writable : writables)
        exchange(writable);
  }
  
  // write size in bytes of element first (as int),
  // then the element itself.
  // upon reading, size is actually ignored.
  void exchangeWithSize(ByteIO writable) {
    if (writeMode()) {
      byte[] data = writable.saveToByteArray();
      writeInt(l(data));
      write(data);
    }
    
    if (readMode()) {
      int n = readInt();
      writable.readWrite(this);
    }
  }
  
  void finish() {}
  
  public void close() {
    main.close(inputStream);
    main.close(outputStream);
    finish();
  }
}
// you still need to implement hasNext() and next()
static abstract class IterableIterator implements Iterator, Iterable {
  public Iterator iterator() {
    return this;
  }
  
  public void remove() {
    unsupportedOperation();
  }
}
// Each instance is for one coin
static class G22DriftSystem extends MetaWithChangeListeners {
   final public G22DriftSystem setCryptoCoin(String cryptoCoin){ return cryptoCoin(cryptoCoin); }
public G22DriftSystem cryptoCoin(String cryptoCoin) { this.cryptoCoin = cryptoCoin; return this; }  final public String getCryptoCoin(){ return cryptoCoin(); }
public String cryptoCoin() { return cryptoCoin; }
 String cryptoCoin;
  
  // Position currently taken on platform (in crypto coin units)
  // Positive for long position, negative for short position,
  // zero for no position.
   final public G22DriftSystem setDriftOnPlatform(double driftOnPlatform){ return driftOnPlatform(driftOnPlatform); }
public G22DriftSystem driftOnPlatform(double driftOnPlatform) { this.driftOnPlatform = driftOnPlatform; return this; }  final public double getDriftOnPlatform(){ return driftOnPlatform(); }
public double driftOnPlatform() { return driftOnPlatform; }
 double driftOnPlatform;
  
  // Drift value we want to have
   final public G22DriftSystem setTargetDrift(double targetDrift){ return targetDrift(targetDrift); }
public G22DriftSystem targetDrift(double targetDrift) { this.targetDrift = targetDrift; return this; }  final public double getTargetDrift(){ return targetDrift(); }
public double targetDrift() { return targetDrift; }
 double targetDrift;
  
   final public G22DriftSystem setMarket(IFuturesMarket market){ return market(market); }
public G22DriftSystem market(IFuturesMarket market) { this.market = market; return this; }  final public IFuturesMarket getMarket(){ return market(); }
public IFuturesMarket market() { return market; }
transient IFuturesMarket market;
  
  public transient FieldVar varCoinParams_cache;
public FieldVar varCoinParams() { if (varCoinParams_cache == null) varCoinParams_cache = varCoinParams_load(); return varCoinParams_cache;}
public FieldVar varCoinParams_load() {
        return new FieldVar(this, "coinParams", () -> coinParams(), coinParams -> coinParams(coinParams)); }
 final public G22DriftSystem setCoinParams(FutureCoinParameters coinParams){ return coinParams(coinParams); }
public G22DriftSystem coinParams(FutureCoinParameters coinParams) { if (!eq(this.coinParams, coinParams)) { this.coinParams = coinParams; change(); } return this; }
 final public FutureCoinParameters getCoinParams(){ return coinParams(); }
public FutureCoinParameters coinParams() { return coinParams; }
 FutureCoinParameters coinParams;
  
  Set activeStrategies = syncLinkedHashSet();
  
  double calculateTargetDrift() {
    double drift = 0;
    for (var strat : cloneList(activeStrategies))
      drift += strat.drift();
    targetDrift(drift);
    return drift;
  }
}
static class TimestampRange implements Comparable , IFieldsToList{
  static final String _fieldOrder = "start end";
  Timestamp start;
  Timestamp end;
  TimestampRange() {}
  TimestampRange(Timestamp start, Timestamp end) {
  this.end = end;
  this.start = start;}
public boolean equals(Object o) {
if (!(o instanceof TimestampRange)) return false;
    TimestampRange __1 =  (TimestampRange) o;
    return eq(start, __1.start) && eq(end, __1.end);
}
  public int hashCode() {
    int h = -2020922905;
    h = boostHashCombine(h, _hashCode(start));
    h = boostHashCombine(h, _hashCode(end));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {start, end}; }
  TimestampRange(long start, long end) {
    this(new Timestamp(start), new Timestamp(end));
  }
  
  final Timestamp startTime(){ return start(); }
Timestamp start() { return start; }
  final Timestamp endTime(){ return end(); }
Timestamp end() { return end; }
  
  final TimestampRange startTime(Timestamp start){ return start(start); }
TimestampRange start(Timestamp start) { this.start = start; return this; }
  final TimestampRange endTime(Timestamp end){ return end(end); }
TimestampRange end(Timestamp end) { this.end = end; return this; }
  
  boolean complete() { return start != null && end != null; }
  
  Duration duration() { return !complete() ? null : end.minusAsDuration(start); }
  
  long length() { return !complete() ? 0 : end.unixDate()-start.unixDate(); }
  
  public String toString() { return start + " to " + end; }
  
  public int compareTo(TimestampRange r) {
    return cmp(start, r.start);
  }
  
  List toList() { return ll(start, end); }
}
abstract static class AbstractJuicer extends MetaWithChangeListeners {
  AbstractJuicer() {}
  // what we are monitoring & closing (a position or a strategy)
  public transient FieldVar varJuiceable_cache;
public FieldVar varJuiceable() { if (varJuiceable_cache == null) varJuiceable_cache = varJuiceable_load(); return varJuiceable_cache;}
public FieldVar varJuiceable_load() {
        return new FieldVar(this, "juiceable", () -> juiceable(), juiceable -> juiceable(juiceable)); }
 final public AbstractJuicer setJuiceable(Juiceable juiceable){ return juiceable(juiceable); }
public AbstractJuicer juiceable(Juiceable juiceable) { if (!eq(this.juiceable, juiceable)) { this.juiceable = juiceable; change(); } return this; }
 final public Juiceable getJuiceable(){ return juiceable(); }
public Juiceable juiceable() { return juiceable; }
 Juiceable juiceable;
  
  public transient FieldVar varJuiceValue_cache;
public FieldVar varJuiceValue() { if (varJuiceValue_cache == null) varJuiceValue_cache = varJuiceValue_load(); return varJuiceValue_cache;}
public FieldVar varJuiceValue_load() {
        return new FieldVar(this, "juiceValue", () -> juiceValue(), juiceValue -> juiceValue(juiceValue)); }
 final public AbstractJuicer setJuiceValue(double juiceValue){ return juiceValue(juiceValue); }
public AbstractJuicer juiceValue(double juiceValue) { if (!eq(this.juiceValue, juiceValue)) { this.juiceValue = juiceValue; change(); } return this; }
 final public double getJuiceValue(){ return juiceValue(); }
public double juiceValue() { return juiceValue; }
 double juiceValue;
  
  public transient FieldVar varClosingAllowed_cache;
public FieldVar varClosingAllowed() { if (varClosingAllowed_cache == null) varClosingAllowed_cache = varClosingAllowed_load(); return varClosingAllowed_cache;}
public FieldVar