<jupyter_start><jupyter_text>A Trader Stabilize Fiat Value Curve of Crypto Currency<jupyter_code>%%bash
pip3 install scipy
# export
import math
from decimal import Decimal
from scipy import optimize as opt
from numpy.lib import scimath
import time


class Trader(object):

    TRADE_RATE = Decimal('Infinity')

    SAMPLE_INTERVAL = 60
    HAPPY_COOLING_INTERVAL = 30
    SAD_COOLING_INTERVAL = 60 * 6

    LOSSABLE_UNIT_CC_SELL_JPY = Decimal('Infinity')

    MAX_GAIN_JPY = Decimal('Infinity') # useless
    MAX_LOSS_JPY = Decimal('Infinity') # useless
    MAX_LOST_JPY = Decimal('Infinity')

    def __init__(self, exchange, status, gainable_unit_cc_sold_ratio, lossable_unit_cc_bought_ratio, min_trade_fiat_price, max_trade_fiat_price):
        self.exchange = exchange

        self.status = status

        self.gainable_unit_cc_sold_ratio = gainable_unit_cc_sold_ratio # TODO: calculate from sample variance
        self.lossable_unit_cc_bought_ratio = lossable_unit_cc_bought_ratio

        self.min_unit_cc_trade_fiat_money = min_trade_fiat_price
        self.max_unit_cc_trade_fiat_money = max_trade_fiat_price

    def get_price(self):
        return self.exchange.get_price(self.status.crypto_symbol, self.status.fiat_symbol)

    def buy(self, unit_amount):
        if unit_amount <= 0:
            raise Exception('Trader::buy  unit_amount <= 0')
        amount = self.status.trade_unit * unit_amount
        self.exchange.buy(self.status.crypto_symbol, self.status.fiat_symbol, amount, self.status.now_buy_fiat_price_without_fee)

#         old_status = self.status
#         new_status = copy.deepcopy(old_status)

#         new_status.buy_count += 1
#         new_status.update_bought_status(fiat_price=fiat_price, unit_amount=unit_amount)

#         old_amount = old_status.bought_amount
#         old_percentage = old_status.get_usage() * 100
#         new_amount = new_status.bought_amount
#         new_percentage = new_status.get_usage(getattr(self.portfolio, self.status.fiat_symbol)) * 100

#         bookkeeper.fth_origin.write(
#             f'BOUGHT {amount:.4f} = -{old_amount:.4f} ({old_percentage:.2f}%) +{new_amount:.4f} ({new_percentage:.2f}%) CELO for {fiat_price:.4f} USDT/CELO:'
#             f' NO.{new_status.trade_count + 1}-{new_status.buy_count} at {datetime.now()}\n\n'
#         )

    def sell(self, unit_amount):
        if unit_amount <= 0:
            raise Exception('Trader::sell  unit_amount <= 0')
        amount = self.status.trade_unit * unit_amount
        self.exchange.sell(self.status.crypto_symbol, self.status.fiat_symbol, amount, self.status.now_sell_fiat_price_without_fee)

#         old_status = self.status
#         new_status = copy.deepcopy(old_status)

#         new_status.sell_count += 1
#         new_status.update_bought_status(fiat_price=new_status.bought_average_fiat_price, unit_amount=-unit_amount)
#         new_status.update_sold_status(fiat_price=fiat_price, unit_amount=unit_amount)

#         old_avg_price = old_status.bought_average_fiat_price
#         diff_price = fiat_price - old_avg_price
#         gained_fiat_money = old_status.estimate_gained_fiat_money(unit_amount, fiat_price)
#         new_status.total_gained_fiat_money += gained_fiat_money

#         old_amount = old_status.bought_amount
#         old_percentage = old_status.get_usage() * 100
#         new_amount = new_status.bought_amount
#         new_percentage = new_status.get_usage(getattr(self.portfolio, self.status.fiat_symbol)) * 100

#         bookkeeper.fth_origin.write(
#             f'GAINED {gained_fiat_money:+.4f} USDT: GAINED {new_status.total_gained_fiat_money:+.4f} USDT in total at {datetime.now()}\n'
#             f'SOLD {amount:.4f} = +{old_amount:.4f} ({old_percentage:.2f}%) -{new_amount:.4f} ({new_percentage:.2f}%) CELO'
#             f' for {diff_price:.4f} = {fiat_price:.4f} - {old_avg_price:.4f} USDT/CELO: NO.{new_status.trade_count + 1}-{new_status.sell_count}\n\n'
#         )

    def has_buyable_fiat_price(self):
        return self.status.now_buy_fiat_price > self.min_unit_cc_trade_fiat_money and self.status.now_buy_fiat_price < self.max_unit_cc_trade_fiat_money

    def get_buy_unit_amount(self):
        status = self.status

        used_fiat_money = float(status.used_fiat_money)
        unit_cc_diff_jpy = float(abs(status.bought_average_fiat_price - status.now_buy_fiat_price))

        lossable_unit_cc_bought_jpy = (status.bought_average_fiat_price - self.min_unit_cc_trade_fiat_money) * self.lossable_unit_cc_bought_ratio
        status.min_bought_average_fiat_price = status.bought_average_fiat_price - lossable_unit_cc_bought_jpy

        if status.used_fiat_money >= status.get_max_used_fiat_money():
            status.max_next_buy_fiat_price = Decimal('-Infinity')
            return 0

        # Minimum Action Price Difference
        min_unit_cc_diff_jpy = float(lossable_unit_cc_bought_jpy) / scimath.log(float(status.get_max_used_fiat_money() / status.used_fiat_money))
        status.max_next_buy_fiat_price = status.bought_average_fiat_price - Decimal(repr(min_unit_cc_diff_jpy)) * status.now_buy_fiat_price / status.bought_average_fiat_price

        bought_average_fiat_price = float(status.bought_average_fiat_price)
        now_buy_fiat_price = float(status.now_buy_fiat_price)
        trade_unit = float(status.trade_unit)

        return math.ceil(
            used_fiat_money * opt.fsolve(
                lambda c: [
                    (
                        unit_cc_diff_jpy - min_unit_cc_diff_jpy * scimath.log(1 + c[0]) * (bought_average_fiat_price * c[0] + now_buy_fiat_price) / (bought_average_fiat_price * c[0])
                    ).real
                ],
                [-0.5]
            )[0] / now_buy_fiat_price / trade_unit
        )

    def get_sell_unit_amount(self):
        status = self.status

        if status.bought_average_fiat_price is None:
            return 0

        sold_unit_amount = float(status.sold_unit_amount)
        unit_cc_diff_jpy = float(abs(status.now_sell_fiat_price - status.sold_average_fiat_price))

        status.max_sold_average_fiat_price = status.bought_average_fiat_price * (1 + self.gainable_unit_cc_sold_ratio)

        if status.sold_unit_amount >= status.get_max_sold_unit_amount():
            status.min_next_sell_fiat_price = Decimal('Infinity')
            return 0

        min_unit_cc_diff_jpy = float(status.max_sold_average_fiat_price - status.sold_average_fiat_price) / scimath.log(float(status.get_max_sold_unit_amount() / status.sold_unit_amount))
        status.min_next_sell_fiat_price = status.sold_average_fiat_price + Decimal(repr(min_unit_cc_diff_jpy))

        c = opt.fsolve(
            lambda c: [
                (
                    unit_cc_diff_jpy - min_unit_cc_diff_jpy * scimath.log(1 + c[0]) * (c[0] + 1) / c[0]
                ).real
            ],
            [-0.5]
        )[0]

        if c < -1:
            return math.floor(status.bought_unit_amount)

        if c < 0:
            return 0

        return math.ceil(sold_unit_amount * c)

    def check_and_trade(self):
        status = self.status

        min_trade_fiat_money_limit = self.exchange.get_trading_specification(
            status.crypto_symbol, status.fiat_symbol
        ).get('min_trade_fiat_money_limit', 200)

        status.update(self.get_price())

        please_sell_unit_amount = 0

        if (status.sample_number % type(self).TRADE_RATE == 0 or status.sold_unit_amount == 0) and status.fiat_price_is_higher_than_average():
            please_sell_unit_amount = min(
                math.ceil(min_trade_fiat_money_limit / status.now_sell_fiat_price_without_fee / status.trade_unit),
                math.floor(status.bought_unit_amount)
            )
            cooling_interval = type(self).HAPPY_COOLING_INTERVAL

        if status.sold_unit_amount > 0 and status.now_sell_fiat_price > status.sold_average_fiat_price:
            trade_unit_amount = min(self.get_sell_unit_amount(), math.floor(status.bought_unit_amount))
            if trade_unit_amount > 0:
                please_sell_unit_amount = trade_unit_amount
                cooling_interval = type(self).HAPPY_COOLING_INTERVAL

        gained_fiat_money = status.estimate_gained_fiat_money(status.bought_unit_amount)
        if gained_fiat_money is not None and gained_fiat_money > type(self).MAX_GAIN_JPY:
            please_sell_unit_amount = math.floor(status.bought_unit_amount)
            cooling_interval = type(self).HAPPY_COOLING_INTERVAL

        if gained_fiat_money is not None and gained_fiat_money < -type(self).MAX_LOSS_JPY:
            please_sell_unit_amount = math.floor(status.bought_unit_amount)
            cooling_interval = type(self).SAD_COOLING_INTERVAL

        if status.bought_average_fiat_price is not None and status.now_sell_fiat_price < status.bought_average_fiat_price - type(self).LOSSABLE_UNIT_CC_SELL_JPY and status.get_usage() > 90/100:
            please_sell_unit_amount = math.floor(status.bought_unit_amount)
            cooling_interval = type(self).SAD_COOLING_INTERVAL

#         if status.bought_unit_amount > 0 and status.now_sell_fiat_price < self.min_unit_cc_trade_fiat_money:
#             please_sell_unit_amount = status.bought_unit_amount
#             cooling_interval = type(self).SAD_COOLING_INTERVAL

        if self.exchange.trade_fiat_money_is_larger_than_limit(
            status.crypto_symbol,
            status.fiat_symbol,
            status.trade_unit * please_sell_unit_amount,
            status.now_sell_fiat_price_without_fee
        ):
            self.sell(please_sell_unit_amount)
            status.sample_number = 0
            return cooling_interval, 'sell', please_sell_unit_amount

        please_buy_unit_amount = 0

        # TODO: self-adaptive by trend analysis
        # init_buy_jpy = status.get_max_used_fiat_money() / Decimal(math.exp(self.lossable_unit_cc_bought_ratio / (1 - self.lossable_unit_cc_bought_ratio)))
        init_buy_jpy = min_trade_fiat_money_limit * 5
        status.init_buy_jpy = init_buy_jpy
        # self.exchange.has_enough_unused_fiat_money(init_buy_jpy, status.unused_fiat_money) !! status.unused_fiat_money !! what if set max_used_fiat_money_limit to 0
        if status.used_fiat_money < min_trade_fiat_money_limit and self.exchange.has_enough_unused_fiat_money(init_buy_jpy, status.unused_fiat_money) and self.has_buyable_fiat_price():
            buy_fiat_money = max(init_buy_jpy - status.used_fiat_money, min_trade_fiat_money_limit)
            please_buy_unit_amount = math.ceil(buy_fiat_money / status.now_buy_fiat_price / status.trade_unit)

        if status.used_fiat_money < init_buy_jpy and status.fiat_price_is_lower_than_average() and self.exchange.has_enough_unused_fiat_money(init_buy_jpy - status.used_fiat_money, status.unused_fiat_money) and self.has_buyable_fiat_price():
            buy_fiat_money = max(init_buy_jpy - status.used_fiat_money, min_trade_fiat_money_limit)
            please_buy_unit_amount = math.ceil(buy_fiat_money / status.now_buy_fiat_price / status.trade_unit)

        if status.used_fiat_money >= init_buy_jpy and status.fiat_price_is_lower_than_average():
            trade_unit_amount = self.get_buy_unit_amount()
            while trade_unit_amount > 0 and not self.exchange.has_enough_unused_fiat_money(status.now_buy_fiat_price * status.trade_unit * trade_unit_amount, status.unused_fiat_money):
                trade_unit_amount >>= 1
            if trade_unit_amount > 0:
                please_buy_unit_amount = trade_unit_amount

        # print(f'{status.crypto_symbol}-{status.fiat_symbol}: please_buy_fiat_money={status.now_buy_fiat_price_without_fee * status.trade_unit * please_buy_unit_amount}\n')
        if self.exchange.trade_fiat_money_is_larger_than_limit(
            status.crypto_symbol,
            status.fiat_symbol,
            status.trade_unit * please_buy_unit_amount,
            status.now_buy_fiat_price_without_fee
        ):
            self.buy(please_buy_unit_amount)
            status.sample_number = 0
            return type(self).SAMPLE_INTERVAL, 'buy', please_buy_unit_amount

        return type(self).SAMPLE_INTERVAL, 'wait', 0
from ccstabilizer import Status
from decimal import Decimal

crypto_symbol = 'OCEAN'
fiat_symbol = 'USDT'

status = Status(
    robot_name=f'{crypto_symbol} Robot',
    crypto_symbol=crypto_symbol,
    fiat_symbol=fiat_symbol,
    trade_unit=Decimal('0.01'),
    max_used_fiat_money_limit=Decimal('1000'),
)
from ccstabilizer import MXC
from ccstabilizer import secrets
exchange = MXC()
Trader(
    exchange=exchange,
    status=status,
    gainable_unit_cc_sold_ratio=Decimal('0.236'),
    lossable_unit_cc_bought_ratio=Decimal('0.618'),
    min_trade_fiat_price=Decimal('0'),
    max_trade_fiat_price=Decimal('Infinity')
).get_price()
from ccstabilizer import Binance
from ccstabilizer import secrets
exchange = Binance()
Trader(
    exchange=exchange,
    status=status,
    gainable_unit_cc_sold_ratio=Decimal('0.236'),
    lossable_unit_cc_bought_ratio=Decimal('0.618'),
    min_trade_fiat_price=Decimal('0'),
    max_trade_fiat_price=Decimal('Infinity')
).get_price()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0848112778535698, 'fraction_numerical': 0.00871608306806124, 'mean_word_length': 3.3808100929614873, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '1', 'dir': 'jupyter-structured-clean-dedup', 'id': '325155', 'license': 'Apache-2.0', 'n_tokens_mistral': 4882, 'n_tokens_neox': 4512, 'n_words': 613, 'path': 'notebooks/23_trader.ipynb', 'repo_name': 'seii-saintway/crypto-currency-stabilizer', 'size': '13194'}