Repository: hummingbot/hummingbot
Pull request: feat / Add inventory cost price type
Closes: #2314

**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/")

**After creating this PR, please make sure:**

- [x] You link the related GitHub issue or ticket

**A description of the changes proposed in the pull request**:


Problem statement:
[Feature] Pure Market Maker strategy using the inventory cost as mid price for new orders
**Feature Suggestion**
Implement a simpler and less-risk strategy that considers the average inventory buying cost as the reference for new orders.

Every time a bid type order is filled, the bot updates a new variable called `inventory_price`, which will store the volume-weighted average price of the inventory, and the strategy will use this value as the reference to define the ask and bid price of new orders, instead of the market mid-price.

Example:

First, the bot would have to set a price for the present inventory, and we have two options for that:

1 - The user defines the inventory cost starting price
2 - If there is no price entered, the bot could use the mid market price when `start` is entered

Either way, two values should be stored:

`inventory_size` - the number of assets on the account at the start of the bot run;
`inventory_price` - the price used on one of the two cases above

Then calculate:

`inventory_cost = inventory_size * inventory price`

The bot proceeds to send the bid and ask orders to the exchange using inventory_price as the mid-price, where:

`ask_price = inventory_price + inventory_price * ask_spread`
`bid_price = inventory_price - inventory_price * bid_spread`

When a bid order is filled:

`inventory_size = inventory_size + order_amount`
`inventory_cost = inventory_cost + (order_amount * bid_price)`

`inventory_price = inventory_cost / inventory_size`

When a ask order is filled:

`inventory_size = inventory_size - order_amount`
`inventory_cost = inventory_cost - (order_amount * ask_price)`

`inventory_price = inventory_cost / inventory_size`

When the new order cycle starts, repeat that calculation of `ask_price` and `bid_price` with the new value of `inventory_size`.

There is also the need for a check to compare the inventory_price with the market mid-price, to avoid sending orders as market taker:

`IF inventory_price > market_mid_price:

`ask_price = inventory_price + inventory_price * ask_spread`
`bid_price = market_mid_price - market_mid_price * bid_spread`

`IF inventory_price < market_mid_price:

`ask_price = market_mid_price + market_mid_price * ask_spread`
`bid_price = inventory_price - inventory_price * bid_spread`

**Impact**
This strategy is easier to understand, especially by those new to market making, since the bot will only create orders that will be profitable relative to the buying cost.

**Additional context**
It doesn't have to be a new strategy, but a trigger inside the pure_mm strategy, like ping_pong.

Hints from issue discussion:
@phbrgnomo is there a typo in section "When a ask order is filled:", should be

`inventory_cost = inventory_cost - (order_amount * ask_price)` ?
It's interesting how this simple strategy behaves when profits are realized: with every profit trade inventory cost is decreased, which means that upper bound for buy orders is decreased too.

Simple example: 
1. Buy 1 BTC at 5000 USD
1. Price goes up and you're sell 0.45 BTC at 10000
1. Inventory cost now is `5000 - 0.45 * 10000 = 500`
1. The strategy is now limited to place buy orders not higher than 500 USD/BTC

Commit message: (fix) Add default value for inventory_price

It's ok to set default because it doesn't set the default in database.
So, it doesn't affect anything

Patch to review:
diff --git a/hummingbot/client/command/config_command.py b/hummingbot/client/command/config_command.py
index b5da7a80bcb..05d56d54d52 100644
--- a/hummingbot/client/command/config_command.py
+++ b/hummingbot/client/command/config_command.py
@@ -6,12 +6,13 @@
 from decimal import Decimal
 import pandas as pd
 from os.path import join
+from sqlalchemy.orm import Session
 from hummingbot.client.settings import (
     GLOBAL_CONFIG_PATH,
     CONF_FILE_PATH,
 )
 from hummingbot.client.config.global_config_map import global_config_map
-from hummingbot.client.config.config_validators import validate_bool
+from hummingbot.client.config.config_validators import validate_bool, validate_decimal
 from hummingbot.client.config.config_helpers import (
     missing_required_configs,
     save_to_yml
@@ -19,6 +20,7 @@
 from hummingbot.client.config.security import Security
 from hummingbot.client.config.config_var import ConfigVar
 from hummingbot.core.utils.async_utils import safe_ensure_future
+from hummingbot.model.inventory_cost import InventoryCost
 from hummingbot.strategy.pure_market_making import (
     PureMarketMakingStrategy
 )
@@ -141,6 +143,8 @@ async def _config_single_key(self,  # type: HummingbotApplication
                 self._notify("Please follow the prompt to complete configurations: ")
             if config_var.key == "inventory_target_base_pct":
                 await self.asset_ratio_maintenance_prompt(config_map, input_value)
+            elif config_var.key == "inventory_price":
+                await self.inventory_price_prompt(config_map, input_value)
             else:
                 await self.prompt_a_config(config_var, input_value=input_value, assign_default=False)
             if self.app.to_stop_config:
@@ -219,3 +223,51 @@ async def asset_ratio_maintenance_prompt(self,  # type: HummingbotApplication
                     self.app.to_stop_config = False
                     return
                 await self.prompt_a_config(config_map["inventory_target_base_pct"])
+
+    async def inventory_price_prompt(
+        self,  # type: HummingbotApplication
+        config_map,
+        input_value=None,
+    ):
+        key = "inventory_price"
+        if input_value:
+            config_map[key].value = Decimal(input_value)
+        else:
+            exchange = config_map["exchange"].value
+            market = config_map["market"].value
+            base_asset, quote_asset = market.split("-")
+            balances = await UserBalances.instance().balances(
+                exchange, base_asset, quote_asset
+            )
+            if balances.get(base_asset) is None:
+                return
+
+            cvar = ConfigVar(
+                key="temp_config",
+                prompt=f"On {exchange}, you have {balances[base_asset]:.4f} {base_asset}. "
+                f"What was the price for this amount in {quote_asset}?  >>> ",
+                required_if=lambda: True,
+                type_str="decimal",
+                validator=lambda v: validate_decimal(
+                    v, min_value=Decimal("0"), inclusive=True
+                ),
+            )
+            await self.prompt_a_config(cvar)
+            config_map[key].value = cvar.value
+
+            try:
+                quote_volume = balances[base_asset] * cvar.value
+            except TypeError:
+                # TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'NoneType' - bad input / no input
+                self._notify("Inventory price not updated due to bad input")
+                return
+
+            session: Session = self.trade_fill_db.get_shared_session()
+            InventoryCost.add_volume(
+                session,
+                base_asset=base_asset,
+                quote_asset=quote_asset,
+                base_volume=balances[base_asset],
+                quote_volume=quote_volume,
+                overwrite=True,
+            )
diff --git a/hummingbot/core/event/events.py b/hummingbot/core/event/events.py
index a59c4fa86c0..c0ae6d38412 100644
--- a/hummingbot/core/event/events.py
+++ b/hummingbot/core/event/events.py
@@ -95,6 +95,7 @@ class PriceType(Enum):
     BestAsk = 3
     LastTrade = 4
     LastOwnTrade = 5
+    InventoryCost = 6
 
 
 class MarketTransactionFailureEvent(NamedTuple):
diff --git a/hummingbot/exceptions.py b/hummingbot/exceptions.py
new file mode 100644
index 00000000000..7b15f850a74
--- /dev/null
+++ b/hummingbot/exceptions.py
@@ -0,0 +1,6 @@
+class HummingbotBaseException(Exception):
+    pass
+
+
+class NoPrice(HummingbotBaseException):
+    pass
diff --git a/hummingbot/model/inventory_cost.py b/hummingbot/model/inventory_cost.py
new file mode 100644
index 00000000000..7844ff5d2c7
--- /dev/null
+++ b/hummingbot/model/inventory_cost.py
@@ -0,0 +1,75 @@
+from decimal import Decimal
+from typing import Optional
+
+from sqlalchemy import (
+    Column,
+    String,
+    Numeric,
+    Integer,
+    UniqueConstraint,
+)
+from sqlalchemy.orm import Session
+
+from . import HummingbotBase
+
+
+class InventoryCost(HummingbotBase):
+    __tablename__ = "InventoryCost"
+    __table_args__ = (
+        UniqueConstraint("base_asset", "quote_asset"),
+    )
+
+    id = Column(Integer, primary_key=True, nullable=False)
+    base_asset = Column(String(45), nullable=False)
+    quote_asset = Column(String(45), nullable=False)
+    base_volume = Column(Numeric(48, 18), nullable=False)
+    quote_volume = Column(Numeric(48, 18), nullable=False)
+
+    @classmethod
+    def get_record(
+        cls, sql_session: Session, base_asset: str, quote_asset: str
+    ) -> Optional["InventoryCost"]:
+        return (
+            sql_session.query(cls)
+            .filter(cls.base_asset == base_asset, cls.quote_asset == quote_asset)
+            .first()
+        )
+
+    @classmethod
+    def add_volume(
+        cls,
+        sql_session: Session,
+        base_asset: str,
+        quote_asset: str,
+        base_volume: Decimal,
+        quote_volume: Decimal,
+        overwrite: bool = False,
+    ) -> None:
+        if overwrite:
+            update = {
+                "base_volume": base_volume,
+                "quote_volume": quote_volume,
+            }
+        else:
+            update = {
+                "base_volume": cls.base_volume + base_volume,
+                "quote_volume": cls.quote_volume + quote_volume,
+            }
+
+        rows_updated: int = sql_session.query(cls).filter(
+            cls.base_asset == base_asset, cls.quote_asset == quote_asset
+        ).update(update)
+
+        if not rows_updated:
+            record = InventoryCost(
+                base_asset=base_asset,
+                quote_asset=quote_asset,
+                base_volume=float(base_volume),
+                quote_volume=float(quote_volume),
+            )
+            sql_session.add(record)
+
+        try:
+            sql_session.commit()
+        except Exception:
+            sql_session.rollback()
diff --git a/hummingbot/strategy/pure_market_making/__init__.py b/hummingbot/strategy/pure_market_making/__init__.py
index 02d5e41c191..3e98fb1a8b2 100644
--- a/hummingbot/strategy/pure_market_making/__init__.py
+++ b/hummingbot/strategy/pure_market_making/__init__.py
@@ -4,9 +4,11 @@
 from .asset_price_delegate import AssetPriceDelegate
 from .order_book_asset_price_delegate import OrderBookAssetPriceDelegate
 from .api_asset_price_delegate import APIAssetPriceDelegate
+from .inventory_cost_price_delegate import InventoryCostPriceDelegate
 __all__ = [
     PureMarketMakingStrategy,
     AssetPriceDelegate,
     OrderBookAssetPriceDelegate,
-    APIAssetPriceDelegate
+    APIAssetPriceDelegate,
+    InventoryCostPriceDelegate,
 ]
diff --git a/hummingbot/strategy/pure_market_making/inventory_cost_price_delegate.py b/hummingbot/strategy/pure_market_making/inventory_cost_price_delegate.py
new file mode 100644
index 00000000000..3fc4fa99f43
--- /dev/null
+++ b/hummingbot/strategy/pure_market_making/inventory_cost_price_delegate.py
@@ -0,0 +1,68 @@
+from decimal import Decimal, InvalidOperation
+
+from hummingbot.core.event.events import OrderFilledEvent, TradeType
+from hummingbot.exceptions import NoPrice
+from hummingbot.model.inventory_cost import InventoryCost
+from hummingbot.model.sql_connection_manager import SQLConnectionManager
+
+
+class InventoryCostPriceDelegate:
+    def __init__(self, sql: SQLConnectionManager, trading_pair: str) -> None:
+        self.base_asset, self.quote_asset = trading_pair.split("-")
+        self._session = sql.get_shared_session()
+
+    @property
+    def ready(self) -> bool:
+        return True
+
+    def get_price(self) -> Decimal:
+        record = InventoryCost.get_record(
+            self._session, self.base_asset, self.quote_asset
+        )
+        try:
+            price = record.quote_volume / record.base_volume
+        except (ZeroDivisionError, InvalidOperation, AttributeError):
+            raise NoPrice("Inventory cost delegate does not have price cost data yet")
+
+        return Decimal(price)
+
+    def process_order_fill_event(self, fill_event: OrderFilledEvent) -> None:
+        base_asset, quote_asset = fill_event.trading_pair.split("-")
+        quote_volume = fill_event.amount * fill_event.price
+        base_volume = fill_event.amount
+
+        for fee_asset, fee_amount in fill_event.trade_fee.flat_fees:
+            if fill_event.trade_type == TradeType.BUY:
+                if fee_asset == base_asset:
+                    base_volume -= fee_amount
+                elif fee_asset == quote_asset:
+                    quote_volume += fee_amount
+                else:
+                    # Ok, some other asset used (like BNB), assume that we paid in base asset for simplicity
+                    base_volume /= 1 + fill_event.trade_fee.percent
+            else:
+                if fee_asset == base_asset:
+                    base_volume += fee_amount
+                elif fee_asset == quote_asset:
+                    quote_volume -= fee_amount
+                else:
+                    # Ok, some other asset used (like BNB), assume that we paid in quote asset for simplicity
+                    quote_volume /= 1 + fill_event.trade_fee.percent
+
+        if fill_event.trade_type == TradeType.SELL:
+            base_volume = -base_volume
+            quote_volume = -quote_volume
+
+            # Make sure we're not going to create negative inventory cost here. If user didn't properly set cost,
+            # just assume 0 cost, so consequent buys will set cost correctly
+            record = InventoryCost.get_record(self._session, base_asset, quote_asset)
+            if record:
+                base_volume = max(-record.base_volume, base_volume)
+                quote_volume = max(-record.quote_volume, quote_volume)
+            else:
+                base_volume = Decimal("0")
+                quote_volume = Decimal("0")
+
+        InventoryCost.add_volume(
+            self._session, base_asset, quote_asset, base_volume, quote_volume
+        )
diff --git a/hummingbot/strategy/pure_market_making/pure_market_making.pxd b/hummingbot/strategy/pure_market_making/pure_market_making.pxd
index 02744b54883..44f0a0eb0d9 100644
--- a/hummingbot/strategy/pure_market_making/pure_market_making.pxd
+++ b/hummingbot/strategy/pure_market_making/pure_market_making.pxd
@@ -31,6 +31,8 @@ cdef class PureMarketMakingStrategy(StrategyBase):
         object _bid_order_optimization_depth
         bint _add_transaction_costs_to_orders
         object _asset_price_delegate
+        object _inventory_cost_price_delegate
+        bint _inventory_cost_allow_higher_bids
         object _price_type
         bint _take_if_crossed
         object _price_ceiling
diff --git a/hummingbot/strategy/pure_market_making/pure_market_making.pyx b/hummingbot/strategy/pure_market_making/pure_market_making.pyx
index 4441707a91c..b52bbdb97f9 100644
--- a/hummingbot/strategy/pure_market_making/pure_market_making.pyx
+++ b/hummingbot/strategy/pure_market_making/pure_market_making.pyx
@@ -20,6 +20,7 @@ from hummingbot.core.network_iterator import NetworkStatus
 from hummingbot.connector.exchange_base import ExchangeBase
 from hummingbot.connector.exchange_base cimport ExchangeBase
 from hummingbot.core.event.events import OrderType
+from hummingbot.exceptions import NoPrice
 
 from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple
 from hummingbot.strategy.strategy_base import StrategyBase
@@ -36,6 +37,7 @@ from .asset_price_delegate import AssetPriceDelegate
 from .inventory_skew_calculator cimport c_calculate_bid_ask_ratios_from_base_asset_ratio
 from .inventory_skew_calculator import calculate_total_order_size
 from .order_book_asset_price_delegate cimport OrderBookAssetPriceDelegate
+from .inventory_cost_price_delegate import InventoryCostPriceDelegate
 
 
 NaN = float("nan")
@@ -82,6 +84,8 @@ cdef class PureMarketMakingStrategy(StrategyBase):
                  bid_order_optimization_depth: Decimal = s_decimal_zero,
                  add_transaction_costs_to_orders: bool = False,
                  asset_price_delegate: AssetPriceDelegate = None,
+                 inventory_cost_price_delegate: InventoryCostPriceDelegate = None,
+                 inventory_cost_allow_higher_bids: bool = False,
                  price_type: str = "mid_price",
                  take_if_crossed: bool = False,
                  price_ceiling: Decimal = s_decimal_neg_one,
@@ -123,6 +127,8 @@ cdef class PureMarketMakingStrategy(StrategyBase):
         self._bid_order_optimization_depth = bid_order_optimization_depth
         self._add_transaction_costs_to_orders = add_transaction_costs_to_orders
         self._asset_price_delegate = asset_price_delegate
+        self._inventory_cost_price_delegate = inventory_cost_price_delegate
+        self._inventory_cost_allow_higher_bids = inventory_cost_allow_higher_bids
         self._price_type = self.get_price_type(price_type)
         self._take_if_crossed = take_if_crossed
         self._price_ceiling = price_ceiling
@@ -347,16 +353,17 @@ cdef class PureMarketMakingStrategy(StrategyBase):
         self._order_override = value
 
     def get_price(self) -> float:
-        if self._asset_price_delegate is not None:
-            price_provider = self._asset_price_delegate
-        else:
-            price_provider = self._market_info
+        price_provider = self._asset_price_delegate or self._market_info
         if self._price_type is PriceType.LastOwnTrade:
             price = self._last_own_trade_price
+        elif self._price_type is PriceType.InventoryCost:
+            price = price_provider.get_price_by_type(PriceType.MidPrice)
         else:
             price = price_provider.get_price_by_type(self._price_type)
+
         if price.is_nan():
             price = price_provider.get_price_by_type(PriceType.MidPrice)
+
         return price
 
     def get_last_price(self) -> float:
@@ -418,6 +425,22 @@ cdef class PureMarketMakingStrategy(StrategyBase):
     def asset_price_delegate(self, value):
         self._asset_price_delegate = value
 
+    @property
+    def inventory_cost_price_delegate(self) -> AssetPriceDelegate:
+        return self._inventory_cost_price_delegate
+
+    @inventory_cost_price_delegate.setter
+    def inventory_cost_price_delegate(self, value):
+        self._inventory_cost_price_delegate = value
+
+    @property
+    def inventory_cost_allow_higher_bids(self):
+        return self._inventory_cost_allow_higher_bids
+
+    @inventory_cost_allow_higher_bids.setter
+    def inventory_cost_allow_higher_bids(self, value):
+        self._inventory_cost_allow_higher_bids = value
+
     @property
     def order_tracker(self):
         return self._sb_order_tracker
@@ -550,9 +573,19 @@ cdef class PureMarketMakingStrategy(StrategyBase):
             bid_price = market.get_price(trading_pair, False)
             ask_price = market.get_price(trading_pair, True)
             ref_price = float("nan")
-            if market == self._market_info.market and self._asset_price_delegate is None:
+            if market == self._market_info.market and self._inventory_cost_price_delegate is not None:
+                # We're using inventory_cost, show it's price
+                try:
+                    ref_price = self._inventory_cost_price_delegate.get_price()
+                except NoPrice:
+                    ref_price = self.get_price()
+            elif market == self._market_info.market and self._asset_price_delegate is None:
                 ref_price = self.get_price()
-            elif market == self._asset_price_delegate.market and self._price_type is not PriceType.LastOwnTrade:
+            elif (
+                self._asset_price_delegate is not None
+                and market == self._asset_price_delegate.market
+                and self._price_type is not PriceType.LastOwnTrade
+            ):
                 ref_price = self._asset_price_delegate.get_price_by_type(self._price_type)
             markets_data.append([
                 market.display_name,
@@ -648,7 +681,7 @@ cdef class PureMarketMakingStrategy(StrategyBase):
             # asset_mid_price = self.c_set_mid_price(market_info)
             if self._create_timestamp <= self._current_timestamp:
                 # 1. Create base order proposals
-                proposal =self.c_create_base_proposal()
+                proposal = self.c_create_base_proposal()
                 # 2. Apply functions that limit numbers of buys and sells proposal
                 self.c_apply_order_levels_modifiers(proposal)
                 # 3. Apply functions that modify orders price
@@ -678,6 +711,18 @@ cdef class PureMarketMakingStrategy(StrategyBase):
             list buys = []
             list sells = []
 
+        buy_reference_price = sell_reference_price = self.get_price()
+
+        if self._inventory_cost_price_delegate is not None:
+            try:
+                inventory_cost_price = self._inventory_cost_price_delegate.get_price()
+            except NoPrice:
+                pass
+            else:
+                if not self._inventory_cost_allow_higher_bids:
+                    buy_reference_price = min(inventory_cost_price, buy_reference_price)
+                sell_reference_price = max(inventory_cost_price, sell_reference_price)
+
         # First to check if a customized order override is configured, otherwise the proposal will be created according
         # to order spread, amount, and levels setting.
         order_override = self._order_override
@@ -685,14 +730,14 @@ cdef class PureMarketMakingStrategy(StrategyBase):
             for key, value in order_override.items():
                 if str(value[0]) in ["buy", "sell"]:
                     if str(value[0]) == "buy":
-                        price = self.get_price() * (Decimal("1") - Decimal(str(value[1])) / Decimal("100"))
+                        price = buy_reference_price * (Decimal("1") - Decimal(str(value[1])) / Decimal("100"))
                         price = market.c_quantize_order_price(self.trading_pair, price)
                         size = Decimal(str(value[2]))
                         size = market.c_quantize_order_amount(self.trading_pair, size)
                         if size > 0 and price > 0:
                             buys.append(PriceSize(price, size))
                     elif str(value[0]) == "sell":
-                        price = self.get_price() * (Decimal("1") + Decimal(str(value[1])) / Decimal("100"))
+                        price = sell_reference_price * (Decimal("1") + Decimal(str(value[1])) / Decimal("100"))
                         price = market.c_quantize_order_price(self.trading_pair, price)
                         size = Decimal(str(value[2]))
                         size = market.c_quantize_order_amount(self.trading_pair, size)
@@ -700,14 +745,14 @@ cdef class PureMarketMakingStrategy(StrategyBase):
                             sells.append(PriceSize(price, size))
         else:
             for level in range(0, self._buy_levels):
-                price = self.get_price() * (Decimal("1") - self._bid_spread - (level * self._order_level_spread))
+                price = buy_reference_price * (Decimal("1") - self._bid_spread - (level * self._order_level_spread))
                 price = market.c_quantize_order_price(self.trading_pair, price)
                 size = self._order_amount + (self._order_level_amount * level)
                 size = market.c_quantize_order_amount(self.trading_pair, size)
                 if size > 0:
                     buys.append(PriceSize(price, size))
             for level in range(0, self._sell_levels):
-                price = self.get_price() * (Decimal("1") + self._ask_spread + (level * self._order_level_spread))
+                price = sell_reference_price * (Decimal("1") + self._ask_spread + (level * self._order_level_spread))
                 price = market.c_quantize_order_price(self.trading_pair, price)
                 size = self._order_amount + (self._order_level_amount * level)
                 size = market.c_quantize_order_amount(self.trading_pair, size)
@@ -947,6 +992,9 @@ cdef class PureMarketMakingStrategy(StrategyBase):
                         f"{order_filled_event.amount} {market_info.base_asset} filled."
                     )
 
+            if self._inventory_cost_price_delegate is not None:
+                self._inventory_cost_price_delegate.process_order_fill_event(order_filled_event)
+
     cdef c_did_complete_buy_order(self, object order_completed_event):
         cdef:
             str order_id = order_completed_event.order_id
@@ -1229,5 +1277,7 @@ cdef class PureMarketMakingStrategy(StrategyBase):
             return PriceType.LastTrade
         elif price_type_str == 'last_own_trade_price':
             return PriceType.LastOwnTrade
+        elif price_type_str == 'inventory_cost':
+            return PriceType.InventoryCost
         else:
             raise ValueError(f"Unrecognized price type string {price_type_str}.")
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 a542043d346..78a4d5dff8b 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
@@ -236,6 +236,13 @@ def exchange_on_validated(value: str):
                   type_str="decimal",
                   validator=lambda v: validate_decimal(v, min_value=0, inclusive=False),
                   default=Decimal("1")),
+    "inventory_price":
+        ConfigVar(key="inventory_price",
+                  prompt="What is the price of your base asset inventory? ",
+                  type_str="decimal",
+                  validator=lambda v: validate_decimal(v, min_value=Decimal("0"), inclusive=True),
+                  default=Decimal("1"),
+                  ),
     "filled_order_delay":
         ConfigVar(key="filled_order_delay",
                   prompt="How long do you want to wait before placing the next order "
@@ -296,7 +303,8 @@ def exchange_on_validated(value: str):
                   on_validated=on_validate_price_source),
     "price_type":
         ConfigVar(key="price_type",
-                  prompt="Which price type to use? (mid_price/last_price/last_own_trade_price/best_bid/best_ask) >>> ",
+                  prompt="Which price type to use? ("
+                         "mid_price/last_price/last_own_trade_price/best_bid/best_ask/inventory_cost) >>> ",
                   type_str="str",
                   required_if=lambda: pure_market_making_config_map.get("price_source").value != "custom_api",
                   default="mid_price",
@@ -304,7 +312,9 @@ def exchange_on_validated(value: str):
                                                     "last_price",
                                                     "last_own_trade_price",
                                                     "best_bid",
-                                                    "best_ask"} else
+                                                    "best_ask",
+                                                    "inventory_cost",
+                                                    } else
                   "Invalid price type."),
     "price_source_exchange":
         ConfigVar(key="price_source_exchange",
@@ -331,6 +341,14 @@ def exchange_on_validated(value: str):
                   prompt="Enter pricing API URL >>> ",
                   required_if=lambda: pure_market_making_config_map.get("price_source").value == "custom_api",
                   type_str="str"),
+    "inventory_cost_allow_higher_bids":
+        ConfigVar(key="inventory_cost_allow_higher_bids",
+                  prompt="Do you want to allow placing bid orders higher than inventory price? ("
+                         "Yes/No) >>> ",
+                  required_if=lambda: pure_market_making_config_map.get(
+                      "price_type").value == "inventory_cost",
+                  type_str="bool",
+                  validator=validate_bool),
     "order_override":
         ConfigVar(key="order_override",
                   prompt=None,
diff --git a/hummingbot/strategy/pure_market_making/start.py b/hummingbot/strategy/pure_market_making/start.py
index 39133be0b98..6041e94e782 100644
--- a/hummingbot/strategy/pure_market_making/start.py
+++ b/hummingbot/strategy/pure_market_making/start.py
@@ -3,11 +3,13 @@
     Tuple,
 )
 
+from hummingbot.client.hummingbot_application import HummingbotApplication
 from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple
 from hummingbot.strategy.pure_market_making import (
     PureMarketMakingStrategy,
     OrderBookAssetPriceDelegate,
-    APIAssetPriceDelegate
+    APIAssetPriceDelegate,
+    InventoryCostPriceDelegate,
 )
 from hummingbot.strategy.pure_market_making.pure_market_making_config_map import pure_market_making_config_map as c_map
 from hummingbot.connector.exchange.paper_trade import create_paper_trade_market
@@ -66,6 +68,11 @@ def start(self):
             asset_price_delegate = OrderBookAssetPriceDelegate(ext_market, asset_trading_pair)
         elif price_source == "custom_api":
             asset_price_delegate = APIAssetPriceDelegate(price_source_custom_api)
+        inventory_cost_price_delegate = None
+        if price_type == "inventory_cost":
+            db = HummingbotApplication.main_application().trade_fill_db
+            inventory_cost_price_delegate = InventoryCostPriceDelegate(db, trading_pair)
+        inventory_cost_allow_higher_bids = c_map.get("inventory_cost_allow_higher_bids").value
         take_if_crossed = c_map.get("take_if_crossed").value
 
         strategy_logging_options = PureMarketMakingStrategy.OPTION_LOG_ALL
@@ -91,6 +98,8 @@ def start(self):
             add_transaction_costs_to_orders=add_transaction_costs_to_orders,
             logging_options=strategy_logging_options,
             asset_price_delegate=asset_price_delegate,
+            inventory_cost_price_delegate=inventory_cost_price_delegate,
+            inventory_cost_allow_higher_bids=inventory_cost_allow_higher_bids,
             price_type=price_type,
             take_if_crossed=take_if_crossed,
             price_ceiling=price_ceiling,
diff --git a/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml b/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml
index 1d804dad704..7976befa4ef 100644
--- a/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml
+++ b/hummingbot/templates/conf_pure_market_making_strategy_TEMPLATE.yml
@@ -60,6 +60,12 @@ inventory_target_base_pct: null
 # inventory skew feature).
 inventory_range_multiplier: null
 
+# Initial price of the base asset. Note: this setting is not affects anything, the price is kept in the database.
+inventory_price: null
+
+# Allow to place bids higher than inventory price?
+inventory_cost_allow_higher_bids: null
+
 # Number of levels of orders to place on each side of the order book.
 order_levels: null
 
@@ -94,7 +100,7 @@ add_transaction_costs: null
 # The price source (current_market/external_market/custom_api).
 price_source: null
 
-# The price type (mid_price/last_price/last_own_trade_price/best_bid/best_ask).
+# The price type (mid_price/last_price/last_own_trade_price/best_bid/best_ask/inventory_cost).
 price_type: null
 
 # An external exchange name (for external exchange pricing source).
diff --git a/test/test_inventory_cost_price_delegate.py b/test/test_inventory_cost_price_delegate.py
new file mode 100644
index 00000000000..f56fb3e814c
--- /dev/null
+++ b/test/test_inventory_cost_price_delegate.py
@@ -0,0 +1,119 @@
+import unittest
+from decimal import Decimal
+
+from hummingbot.core.event.events import OrderFilledEvent, TradeType
+from hummingbot.core.event.events import TradeFee, OrderType
+from hummingbot.exceptions import NoPrice
+from hummingbot.model.inventory_cost import InventoryCost
+from hummingbot.model.sql_connection_manager import (
+    SQLConnectionManager,
+    SQLConnectionType,
+)
+from hummingbot.strategy.pure_market_making.inventory_cost_price_delegate import (
+    InventoryCostPriceDelegate,
+)
+
+
+class TestInventoryCostPriceDelegate(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.trade_fill_sql = SQLConnectionManager(
+            SQLConnectionType.TRADE_FILLS, db_path=""
+        )
+        cls.trading_pair = "BTC-USDT"
+        cls.base_asset, cls.quote_asset = cls.trading_pair.split("-")
+        cls._session = cls.trade_fill_sql.get_shared_session()
+
+    def setUp(self):
+        for table in [InventoryCost.__table__]:
+            self.trade_fill_sql.get_shared_session().execute(table.delete())
+        self.delegate = InventoryCostPriceDelegate(
+            self.trade_fill_sql, self.trading_pair
+        )
+
+    def test_process_order_fill_event_buy(self):
+        amount = Decimal("1")
+        price = Decimal("9000")
+        event = OrderFilledEvent(
+            timestamp=1,
+            order_id="order1",
+            trading_pair=self.trading_pair,
+            trade_type=TradeType.BUY,
+            order_type=OrderType.LIMIT,
+            price=price,
+            amount=amount,
+            trade_fee=TradeFee(percent=Decimal("0"), flat_fees=[]),
+        )
+        # first event creates DB record
+        self.delegate.process_order_fill_event(event)
+        count = self._session.query(InventoryCost).count()
+        self.assertEqual(count, 1)
+
+        # second event causes update to existing record
+        self.delegate.process_order_fill_event(event)
+        record = InventoryCost.get_record(
+            self._session, self.base_asset, self.quote_asset
+        )
+        self.assertEqual(record.base_volume, amount * 2)
+        self.assertEqual(record.quote_volume, price * 2)
+
+    def test_process_order_fill_event_sell_no_initial_cost_set(self):
+        amount = Decimal("1")
+        price = Decimal("9000")
+        event = OrderFilledEvent(
+            timestamp=1,
+            order_id="order1",
+            trading_pair=self.trading_pair,
+            trade_type=TradeType.SELL,
+            order_type=OrderType.LIMIT,
+            price=price,
+            amount=amount,
+            trade_fee=TradeFee(percent=Decimal("0"), flat_fees=[]),
+        )
+        # First event is a sell when there was no InventoryCost created. Assume wrong input from user
+        self.delegate.process_order_fill_event(event)
+        record = InventoryCost.get_record(
+            self._session, self.base_asset, self.quote_asset
+        )
+        self.assertEqual(record.base_volume, Decimal("0"))
+        self.assertEqual(record.quote_volume, Decimal("0"))
+
+        # Second event - InventoryCost created, amounts are 0, test that record updated correctly
+        self.delegate.process_order_fill_event(event)
+        record = InventoryCost.get_record(
+            self._session, self.base_asset, self.quote_asset
+        )
+        self.assertEqual(record.base_volume, Decimal("0"))
+        self.assertEqual(record.quote_volume, Decimal("0"))
+
+    def test_get_price_by_type(self):
+        amount = Decimal("1")
+        price = Decimal("9000")
+
+        # Test when no records
+        self.assertRaises(NoPrice, self.delegate.get_price)
+
+        record = InventoryCost(
+            base_asset=self.base_asset,
+            quote_asset=self.quote_asset,
+            base_volume=amount,
+            quote_volume=amount * price,
+        )
+        self._session.add(record)
+        self._session.commit()
+        delegate_price = self.delegate.get_price()
+        self.assertEqual(delegate_price, price)
+
+    def test_get_price_by_type_zero_division(self):
+        amount = Decimal("0")
+        price = Decimal("9000")
+
+        record = InventoryCost(
+            base_asset=self.base_asset,
+            quote_asset=self.quote_asset,
+            base_volume=amount,
+            quote_volume=amount * price,
+        )
+        self._session.add(record)
+        self._session.commit()
+        self.assertRaises(NoPrice, self.delegate.get_price)
diff --git a/test/test_pmm.py b/test/test_pmm.py
index 98b00bb8d11..408a5f6545e 100644
--- a/test/test_pmm.py
+++ b/test/test_pmm.py
@@ -21,8 +21,13 @@
     TradeType,
     PriceType,
 )
+from hummingbot.model.sql_connection_manager import (
+    SQLConnectionManager,
+    SQLConnectionType,
+)
 from hummingbot.strategy.pure_market_making.pure_market_making import PureMarketMakingStrategy
 from hummingbot.strategy.pure_market_making.order_book_asset_price_delegate import OrderBookAssetPriceDelegate
+from hummingbot.strategy.pure_market_making.inventory_cost_price_delegate import InventoryCostPriceDelegate
 from hummingbot.core.data_type.order_book import OrderBook
 from hummingbot.core.data_type.order_book_row import OrderBookRow
 from hummingbot.client.command.config_command import ConfigCommand
@@ -135,6 +140,10 @@ def setUp(self):
                                               volume_step_size=10)
         self.ext_market.add_data(self.ext_data)
         self.order_book_asset_del = OrderBookAssetPriceDelegate(self.ext_market, self.trading_pair)
+        trade_fill_sql = SQLConnectionManager(
+            SQLConnectionType.TRADE_FILLS, db_path=""
+        )
+        self.inventory_cost_price_del = InventoryCostPriceDelegate(trade_fill_sql, self.trading_pair)
 
     def simulate_maker_market_trade(
             self, is_buy: bool, quantity: Decimal, price: Decimal, market: Optional[BacktestMarket] = None,
@@ -773,6 +782,52 @@ def test_inventory_skew_multiple_orders_status(self):
         self.assertEqual("50.0%", status_df.iloc[4, 1])
         self.assertEqual("150.0%", status_df.iloc[4, 2])
 
+    def test_inventory_cost_price_del(self):
+        strategy = self.one_level_strategy
+        strategy.inventory_cost_price_delegate = self.inventory_cost_price_del
+        self.clock.add_iterator(strategy)
+        self.clock.backtest_til(self.start_timestamp + 1)
+
+        # Expecting to have orders set according to mid_price as there is no inventory cost data yet
+        first_bid_order = strategy.active_buys[0]
+        first_ask_order = strategy.active_sells[0]
+        self.assertEqual(Decimal("99"), first_bid_order.price)
+        self.assertEqual(Decimal("101"), first_ask_order.price)
+
+        self.simulate_maker_market_trade(
+            is_buy=False, quantity=Decimal("10"), price=Decimal("98.9"),
+        )
+        self.clock.backtest_til(self.start_timestamp + 7)
+        first_bid_order = strategy.active_buys[0]
+        self.assertEqual(Decimal("98.01"), first_bid_order.price)
+
+    def test_inventory_cost_price_del_allow_higher_bids(self):
+        strategy = self.one_level_strategy
+        strategy.inventory_cost_price_delegate = self.inventory_cost_price_del
+        strategy.inventory_cost_allow_higher_bids = True
+        self.clock.add_iterator(strategy)
+        self.clock.backtest_til(self.start_timestamp + 1)
+
+        # Expecting to have orders set according to mid_price as there is no inventory cost data yet
+        first_bid_order = strategy.active_buys[0]
+        first_ask_order = strategy.active_sells[0]
+        self.assertEqual(Decimal("99"), first_bid_order.price)
+        self.assertEqual(Decimal("101"), first_ask_order.price)
+
+        # Filling buy order
+        self.simulate_maker_market_trade(
+            is_buy=False, quantity=Decimal("1"), price=Decimal("98.9"),
+        )
+        # And shifting market up
+        simulate_order_book_widening(self.book_data.order_book, 101, 110)
+        self.clock.backtest_til(self.start_timestamp + 7)
+
+        # Expect inventory cost price to be equal to buy price
+        self.assertEqual(strategy.inventory_cost_price_delegate.get_price(), Decimal("99"))
+        first_bid_order = strategy.active_buys[0]
+        # Expect bid price to be higher that inventory cost price
+        self.assertGreater(first_bid_order.price, Decimal("101"))
+
     def test_order_book_asset_del(self):
         strategy = self.one_level_strategy
         strategy.asset_price_delegate = self.order_book_asset_del


Code review comments:
diff_hunk: @@ -0,0 +1,68 @@
+from decimal import Decimal, InvalidOperation
+
+from hummingbot.core.event.events import OrderFilledEvent, TradeType
+from hummingbot.exceptions import NoPrice
+from hummingbot.model.inventory_cost import InventoryCost
+from hummingbot.model.sql_connection_manager import SQLConnectionManager
+
+
+class InventoryCostPriceDelegate:
+    def __init__(self, sql: SQLConnectionManager, trading_pair: str) -> None:
+        self.base_asset, self.quote_asset = trading_pair.split("-")
+        self._session = sql.get_shared_session()
+
+    @property
+    def ready(self) -> bool:
+        return True
+
+    def get_price(self) -> Decimal:
+        record = InventoryCost.get_record(
+            self._session, self.base_asset, self.quote_asset
+        )
+        try:
+            price = record.quote_volume / record.base_volume
+        except (ZeroDivisionError, InvalidOperation, AttributeError):
+            raise NoPrice("Inventory cost delegate does not have price cost data yet")
path: hummingbot/strategy/pure_market_making/inventory_cost_price_delegate.py
text: @user1:
I think it'd be easier to know which conditions exactly causing NoPrice, and it'd be better to use None to handle this rather than exception. e.g.
```python
if record.base_volume <= s_decimal_0 or record.base_volume is None:
    return None
```
InvalidOperation or AttributeError will hide other problem and make it. difficult to debug later on.

@author:
In general, a function must return only one type. It's a common anti-pattern described here:  https://docs.quantifiedcode.com/python-anti-patterns/maintainability/returning_more_than_one_variable_type_from_function_call.html

For these exceptions, all of them are appear when price data is not ready yet (for different reasons). For debug purposes, we can add info on what exact exception happened:

```python
except (ZeroDivisionError, InvalidOperation, AttributeError) as e:
    raise NoPrice(f"Inventory cost delegate does not have price cost data yet: {str(e)}")
```

@user1:
I, personally, don't like catching exceptions for expected None value, it makes code harder to read (many try-excepts and nested ones). Moreover, our practice so far has been returning None is acceptable (with hint `-> Optional[]`), it'd be easy to maintain if we follow this (but sure when there is clear benefit in accepting new coding practice we should consider).

@author:
Refactored to return None.

diff_hunk: @@ -331,6 +341,14 @@ def exchange_on_validated(value: str):
                   prompt="Enter pricing API URL >>> ",
                   required_if=lambda: pure_market_making_config_map.get("price_source").value == "custom_api",
                   type_str="str"),
+    "inventory_cost_allow_higher_bids":
path: hummingbot/strategy/pure_market_making/pure_market_making_config_map.py
text: @user1:
Why do we need this config variable? And why only for bids?
From what I understand the bid price will always be below inventory price (if inventory price is known), the bid price is inventory price (or market mid price is that is lower) minus spread.

@author:
There are 2 reasons:
1. Over time with profit accumulation inventory price goes lower and lower, so bids can become too far from the mid price, see example here https://github.com/CoinAlpha/hummingbot/issues/2314#issuecomment-716409652
2. When market is in uptrend, you may still want to perform occasional buys.

@user1:
Personally, i don't think we should crater for exceptions like this. Reasons:
* Your reasons can be argued for `inventory_cost_allow_lower_asks` as well which we don't have, just the opposite direction.
* Should you consider to apply this only when the user is in profit? also, is it realised or unrealised profit? The main point of this `inventory_cost` feature is to always place only profitable orders.
*  If there is `inventory_cost_allow_lower_asks`, turning both it and `inventory_cost_allow_higher_bids` on wouldn't make any sense, since it simply neglects the feature entirely.

@author:
Ok, I'll revert this option.