persistable sclass MultiCallbackJuicer {
  // Must have at least one level, otherwise no closing at all
  
  sclass Level {
    // At juice value minJuiceValue, define the callback rate.
    // Callback rates are interpolated between levels.
    settable double minJuiceValue;
    settable double callback;
    
    toString { ret "Callback " + keepPercentage + "% starting at " + formatDouble2(minJuiceValue); }
  }
  
  settableWithVar new L<Level> levels;
  
  // Highest juice value seen
  settableWithVar double crest = -infinity();
  
  settableWithVar double currentCallback;
  
  {
    onCalculatingCloseSignals(signals -> {
      if (juiceValue > crest)
        crest(juiceValue);
        
      currentCallback(callbackForJuiceValue(crest));

      var signal = new CloseSignal().createdBy(this).reason(formatDouble2(currentCallback) + "% Callback");
      signal.strength(doubleRatio(crest-juiceValue, currentCallback)*100);
      signals.add(signal);
    });
  }
  
  double callbackForJuiceValue(double juice) {
    for (int i = l(levels)-1; i >= 0; i--) {
      Level level = levels.get(i);
      if (juice < level.minJuiceValue)
        continue;
        
      Level nextLevel = get(levels, i+1);
      if (nextLevel == null)
        ret level;
      
      ret blend(level.keepPercentage, nextLevel.keepPercentage,
        transformToZeroToOne(juice, level.minJuiceValue, nextLevel.minJuiceValue));
    }
    
    ret empty(levels) ? 1000 : first(levels).callback;
  }
  
  void addLevel(double minJuiceValue, double callback) {
    levels.add(new Level().minJuiceValue(minJuiceValue).callback(keepPercentage));
  }
  
  toString {
    ret commaCombine(
      shortClassName(this),
      levels,
      crest == -infinity() ? null : "Highest profit seen: " + formatDouble2(crest),
      "Current callback: " + formatDouble2(currentCallback),
    );
  }
  
  void copyTransientValuesFrom(AbstractJuicer juicer) {
    if (juicer cast MultiCallbackJuicer)
      crest(juicer.crest);
  }
}