diff --git a/README.md b/README.md
index aa71729..a1273ee 100644
--- a/README.md
+++ b/README.md
@@ -192,6 +192,7 @@ Available strategies:
|**cost** | calculate cost for opening channel, and set ppm to cover cost when channel depletes.|**cost_factor**|
|**onchain_fee** | sets the fees to a % equivalent of a standard onchain payment (Requires --electrum-server to be specified.)| **onchain_fee_btc** BTC
within **onchain_fee_numblocks** blocks.|
|**proportional** | sets fee ppm according to balancedness.|**min_fee_ppm**
**max_fee_ppm**
**sum_peer_chans** consider all channels with peer for balance calculations|
+|**proportional_peer_inbound** | sets the fee rate according to balancedness around a range based on the peer's inbound weighted average fee rate|**min_fee_ppm**
**max_fee_ppm**
**sum_peer_chans** consider all channels with peer for balance calculations
**fee_avg_calc_cutoff_ppm** Ignore peer inbound ppm above this value
**avg_fee_ppm_multiplier** Tweak avg ppm by this multiplier (0..1)
**upper_fee_ppm_multiplier** Tweak upper ppm by this multiplier; max_fee_ppm will still be honored (0..1)|
|**disable** | disables the channel in the outgoing direction. Channel will be re-enabled again if it matches another policy (except when that policy uses an 'ignore' strategy).||
|**use_config** | process channel according to rules defined in another config file.|**config_file**|
@@ -203,6 +204,7 @@ All strategies (except the ignore strategy) will apply the following properties
| **min_htlc_msat** | Minimum size (in msat) of HTLC to allow | # msat |
| **max_htlc_msat** | Maximum size (in msat) of HTLC to allow | # msat |
| **max_htlc_msat_ratio** | Maximum size of HTLC to allow as a fraction of total channel capacity | 0..1 |
+| **max_htlc_proportional_slices** | Maximum size of HTLC set proportionally to local balance with slices number of steps | # slices |
| **time_lock_delta** | Time Lock Delta | # blocks |
| **min_fee_ppm_delta** | Minimum change in fees (ppm) before updating channel | ppm delta |
diff --git a/charge_lnd/strategy.py b/charge_lnd/strategy.py
index 2b2e1fc..d3f1e8c 100644
--- a/charge_lnd/strategy.py
+++ b/charge_lnd/strategy.py
@@ -6,6 +6,8 @@
from .config import Config
from .electrum import Electrum
+edges_cache = None
+
def debug(message):
sys.stderr.write(message + "\n")
@@ -19,6 +21,43 @@ def call_strategy(*args, **kwargs):
return call_strategy
return register_strategy
+def calculate_slices(max_value, current_value, num_slices):
+ # Calculate the size of each slice
+ slice_size = max_value // max(num_slices, 10)
+
+ # Find the slice number containing the current_value
+ current_slice = min(current_value // slice_size, num_slices - 1)
+
+ # Determine the upper value of the current slice without going over
+ slice_point = max(1, min(current_slice * slice_size, max_value))
+
+ return slice_point
+
+def get_ratio(channel, policy, **kwargs):
+ if policy.getbool('sum_peer_chans', False):
+ lnd = kwargs['lnd']
+ shared_chans=lnd.get_shared_channels(channel.remote_pubkey)
+ local_balance = 0
+ remote_balance = 0
+ for c in (shared_chans):
+ # Include balance of all active channels with peer
+ if c.active:
+ local_balance += c.local_balance
+ remote_balance += c.remote_balance
+ total_balance = local_balance + remote_balance
+ if total_balance == 0:
+ # Sum inactive channels because the node is likely offline with no active channels.
+ # When they come back online their fees won't be changed.
+ for c in (shared_chans):
+ if not c.active:
+ local_balance += c.local_balance
+ remote_balance += c.remote_balance
+ total_balance = local_balance + remote_balance
+ ratio = local_balance/total_balance
+ else:
+ ratio = channel.local_balance/(channel.local_balance + channel.remote_balance)
+
+ return ratio
class StrategyDelegate:
STRATEGIES = {}
@@ -48,6 +87,11 @@ def execute(self, channel):
def effective_max_htlc_msat(self, channel):
result = self.policy.getint('max_htlc_msat')
+
+ slices = self.policy.getint('max_htlc_proportional_slices')
+ if slices:
+ result = calculate_slices(channel.capacity, channel.local_balance, slices) * 1000
+
ratio = self.policy.getfloat('max_htlc_msat_ratio')
if ratio:
ratio = max(0,min(1,ratio))
@@ -81,28 +125,7 @@ def strategy_proportional(channel, policy, **kwargs):
if ppm_min is None or ppm_max is None:
raise Exception('proportional strategy requires min_fee_ppm and max_fee_ppm properties')
- if policy.getbool('sum_peer_chans', False):
- lnd = kwargs['lnd']
- shared_chans=lnd.get_shared_channels(channel.remote_pubkey)
- local_balance = 0
- remote_balance = 0
- for c in (shared_chans):
- # Include balance of all active channels with peer
- if c.active:
- local_balance += c.local_balance
- remote_balance += c.remote_balance
- total_balance = local_balance + remote_balance
- if total_balance == 0:
- # Sum inactive channels because the node is likely offline with no active channels.
- # When they come back online their fees won't be changed.
- for c in (shared_chans):
- if not c.active:
- local_balance += c.local_balance
- remote_balance += c.remote_balance
- total_balance = local_balance + remote_balance
- ratio = local_balance/total_balance
- else:
- ratio = channel.local_balance/(channel.local_balance + channel.remote_balance)
+ ratio = get_ratio(channel, policy, **kwargs)
ppm = int(ppm_min + (1.0 - ratio) * (ppm_max - ppm_min))
# clamp to 0..inf
@@ -118,6 +141,140 @@ def strategy_match_peer(channel, policy, **kwargs):
return (policy.getint('base_fee_msat', peernode_policy.fee_base_msat),
policy.getint('fee_ppm', peernode_policy.fee_rate_milli_msat))
+@strategy(name = 'proportional_peer_inbound')
+def strategy_proportional_peer_inbound(channel, policy, **kwargs):
+ lnd = kwargs['lnd']
+ chan_info = lnd.get_chan_info(channel.chan_id)
+ my_pubkey = lnd.get_own_pubkey()
+
+ peer_node_id = chan_info.node1_pub if chan_info.node2_pub == my_pubkey else chan_info.node2_pub
+
+ # avoid having to fetch get_edges() multiple times
+ global edges_cache
+
+ if not edges_cache:
+ edges_list = lnd.get_edges()
+ # cache only the data we need
+ edges = []
+ for edge in edges_list:
+ the_edge = {
+ "node1_pub": edge.node1_pub,
+ "node2_pub": edge.node2_pub,
+ 'capacity': edge.capacity,
+ "node1_policy": {
+ 'fee_rate_milli_msat': edge.node1_policy.fee_rate_milli_msat,
+ 'max_htlc_msat': edge.node1_policy.max_htlc_msat
+ },
+ "node2_policy": {
+ 'fee_rate_milli_msat': edge.node2_policy.fee_rate_milli_msat,
+ 'max_htlc_msat': edge.node2_policy.max_htlc_msat
+ }
+ }
+ edges.append(the_edge)
+ edges_cache = edges
+ else:
+ edges = edges_cache
+
+ peer_inbound = []
+
+ total_peer_capacity = 0
+ ppm_avg = 0
+
+ for edge in edges:
+ if edge['node1_pub'] == peer_node_id:
+ if edge['node2_pub'] == my_pubkey:
+ # ignore this edge if it's shared between our node and peer
+ continue
+ inbound = {
+ #'capacity': edge['capacity'],
+ # We will use the max_htlc_msat // 1000 as the estimated capacity
+ 'capacity': edge['node2_policy']['max_htlc_msat'] // 1000,
+ # We will take node2_policy because we want inbound policy, not outbound
+ 'fee_rate_milli_msat': edge['node2_policy']['fee_rate_milli_msat']
+ }
+ total_peer_capacity += inbound['capacity']
+ peer_inbound.append(inbound)
+ elif edge['node2_pub'] == peer_node_id:
+ if edge['node1_pub'] == my_pubkey:
+ # ignore this edge if it's shared between our node and peer
+ continue
+ inbound = {
+ #'capacity': edge['capacity'],
+ # We will use the max_htlc_msat // 1000 as the estimated capacity
+ 'capacity': edge['node1_policy']['max_htlc_msat'] // 1000,
+ # We will take node1_policy because we want inbound policy, not outbound
+ 'fee_rate_milli_msat': edge['node1_policy']['fee_rate_milli_msat']
+ }
+ peer_inbound.append(inbound)
+ total_peer_capacity += inbound['capacity']
+
+ # Calculate the weighted average inbound fee by multiplying each fee by
+ # the adjusted ratio and taking the sum.
+
+ fee_avg_calc_cutoff_ppm = policy.getint('fee_avg_calc_cutoff_ppm')
+
+ for inbound in peer_inbound:
+ if inbound['fee_rate_milli_msat'] >= fee_avg_calc_cutoff_ppm:
+ # ignore fee rate values over max_usable_ppm in our calculation
+ continue
+ ppm_avg += int((inbound['capacity']/total_peer_capacity)*inbound['fee_rate_milli_msat'])
+
+ # Use the same channel ratio calculation as proportional strategy
+ ratio = get_ratio(channel, policy, **kwargs)
+
+ ppm_min = policy.getint('min_fee_ppm')
+ ppm_max = policy.getint('max_fee_ppm')
+ avg_fee_ppm_multiplier = policy.getfloat('avg_fee_ppm_multiplier')
+ upper_fee_ppm_multiplier = policy.getfloat('upper_fee_ppm_multiplier')
+
+ if ppm_min is None or ppm_max is None or avg_fee_ppm_multiplier is None or upper_fee_ppm_multiplier is None:
+ raise Exception('proportional inbound weighted strategy requires min_fee_ppm, max_fee_ppm, avg_fee_ppm_multiplier, and upper_fee_ppm_multiplier properties')
+
+ if ppm_min >= ppm_max:
+ raise Exception('ppm_min should be less than ppm_max')
+
+ # The avg_fee_ppm_multiplier can tweak the ppm_avg to be slightly
+ # lower or higher, changing the center-point of the calculation.
+ # You might do this if you think the average values are all
+ # too cheap or too expensive. We will also make sure the average
+ # is not lower than ppm_min here, or higher than ppm_max.
+ # It's probably perfectly fine to leave this multiplier at 1.
+
+ ppm_avg = min(ppm_max, max(ppm_min, ppm_avg * avg_fee_ppm_multiplier))
+
+ # The upper_fee_ppm_multiplier sets the upper maximum when calculating
+ # the ppm when the local balance proportion drops below 0.5. A value
+ # of 2 means that the upper maximum is double the ppm_avg. If you
+ # wanted to increase fees more aggressively as the local balance falls,
+ # you could choose a higher multiplier.
+ # It's probably perfectly fine to leave this multiplier at 2.
+
+ ppm_upper = ppm_avg * upper_fee_ppm_multiplier
+
+ # When the ratio is near half, we want the ppm to be exactly
+ # the ppm_avg. When the ratio is lower, we want a higher ppm that
+ # is between ppm_avg and ppm_upper. When the ratio is higher,
+ # we want a lower ppm between ppm_avg and ppm_min. Since the range
+ # between can be unequal, e.g. average = 500, min = 400, max
+ # = 3000, we don't want to make the 0.5 ratio be the middle
+ # of the range 400 - 3000. It's much larger than the target
+ # average of 500. So we calculate the two sides separately.
+
+ if ratio < 0.5:
+ ppm = int(ppm_upper + 2 * (ppm_avg - ppm_upper) * ratio)
+ elif ratio > 0.5:
+ ppm = int(ppm_min + 2 * (ppm_avg - ppm_min) * (1 - ratio))
+ else:
+ ppm = ppm_avg
+
+ # Cap it to the max
+ ppm = min(ppm, ppm_max)
+
+ # clamp to 0..inf
+ ppm = max(ppm, 0)
+
+ return (policy.getint('base_fee_msat'), ppm)
+
@strategy(name = 'cost')
def strategy_cost(channel, policy, **kwargs):
lnd = kwargs['lnd']
diff --git a/examples/max-htlc-proportional.config b/examples/max-htlc-proportional.config
new file mode 100644
index 0000000..936dd7c
--- /dev/null
+++ b/examples/max-htlc-proportional.config
@@ -0,0 +1,24 @@
+# all channels max_htlc set to a value proportional to
+# local balance, with specified number of divisions
+# e.g.
+# slices = 10
+# channel_size = 1_000_000 sats
+# max_htlc values:
+# 100_000, 200_000, 300_000, 400_000, 500_000
+# 600_000, 700_000, 800_000, 900_000, 1_000_000
+#
+# slices = 2
+# channel_size = 800_000_sats
+# max_htlc values:
+# 400_000, 800_000
+#
+# Rationale: avoid insufficient local balance failures
+# to improve your node's reputation of successful forwarding.
+
+[default]
+strategy = ignore
+
+[proportional_htlc]
+chan.max_local_balance = 30_000_000
+strategy = static
+max_htlc_proportional_slices = 6
\ No newline at end of file
diff --git a/examples/weighted-average-of-peer-incoming-rate.config b/examples/weighted-average-of-peer-incoming-rate.config
new file mode 100644
index 0000000..dcd41c0
--- /dev/null
+++ b/examples/weighted-average-of-peer-incoming-rate.config
@@ -0,0 +1,44 @@
+# Use a fee that is proportional to the channel's local balance with
+# the ppm range surrounding the weighted average of the peer node's
+# inbound ppm rate.
+#
+# Example:
+# - Let's suppose you have a channel with Peer XYZ.
+# - Peer XYZ has 4 channels.
+# - Those 4 channels are 1M, 2M, 3M, and 4M sats capacity.
+# - The inbound fee rates for those channels are 100 ppm, 200 ppm,
+# 300 ppm, and 400 ppm.
+# - The weighted average is the sum of fees multiplied by the
+# channel's capacity. In this case, 300 ppm. Note, it's higher
+# than the plain average, because more capacity is at the 400 ppm
+# rate compared to the lower ppm rate.
+#
+# The `min_fee_ppm` and `max_fee_ppm` are absolute, and will override
+# whatever calculations are made.
+#
+# The `fee_avg_calc_cutoff_ppm` is a cutoff maximum value when
+# considering the peer's inbound channel ppm. We can exclude these
+# extreme values from the calculation to get a more reasonable avg.
+#
+# The `avg_fee_ppm_multiplier` shifts the calculated peer's avg to
+# higher or lower. This is useful if you think the avg is too low
+# or too high.
+#
+# The `upper_fee_ppm_multiplier` determines how aggresively you want
+# your ppm to rise as the local channel balance falls. A multiplier
+# of 2 sets the upper ppm fee to be double as balance approaches 0.
+
+[proportional_peer_inbound]
+strategy = proportional_peer_inbound
+node.min_shared_channels_active = 1
+base_fee_msat = 0
+min_fee_ppm_delta = 50
+max_htlc_proportional_slices = 6
+
+min_fee_ppm = 1
+max_fee_ppm = 99_999
+fee_avg_calc_cutoff_ppm = 5_000
+avg_fee_ppm_multiplier = 1.0
+upper_fee_ppm_multiplier = 2.0
+
+time_lock_delta = 18
\ No newline at end of file