見出し画像

ケリー基準を用いて賭けを繰り返すシミュレーションプログラム / kelly.py

本プログラムの動作と実行結果から判明した知見については以下記事にて詳細に解説しています。

2024/9/1 一回のゲームあたり購入と売却時と二回、売買手数料がかかる前提としてプログラムを修正しました。
また「-q」オプションを追加しました。これは、各シミュレーション結果の詳細表示を省略するオプションとなります。

2024/9/2 手数料比率が高すぎて初期金額を1万にすると成功確率がさがるため初期金額を10万円に変更しました

2024/9/3 初期金額を指定するオプション-cを追加。手数料の計算と手数料総額を計算する部分がややこしかったので微修正。金額を万、億、兆とわかりやすく表示するように修正。資産が0円以下になった場合はそれ以降賭けができなくなるように修正。

kelly.py

import random
import argparse
import math

# コマンドラインオプションを解析
parser = argparse.ArgumentParser(description="ケリー基準に基づいた賭けを行うシミュレーション")

parser.add_argument('-v', '--verbose',  action='store_true', help='詳細な情報を表示')
parser.add_argument('-f', '--fix_f_star', type=float, help='f_starを固定値にする')
parser.add_argument('-n', '--num_simulations', type=int, default=10, help='シミュレーションの回数')

# 実際の勝率から期待値を計算する際の幅
parser.add_argument('-w', '--expect_width', type=float, default=0.2, help='期待値の幅')

# オッズ(賭けた場合の利益率)を指定
parser.add_argument('-o', '--odds', type=float, help='オッズ(賭けた場合の利益率)を指定', default=0.3)

# 実際の勝率をランダムにする
parser.add_argument('-r', '--random_real', action='store_true', help='勝率を期待値をはずれてランダムにする')

# f_star計算をどのタイプにするか
parser.add_argument('-t', '--f_star_type', type=int, help='f_starの計算方法を指定', default=0)

# num_betsを指定
parser.add_argument('-i', '--num_bets', type=int, help='掛けの回数を指定', default=100)

# -show table
parser.add_argument('-s', '--show_table', action='store_true', help='f_starの結果一覧表を表示')

# 期待値の上限 (default: 0.8)
parser.add_argument('-e', '--expect_max', type=float, default=0.8, help='期待値の上限')

# シミュレーションの結果を簡略表示
parser.add_argument('-q', '--quiet', action='store_true', help='シミュレーションの結果を簡略表示')

# 初期資金を指定
parser.add_argument('-c', '--initial_funds', type=int, help='初期資金を指定', default=100000) # 10万円

# グローバル変数 args
args = parser.parse_args()

# 初期資金 / 10万円 
# 最初は1万円からスタートしていたが、手数料を考慮すると手数料比率が高すぎて
# 成功の確率が-i=1000であっても94%くらいに下がるので初期資金を10万円に変更
initial_funds = args.initial_funds

# (1シミュレーション当たりの) 掛けの回数
num_bets = args.num_bets

# 勝った場合の利益率
win_odds = args.odds

# 負けた場合の損失率 ※ひとまず勝った場合のオッズと同じにする
loss_odds = args.odds

# 0) これは簡略化されたバージョンで、特定の状況では使用されますが、損失のオッズを考慮していないため、一般的ではありません。
def calc_f0(win_prob, win_odds):
    f_star = win_prob - ((1 - win_prob) / (win_odds))
    return f_star

# 1) これもケリー基準の一形態ですが、f2ほど一般的ではありません
def calc_f1(win_prob, win_odds, loss_odds):
    p = win_prob     # 勝つ確率 / 0.3の場合、宝くじが当たる確率は30%
    a = loss_odds    # 没収率   / 0.3の場合、100円を賭けて宝くじがはずれれば、30円(30円の損失)が没収され、70円が手元に残ります。
    b = win_odds + 1 # 当選時の報酬 / 100円かけて300円返ってきた場合は3
    f_star = (p - a) - ((1 - p) / b)
    return f_star

# 2) この式が最も一般的に使用され、「正式な」ケリー基準の計算式と考えられる理由は以下の通りです:
# 完全性:この式は勝ちの確率(win_prob)、勝ちの場合の利益率(win_odds)、負けの場合の損失率(loss_odds)のすべてを考慮しています。
# 一般性:この式は様々な状況に適用できる汎用性があります。
# 理論的裏付け:この式は情報理論と確率論に基づいて導出されており、長期的な資金成長を最大化するという本来の目的に沿っています。
# 広く認知:この形式の式は、金融やギャンブル理論の文献で最も頻繁に引用されています。
def calc_f2(win_prob, win_odds, loss_odds):
    return (win_prob * win_odds - (1 - win_prob) * loss_odds) / win_odds

# 3) 対数を使用した形式で、理論的には正確ですが、実用的には f2 と近似します。
def calc_f3(win_prob, win_odds, loss_odds):
    return (win_prob * math.log(1 + win_odds) - (1 - win_prob) * math.log(1 + loss_odds)) / math.log(1 + win_odds)

# 4) リスク調整版で、実践的な使用には適していますが、純粋な理論的ケリー基準ではありません。
def calc_f4(win_prob, win_odds, loss_odds, risk_factor=0.5):
    kelly_fraction = (win_prob * win_odds - (1 - win_prob) * loss_odds) / win_odds
    return kelly_fraction * risk_factor

# 5) ドローダウン制約を加えた実践的なバージョンですが、純粋なケリー基準ではありません。
# この関数は f2 の計算結果と、最大ドローダウン制約に基づく値の小さい方を選びます。期待値が低い場合は f2 と同じ結果になりますが、高い期待値(この例では84%以上)では制約が効いて異なる値になっています。
def calc_f5(win_prob, win_odds, loss_odds, max_drawdown=0.2):
    kelly_fraction = (win_prob * win_odds - (1 - win_prob) * loss_odds) / win_odds
    drawdown_constraint = math.log(1 - max_drawdown) / math.log(1 - loss_odds)
    return min(kelly_fraction, drawdown_constraint)

# 6) 完全にランダムで賭ける
def calc_f6(win_prob, win_odds, loss_odds):
    return random.random()

def calc_f_star(win_prob, win_odds, loss_odds):
    if args.f_star_type == 0:
        return calc_f0(win_prob, win_odds)
    elif args.f_star_type == 1:
        return calc_f1(win_prob, win_odds, loss_odds)
    elif args.f_star_type == 2:
        return calc_f2(win_prob, win_odds, loss_odds)
    elif args.f_star_type == 3:
        return calc_f3(win_prob, win_odds, loss_odds)
    elif args.f_star_type == 4:
        return calc_f4(win_prob, win_odds, loss_odds)
    elif args.f_star_type == 5:
        return calc_f5(win_prob, win_odds, loss_odds)
    elif args.f_star_type == 6:
        return calc_f6(win_prob, win_odds, loss_odds)
    else:
        raise Exception("無効なf_star_type")

# 楽天証券の手数料
# 「超割コース」の現物取引手数料
# ref: https://www.rakuten-sec.co.jp/web/domestic/stock/commission.html
def calculate_rakuten_fee(trade_amount):
    if trade_amount <= 50000:
        return 55
    elif trade_amount <= 100000:
        return 99
    elif trade_amount <= 200000:
        return 115
    elif trade_amount <= 500000:
        return 275
    elif trade_amount <= 1000000:
        return 535
    elif trade_amount <= 1500000:
        return 640
    elif trade_amount <= 30000000:
        return 1013
    else:
        return 1070

def apply_trade_with_fees(funds, bet_amount, win, win_odds, loss_odds):
    
    # 買い注文の手数料を計算
    buy_fee = calculate_rakuten_fee(bet_amount)
    
    # 買い注文を実行(手数料を差し引く)
    funds -= bet_amount
    funds -= buy_fee
    
    # 売り注文の結果を計算
    if win:
        profit = bet_amount * win_odds
    else:
        profit = -bet_amount * loss_odds
    
    # 売り注文の手数料を計算
    sell_amount = bet_amount + profit
    sell_fee = calculate_rakuten_fee(sell_amount)
    
    # 売り注文を実行(手数料を差し引く)
    funds += sell_amount
    funds -= sell_fee

    both_fee = buy_fee + sell_fee
    
    return funds, both_fee

# ケリー基準に従い
# 期待値が10,20,30,40,50%...の場合の賭け金を計算
def table_f_star():
#    loss_odds = win_odds / 2 # 負けた場合の損失率を半分にする
#    loss_odds = 1            # 負けた場合の損失率を1にする
    loss_odds = win_odds     # 負けた場合の損失率を同じにする
    
    print(f" - win_odds  = {win_odds:.2f}")
    print(f" - loss_odds = {loss_odds:.2f}")
    print (f"ケリー基準に従い、期待値が10,20,30,40,50%...の場合の賭け金を計算します (オッズ: {win_odds:.2f})")
    for win_prob_percent in range(0, 101, 4):
        win_prob = win_prob_percent / 100
        f0 = calc_f0(win_prob, win_odds)
        f1 = calc_f1(win_prob, win_odds, loss_odds)
        f2 = calc_f2(win_prob, win_odds, loss_odds)
        f3 = calc_f3(win_prob, win_odds, loss_odds)
        f4 = calc_f4(win_prob, win_odds, loss_odds)
        f5 = calc_f5(win_prob, win_odds, loss_odds)
        print(f"期待値: {win_prob_percent:3d}% -> f0: {f0:+.2f}, f1: {f1:+.2f}, f2: {f2:+.2f}, f3: {f3:+.2f}, f4: {f4:+.2f}, f5: {f5:+.2f}")

# print_now_status 関数も手数料を表示するように修正
def print_now_status(i, win_prob, bet_amount, funds, f_star, fees):
    if not args.verbose:
        return
    man_funds = funds / 10000
    print(f" ({i + 1}回目) 勝率: {win_prob:.2f}, 取引額: {bet_amount:.2f}, f_star: {f_star:.2f} 資金: {funds:.2f} ({man_funds:.2f}万円) 手数料: {fees:.2f}")

# 掛けをスキップした際の状況を表示する関数
def print_now_status_if_skip(i, win_prob, f_star):
    if not args.verbose:
        return
    print(f" -> skip ({i + 1}回目) 勝率: {win_prob:.2f} f_star: {f_star:.2f}")    

# シミュレーションを実行する関数
def run_simulation():
    funds = initial_funds
    total_fees = 0
    for i in range(num_bets):
        # (実際の)勝つ確率をランダムに決定 / 0.2~0.8の範囲に収める
        e_min = 0.2
        e_max = args.expect_max
        real_win_prob = random.uniform(e_min, e_max)

        # 期待値は実際の勝率から-0.2~0.2の範囲でランダムに変動
        w = args.expect_width
        win_prob = real_win_prob + random.uniform(-w, w)

        # 0~1の範囲に収める
        win_prob = max(0, min(1, win_prob))

        # (期待値計算後にさらに) 勝率をランダムにする場合
        if args.random_real:
            real_win_prob = random.random()
        
        # ケリー基準による賭け額の割合を計算
        f_star = calc_f_star(win_prob, win_odds, loss_odds)

        # f_starを固定値にする場合
        if args.fix_f_star is not None:
            f_star = args.fix_f_star
            # 勝率が50%以下の場合は取引しない
            if win_prob < 0.5:
                print_now_status_if_skip(i, win_prob, f_star)
                continue

        # f_starが負の場合は取引しない
        if f_star < 0:
            print_now_status_if_skip(i, win_prob, f_star)
            continue
        
		# foundsが0以下になった場合は取引できない
        if funds <= 0:
            print_now_status_if_skip(i, win_prob, f_star)
            continue
        
        # 取引額を計算
        bet_amount = funds * f_star
        
        # 勝敗を決定
        b_win = random.random() < real_win_prob

        # 取引を適用し、手数料を考慮
        funds, fees = apply_trade_with_fees(funds, bet_amount, b_win, win_odds, loss_odds)
        
        # 手数料を加算
        total_fees += fees
        
        print_now_status(i, win_prob, bet_amount, funds, f_star, fees)
    
    return funds, total_fees

# 円の金額を万円、億円、兆円表示というように認知しやすい値に変換
def readable_yen(amount):
	if amount >= 1000000000000:
		return f"{amount / 1000000000000:.2f}兆円"
	elif amount >= 100000000:
		return f"{amount / 100000000:.2f}億円"
	elif amount >= 10000:
		return f"{amount / 10000:.2f}万円"
	else:
		return f"{amount:.2f}円"

def simu_main():
    # 繰り返しシミュレーションを実行
    final_funds_list = []
    total_fees_list = []
    for sim in range(args.num_simulations):
        if not args.quiet:
            print(f"●シミュレーション{sim + 1}を開始")
        final_funds, total_fees = run_simulation()
        final_funds_list.append(final_funds)
        total_fees_list.append(total_fees)
        if not args.quiet:
            print(f"   => シミュレーション{sim + 1}の最終金額: {final_funds:.2f}円 | " + readable_yen(final_funds)) 
            print(f"   => シミュレーション{sim + 1}の総手数料: {total_fees:.2f}円 | " + readable_yen(total_fees))

    print("-- 統計結果 ------------------------------------------------------------")

    # 一回のシミュレーションあたりのゲーム回数
    print(f"・1シミュレーションあたりのゲーム回数: {num_bets}回")

    # 平均金額を計算
    average_funds = sum(final_funds_list) / len(final_funds_list)
    
    print(f"・{args.num_simulations}回のシミュレーションの平均最終金額: {average_funds:.2f}円 | " + readable_yen(average_funds))

    # 初期額よりも多くなった回数を計算
    num_wins = len([f for f in final_funds_list if f > initial_funds])
    r_initial_funds = readable_yen(initial_funds)
    print(f"・初期資金({r_initial_funds})よりも多くなった回数: {num_wins}回 ({num_wins / args.num_simulations * 100:.2f}%)")

    # 最高値
    max_funds = max(final_funds_list)
    print(f"・最高金額: " + readable_yen(max_funds))

    # 最低値
    min_funds = min(final_funds_list)
    print(f"・最低金額: " + readable_yen(min_funds))

    # 標準偏差を計算
    import math
    variance = sum([(f - average_funds) ** 2 for f in final_funds_list]) / len(final_funds_list)
    stddev = math.sqrt(variance)
    print(f"・標準偏差: {stddev:.2f}")

    # 信頼区間を計算
    # 95%信頼区間: 平均値の±1.96倍の標準偏差
    # 99%信頼区間: 平均値の±2.58倍の標準偏差
    confidence_interval = 1.96
    print(f"・95%信頼区間: {average_funds - confidence_interval * stddev:.2f}{average_funds + confidence_interval * stddev:.2f}")
    
    # 平均総手数料を計算
    average_fees = sum(total_fees_list) / len(total_fees_list)
    print(f"・{args.num_simulations}回のシミュレーションの平均総手数料: " + readable_yen(average_fees))

# 使用したf_starの計算方法を表示
def print_star_func_detail(): 
    print ("------------------------------------------------------------------------")
    print("使用したf_starの計算方法:")
    print(" - f_star_type: ", args.f_star_type)

    if args.f_star_type == 0:
        print("f_starの計算方法: win_prob - ((1 - win_prob) / win_odds)")
    elif args.f_star_type == 1:
        print("f_starの計算方法: win_prob - loss_odds - ((1 - win_prob) / (win_odds + 1))")
    elif args.f_star_type == 2:
        print("f_starの計算方法: (win_prob * win_odds - (1 - win_prob) * loss_odds) / win_odds")
    elif args.f_star_type == 3:
        print("f_starの計算方法: (win_prob * math.log(1 + win_odds) - (1 - win_prob) * math.log(1 + loss_odds)) / math.log(1 + win_odds)")
    elif args.f_star_type == 4:
        print("f_starの計算方法: (win_prob * win_odds - (1 - win_prob) * loss_odds) / win_odds * risk_factor")
    elif args.f_star_type == 5:
        print("f_starの計算方法: min(kelly_fraction, drawdown_constraint) where drawdown_constraint = math.log(1 - max_drawdown) / math.log(1 - loss_odds)")
    elif args.f_star_type == 6:
        print("f_starの計算方法: random.random()")
    else:
        raise Exception("Invalid f_star_type")    

if __name__ == "__main__":

    if args.show_table:
        table_f_star()
        exit()

    simu_main()

    # 使用したf_starの計算方法を表示
    if args.fix_f_star is not None:
        print(f"f_starを固定値に設定: {args.fix_f_star}")
    if args.random_real:
        print("勝率を期待値をはずれてランダムにする")

    print_star_func_detail()





いいなと思ったら応援しよう!