Repository: hummingbot/hummingbot
Pull request: Feat/pmm price bands
**Before submitting this PR, please make sure**:
- [X] Your code builds clean without any errors or warnings
- [X] You are using approved title ("feat/", "fix/", "docs/", "refactor/", etc)


**Optional**:
- Related Github issue: #1713
- Related Clubhouse Story:


**A description of the changes proposed in the pull request**:

Adds price bands to PMM. If price is above upper band, only sell orders are placed. If price is below lower band, only buy orders are placed.

Problem statement:
New PMM parameter: price bands 
* Add 2 new parameters: `price_ceiling` and `price_floor`
* Set them at -1 by default, which means they are not used
* If the price is within the band set by `floor_price` and `ceiling_price`, run the bot as normal
* If price goes above `price_ceiling`, only place sell orders
* If price falls below `price_floor`, only place buy orders

Hints from issue discussion:
Hey @mifeng, I've been pocking around in the repo the last couple of days and I'd like to take a crack at a contribution with this if it's up for grabs.
@petioptrv Wow that would be great! 

Note that we are big on unit tests, so if you're working on this feature, please add a test for it in https://github.com/CoinAlpha/hummingbot/tree/master/test

If you have any questions, you can ask them here or on the #dev channel in https://discord.hummingbot.io.
@mifeng will definitely add tests! I did run into some issues however. Apparently PyCharm does not support Cython debugging, so I only get `Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)` when I try to run the current test cases (after I've made the changes to PMM). Do you use an IDE for that or you use directly gbd?
To run tests, run `nosetests <filename>` in the `test` directory
Hey @petioptrv I assigned you to this story so that other devs don't work on it. 

Let me know if you have any questions as you begin to work on it - thanks!

Commit message: Merge branch 'development' into feat/pmm-price-bands

Patch to review:
diff --git a/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py b/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py
index b7c37cfeb9d..19841ab6b98 100644
--- a/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py
+++ b/hummingbot/strategy/pure_market_making/pure_market_making_config_map.py
@@ -55,7 +55,7 @@ def validate_order_amount(value: str) -> Optional[str]:
         if Decimal(value) < min_amount:
             return f"Order amount must be at least {min_amount}."
     except Exception:
-        return f"Invalid order amount."
+        return "Invalid order amount."
 
 
 def price_source_market_prompt():
@@ -132,6 +132,20 @@ def exchange_on_validated(value: str):
                   type_str="decimal",
                   validator=validate_order_amount,
                   prompt_on_new=True),
+    "price_ceiling":
+        ConfigVar(key="price_ceiling",
+                  prompt="Enter the price point above which only sell orders will be placed "
+                         "(Enter -1 to deactivate this feature) >>> ",
+                  type_str="decimal",
+                  default=Decimal("-1"),
+                  validator=lambda v: validate_decimal(v, min_value=0, inclusive=False) if not v == -1 else None),
+    "price_floor":
+        ConfigVar(key="price_floor",
+                  prompt="Enter the price below which only buy orders will be placed "
+                         "(Enter -1 to deactivate this feature) >>> ",
+                  type_str="decimal",
+                  default=Decimal("-1"),
+                  validator=lambda v: validate_decimal(v, min_value=0, inclusive=False) if not v == -1 else None),
     "order_expiration_time":
         ConfigVar(key="order_expiration_time",
                   prompt="How long should your limit orders remain valid until they "
diff --git a/hummingbot/strategy/pure_market_making/pure_market_making_v2.pxd b/hummingbot/strategy/pure_market_making/pure_market_making_v2.pxd
index 4abcc382142..fafe400bd5e 100644
--- a/hummingbot/strategy/pure_market_making/pure_market_making_v2.pxd
+++ b/hummingbot/strategy/pure_market_making/pure_market_making_v2.pxd
@@ -24,6 +24,8 @@ cdef class PureMarketMakingStrategyV2(StrategyBase):
         double _create_timestamp
         double _order_refresh_time
         double _expiration_seconds
+        object _price_ceiling
+        object _price_floor
         double _status_report_interval
         double _last_timestamp
         double _filled_order_delay
@@ -47,6 +49,9 @@ cdef class PureMarketMakingStrategyV2(StrategyBase):
                                                     object market_info,
                                                     object pricing_proposal,
                                                     list active_orders)
+    cdef object c_check_and_apply_price_bands_to_sizing_proposal(self,
+                                                                 object market_info,
+                                                                 object sizing_proposal)
     cdef tuple c_check_and_add_transaction_costs_to_pricing_proposal(self,
                                                                      object market_info,
                                                                      object pricing_proposal,
diff --git a/hummingbot/strategy/pure_market_making/pure_market_making_v2.pyx b/hummingbot/strategy/pure_market_making/pure_market_making_v2.pyx
index e737d535d84..5331d34a881 100644
--- a/hummingbot/strategy/pure_market_making/pure_market_making_v2.pyx
+++ b/hummingbot/strategy/pure_market_making/pure_market_making_v2.pyx
@@ -54,6 +54,7 @@ from .inventory_skew_calculator cimport c_calculate_bid_ask_ratios_from_base_ass
 
 NaN = float("nan")
 s_decimal_zero = Decimal(0)
+s_decimal_neg_one = Decimal(-1)
 s_logger = None
 
 
@@ -109,11 +110,15 @@ cdef class PureMarketMakingStrategyV2(StrategyBase):
                  status_report_interval: float = 900,
                  asset_price_delegate: AssetPriceDelegate = None,
                  expiration_seconds: float = NaN,
+                 price_ceiling: Decimal = s_decimal_neg_one,
+                 price_floor: Decimal = s_decimal_neg_one,
                  hanging_orders_cancel_pct: float = 0.1,
                  order_refresh_tolerance_pct: float = -1.0):
 
         if len(market_infos) < 1:
             raise ValueError(f"market_infos must not be empty.")
+        if price_ceiling != s_decimal_neg_one and price_ceiling < price_floor:
+            raise ValueError(f"Parameter price_ceiling cannot be lower than price_floor.")
 
         super().__init__()
         self._sb_order_tracker = PureMarketMakingOrderTracker()
@@ -130,6 +135,8 @@ cdef class PureMarketMakingStrategyV2(StrategyBase):
         self._all_markets_ready = False
         self._order_refresh_time = order_refresh_time
         self._expiration_seconds = expiration_seconds
+        self._price_ceiling = price_ceiling
+        self._price_floor = price_floor
         self._filled_order_delay = filled_order_delay
         self._add_transaction_costs_to_orders = add_transaction_costs_to_orders
 
@@ -564,6 +571,34 @@ cdef class PureMarketMakingStrategyV2(StrategyBase):
 
         return PricingProposal(updated_buy_order_prices, updated_sell_order_prices)
 
+    cdef object c_check_and_apply_price_bands_to_sizing_proposal(self,
+                                                                 object market_info,
+                                                                 object sizing_proposal):
+        """
+        Sets bid/ask order size to zero if current price is above/below price band limits.
+
+        :param market_info: Pure Market making Pair object.
+        :param sizing_proposal: The current sizing proposal.
+        :return: revised_sizing_proposal
+        """
+        cdef:
+            list buy_order_sizes = [order_size for order_size in sizing_proposal.buy_order_sizes]
+            list sell_order_sizes = [order_size for order_size in sizing_proposal.sell_order_sizes]
+
+        if self._asset_price_delegate is None:
+            top_bid_price = market_info.get_price(False)
+            top_ask_price = market_info.get_price(True)
+            asset_mid_price = (top_bid_price + top_ask_price) * Decimal("0.5")
+        else:
+            asset_mid_price = self._asset_price_delegate.c_get_mid_price()
+
+        if self._price_ceiling != s_decimal_neg_one and asset_mid_price > self._price_ceiling:
+            buy_order_sizes = [s_decimal_zero for order_size in sizing_proposal.buy_order_sizes]
+        if self._price_floor != s_decimal_neg_one and asset_mid_price < self._price_floor:
+            sell_order_sizes = [s_decimal_zero for order_size in sizing_proposal.sell_order_sizes]
+
+        return SizingProposal(buy_order_sizes, sell_order_sizes)
+
     cdef tuple c_check_and_add_transaction_costs_to_pricing_proposal(self,
                                                                      object market_info,
                                                                      object pricing_proposal,
@@ -704,6 +739,10 @@ cdef class PureMarketMakingStrategyV2(StrategyBase):
                                                                           market_info,
                                                                           active_non_hanging_orders,
                                                                           pricing_proposal)
+
+        sizing_proposal = self.c_check_and_apply_price_bands_to_sizing_proposal(market_info,
+                                                                                sizing_proposal)
+
         if self._add_transaction_costs_to_orders:
             no_order_placement, pricing_proposal = self.c_check_and_add_transaction_costs_to_pricing_proposal(
                 market_info,
diff --git a/hummingbot/strategy/pure_market_making/start.py b/hummingbot/strategy/pure_market_making/start.py
index d83a97c87d6..3450bc18864 100644
--- a/hummingbot/strategy/pure_market_making/start.py
+++ b/hummingbot/strategy/pure_market_making/start.py
@@ -28,6 +28,8 @@ def start(self):
         order_refresh_time = c_map.get("order_refresh_time").value
         bid_spread = c_map.get("bid_spread").value / Decimal('100')
         ask_spread = c_map.get("ask_spread").value / Decimal('100')
+        price_ceiling = c_map.get("price_ceiling").value
+        price_floor = c_map.get("price_floor").value
         order_expiration_time = c_map.get("order_expiration_time").value
         order_levels = c_map.get("order_levels").value
         order_level_amount = c_map.get("order_level_amount").value
@@ -120,6 +122,8 @@ def start(self):
                                                    logging_options=strategy_logging_options,
                                                    asset_price_delegate=asset_price_delegate,
                                                    expiration_seconds=order_expiration_time,
+                                                   price_ceiling=price_ceiling,
+                                                   price_floor=price_floor,
                                                    hanging_orders_cancel_pct=hanging_orders_cancel_pct,
                                                    order_refresh_tolerance_pct=order_refresh_tolerance_pct)
     except Exception as e:
diff --git a/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml
index c838db711c8..3ab21f5f8cb 100644
--- a/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml
+++ b/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml
@@ -2,7 +2,7 @@
 ###       Pure market making strategy config         ###
 ########################################################
 
-template_version: 13
+template_version: 14
 strategy: null
 
 # Exchange and token parameters.
@@ -34,6 +34,12 @@ order_refresh_tolerance_pct: null
 # Size of your bid and ask order.
 order_amount: null
 
+# Price band ceiling.
+price_ceiling: null
+
+# Price band floor.
+price_floor: null
+
 # Whether to enable Inventory skew feature (true/false).
 inventory_skew_enabled: null
 


Code review comments:
diff_hunk: @@ -132,6 +132,20 @@ def exchange_on_validated(value: str):
                   type_str="decimal",
                   validator=validate_order_amount,
                   prompt_on_new=True),
+    "price_ceiling":
+        ConfigVar(key="price_ceiling",
+                  prompt="Enter the price point above which only sell orders will be placed "
+                         "(Enter -1 to deactivate this feature) >>> ",
+                  type_str="decimal",
+                  default=Decimal("-1"),
+                  validator=lambda v: validate_decimal(v, min_value=0, inclusive=False) if not v == -1 else None),
+    "price_floor":
+        ConfigVar(key="price_floor",
+                  prompt="Enter the price below which only buy orders will be placed "
+                         "(Enter -1 to deactivate this feature) >>> ",
+                  type_str="decimal",
+                  default=Decimal("-1"),
+                  validator=lambda v: validate_decimal(v, min_value=0, inclusive=False) if not v == -1 else None),
path: hummingbot/strategy/pure_market_making/pure_market_making_config_map.py
text: @user1:
use `if v != -1` instead of `if not v == -1`

diff_hunk: @@ -132,6 +132,20 @@ def exchange_on_validated(value: str):
                   type_str="decimal",
                   validator=validate_order_amount,
                   prompt_on_new=True),
+    "price_ceiling":
+        ConfigVar(key="price_ceiling",
+                  prompt="Enter the price point above which only sell orders will be placed "
+                         "(Enter -1 to deactivate this feature) >>> ",
+                  type_str="decimal",
+                  default=Decimal("-1"),
+                  validator=lambda v: validate_decimal(v, min_value=0, inclusive=False) if not v == -1 else None),
path: hummingbot/strategy/pure_market_making/pure_market_making_config_map.py
text: @user1:
use `if v != -1` instead of `if not v == -1`