"""Educational independent2x model: p=.485, fractional current-balance stakes.

No currency rounding, minimum stake, fees, deposits or profit stop. A crossing
below1 unit is an operational cutoff, not literal zero or inability to fund a
fractional stake. The250-unit event is a50% initial-balance drawdown, not ruin.
Usage: python fractional_bankroll.py [--sessions 20000]
"""
import argparse
from fractions import Fraction
from functools import lru_cache
import json
import logging
import math
import random


@lru_cache(maxsize=None)
def crossing_probability(fraction, barrier, rounds=500, start=500, exact=False):
    fraction = Fraction(fraction)
    if not 0 < fraction < 1 or not isinstance(rounds, int) or rounds < 0:
        raise ValueError('Fraction in(0,1) and nonnegative integer horizon required')
    if not isinstance(start, int) or not isinstance(barrier, int) or min(start, barrier) <= 0:
        raise ValueError('Positive integer start and barrier required')
    if start < barrier:
        return Fraction(1) if exact else 1.0
    numerator, denominator = fraction.numerator, fraction.denominator
    up, down = denominator + numerator, denominator - numerator
    # Exact integer comparisons define the barrier despite floating probability mass.
    ups = [up ** n for n in range(rounds + 1)]
    downs = [down ** n for n in range(rounds + 1)]
    denominators = [denominator ** n for n in range(rounds + 1)]
    zero = Fraction(0) if exact else 0.0
    if start * downs[-1] >= barrier * denominators[-1]:
        return zero
    p = Fraction(97, 200) if exact else .485
    q = 1 - p
    active, absorbed = {0: 1-zero}, zero
    for n in range(1, rounds + 1):
        following = {}
        for wins, mass in active.items():
            for after_wins, chance in ((wins+1, p), (wins, q)):
                moved = mass * chance
                if start * ups[after_wins] * downs[n-after_wins] < barrier * denominators[n]:
                    absorbed += moved
                else:
                    following[after_wins] = following.get(after_wins, zero) + moved
        active = following
    total = absorbed + sum(active.values(), zero)
    assert total == 1 if exact else abs(total-1) < 1e-11
    return absorbed


def loss_run_probability(length, rounds=100, exact=False):
    if not isinstance(length, int) or length < 1 or not isinstance(rounds, int) or rounds < 0:
        raise ValueError('Positive integer length and nonnegative horizon required')
    p = Fraction(97, 200) if exact else .485
    q = 1-p
    zero = Fraction(0) if exact else 0.0
    active, absorbed = [1-zero] + [zero]*(length-1), zero
    for _ in range(rounds):
        following = [sum(active)*p] + [zero]*(length-1)
        for run, mass in enumerate(active):
            if run+1 == length:
                absorbed += mass*q
            else:
                following[run+1] += mass*q
        active = following
    return absorbed


def reference_table():
    return dict(scope='Independent2x model, current fraction, no rounding or minimum',
                start=500, rounds=500,
                fractions=[dict(fraction=float(f),
                    allLossBalance=float(500*(1-f)**500),
                    below1=float(crossing_probability(f, 1)),
                    below250=float(crossing_probability(f, 250)))
                    for f in (Fraction(1,100), Fraction(1,40), Fraction(1,20))],
                runs=[dict(length=k, fixedBlockProbability=float(Fraction(103,200)**k),
                           anywhereIn100=float(loss_run_probability(k))) for k in (4,6,8,10)])


def monte_carlo(sessions=20000, seed=20261023):
    if not isinstance(sessions, int) or sessions < 1:
        raise ValueError('Positive integer session count required')
    results = []
    for index, fraction in enumerate((.01,.025,.05)):
        rng = random.Random(seed+index)
        below1 = below250 = 0
        for _ in range(sessions):
            bank, crossed250 = 500.0, False
            for _ in range(500):
                bank *= 1+fraction if rng.random() < .485 else 1-fraction
                crossed250 |= bank < 250
                if bank < 1:
                    below1 += 1
                    break
            below250 += crossed250
        results.append(dict(fraction=fraction, sessions=sessions,
                            below1Count=below1, below250Count=below250,
                            below1Rate=below1/sessions, below250Rate=below250/sessions))
    # Hoeffding two-sided error for each individual Bernoulli event, confidence99.9%.
    return dict(seed=seed, perEventConfidence=.999,
                absoluteErrorBound=math.sqrt(math.log(2000)/(2*sessions)), results=results)


if __name__ == '__main__':
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--sessions', type=int, default=0)
    args=parser.parse_args()
    result=reference_table()
    if args.sessions:
        result['simulation']=monte_carlo(args.sessions)
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    logging.info(json.dumps(result, indent=2))
