"""
EURUSD D1 → Bitcoin Elliptic Curve (secp256k1) Mapping
========================================================
We load daily EURUSD prices via MetaTrader 5,
normalize the OHLC data, and project it onto the elliptic curve
y² ≡ x³ + 7 (mod p), used in Bitcoin (secp256k1).

Author: Evgeniy Koshtenko
Experiment: mapping financial data to algebraic geometry
"""

import numpy as np
import json
import hashlib
from datetime import datetime

# ============================================================
# secp256k1 parameters (Bitcoin's elliptic curve)
# ============================================================
# Field prime
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
# Group order
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
# Coefficients of the curve y² = x³ + ax + b
A = 0
B = 7
# Generator point G
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8


# ============================================================
# Arithmetic on the secp256k1 elliptic curve
# ============================================================
def mod_inverse(a, m=P):
    """Modular inversion using the extended Euclidean algorithm"""
    if a < 0:
        a = a % m
    g, x, _ = extended_gcd(a, m)
    if g != 1:
        raise ValueError("Modular inversion does not exist")
    return x % m


def extended_gcd(a, b):
    if a == 0:
        return b, 0, 1
    g, x, y = extended_gcd(b % a, a)
    return g, y - (b // a) * x, x


def point_add(p1, p2):
    """Adding two points on the curve"""
    if p1 is None:
        return p2
    if p2 is None:
        return p1

    x1, y1 = p1
    x2, y2 = p2

    if x1 == x2 and y1 != y2:
        return None  # point at infinity

    if x1 == x2:
        # Point doubling
        lam = (3 * x1 * x1 + A) * mod_inverse(2 * y1) % P
    else:
        lam = (y2 - y1) * mod_inverse(x2 - x1) % P

    x3 = (lam * lam - x1 - x2) % P
    y3 = (lam * (x1 - x3) - y1) % P
    return (x3, y3)


def scalar_mult(k, point):
    """Scalar multiplication of a point (double-and-add)"""
    result = None
    addend = point
    while k:
        if k & 1:
            result = point_add(result, addend)
        addend = point_add(addend, addend)
        k >>= 1
    return result


def is_on_curve(x, y):
    """Checking whether a point lies on the curve y² ≡ x³ + 7 (mod p)"""
    return (y * y - x * x * x - B) % P == 0


def tonelli_shanks(n, p):
    """Tonelli–Shanks algorithm for finding a square root modulo p"""
    if pow(n, (p - 1) // 2, p) != 1:
        return None  # no square root

    # Factorization of p − 1 = Q * 2^S
    Q = p - 1
    S = 0
    while Q % 2 == 0:
        Q //= 2
        S += 1

    if S == 1:
        return pow(n, (p + 1) // 4, p)

    # Finding a quadratic non-residue
    z = 2
    while pow(z, (p - 1) // 2, p) != p - 1:
        z += 1

    M = S
    c = pow(z, Q, p)
    t = pow(n, Q, p)
    R = pow(n, (Q + 1) // 2, p)

    while True:
        if t == 1:
            return R
        i = 0
        temp = t
        while temp != 1:
            temp = pow(temp, 2, p)
            i += 1
        b = pow(c, 1 << (M - i - 1), p)
        M = i
        c = pow(b, 2, p)
        t = (t * c) % p
        R = (R * b) % p


def price_to_curve_point(value, salt="eurusd"):
    """
    Mapping a numeric price value to a point on the secp256k1 elliptic curve.
    
    Method: 
    1. Hash the value to obtain the x-coordinate in the field Fp
    2. Compute y² = x³ + 7 mod p
    3. If a square root exists, the point on the curve has been found
    4. Otherwise, increment and try again (try-and-increment)
    """
    value_str = f"{salt}:{value:.10f}"
    
    for nonce in range(256):
        data = f"{value_str}:{nonce}".encode('utf-8')
        h = hashlib.sha256(data).digest()
        x = int.from_bytes(h, 'big') % P

        # y² = x³ + 7 (mod p)
        y_sq = (pow(x, 3, P) + B) % P
        y = tonelli_shanks(y_sq, P)

        if y is not None:
            # Take an even y (canonical form)
            if y % 2 != 0:
                y = P - y
            assert is_on_curve(x, y), "Point is not on the curve!"
            return (x, y, nonce)
    
    return None


def map_ohlc_to_curve(ohlc_data):
    """
    Mapping an array of OHLC data to points on an elliptic curve.
    
    For each candle, we create 4 points (O, H, L, C) and 
    additional metrics:
    - body_point: |Close - Open| → point
    - range_point: |High - Low| → point  
    - momentum: scalar multiplication of points
    """
    results = []
    
    for i, candle in enumerate(ohlc_data):
        date_str = candle['date']
        o, h, l, c = candle['open'], candle['high'], candle['low'], candle['close']
        
        # Mapping each price to a point on the curve
        pt_open = price_to_curve_point(o, f"open:{i}")
        pt_high = price_to_curve_point(h, f"high:{i}")
        pt_low = price_to_curve_point(l, f"low:{i}")
        pt_close = price_to_curve_point(c, f"close:{i}")
        
        # Candlestick body size → point
        body = abs(c - o)
        pt_body = price_to_curve_point(body, f"body:{i}")
        
        # Range → point
        range_val = h - l
        pt_range = price_to_curve_point(range_val, f"range:{i}")
        
        # Adding the Open and Close points on the curve
        if pt_open and pt_close:
            combined = point_add(
                (pt_open[0], pt_open[1]),
                (pt_close[0], pt_close[1])
            )
        else:
            combined = None
        
        # Scalar multiplication: direction * G
        # direction = +1 (bullish) or -1 (bearish) → map to 1 or N-1
        direction = 1 if c >= o else N - 1
        directional_point = scalar_mult(direction, (Gx, Gy))
        
        result = {
            'index': i,
            'date': date_str,
            'ohlc': {'open': o, 'high': h, 'low': l, 'close': c},
            'curve_points': {
                'open': {'x': hex(pt_open[0])[:20] + '...', 'y': hex(pt_open[1])[:20] + '...', 'nonce': pt_open[2]} if pt_open else None,
                'high': {'x': hex(pt_high[0])[:20] + '...', 'y': hex(pt_high[1])[:20] + '...', 'nonce': pt_high[2]} if pt_high else None,
                'low': {'x': hex(pt_low[0])[:20] + '...', 'y': hex(pt_low[1])[:20] + '...', 'nonce': pt_low[2]} if pt_low else None,
                'close': {'x': hex(pt_close[0])[:20] + '...', 'y': hex(pt_close[1])[:20] + '...', 'nonce': pt_close[2]} if pt_close else None,
            },
            'body_point': {'x': hex(pt_body[0])[:20] + '...', 'nonce': pt_body[2]} if pt_body else None,
            'range_point': {'x': hex(pt_range[0])[:20] + '...', 'nonce': pt_range[2]} if pt_range else None,
            'combined_oc': {'x': hex(combined[0])[:20] + '...', 'y': hex(combined[1])[:20] + '...'} if combined else None,
            'direction': 'BULL' if c >= o else 'BEAR',
            'directional_point': {'x': hex(directional_point[0])[:20] + '...', 'y': hex(directional_point[1])[:20] + '...'} if directional_point else None,
        }
        results.append(result)
    
    return results


def analyze_curve_distances(mapped_data):
    """
    Analysis of distances between points on the curve.
    Distance = difference in x-coordinates (mod p) — 
    a “geometric proximity” metric for candles in the curve space.
    """
    distances = []
    for i in range(1, len(mapped_data)):
        prev = mapped_data[i-1]
        curr = mapped_data[i]
        
        if prev['curve_points']['close'] and curr['curve_points']['open']:
            # Take the full x-coordinates for the distance
            x1_hex = prev['curve_points']['close']['x']
            x2_hex = curr['curve_points']['open']['x']
            
            distances.append({
                'from_date': prev['date'],
                'to_date': curr['date'],
                'from_dir': prev['direction'],
                'to_dir': curr['direction'],
            })
    
    return distances


def generate_demo_data(n_candles=100):
    """
    Generating realistic EURUSD D1 data for demonstration purposes.
    In production, replace this with the MT5 loader (see mt5_loader below).
    """
    np.random.seed(42)
    
    base_price = 1.0850
    prices = [base_price]
    
    for i in range(n_candles * 4):
        change = np.random.normal(0, 0.0015)
        # Add mean reversion
        mean_rev = -0.01 * (prices[-1] - base_price)
        prices.append(prices[-1] + change + mean_rev)
    
    ohlc_data = []
    date = datetime(2024, 1, 2)
    
    for i in range(n_candles):
        idx = i * 4
        chunk = prices[idx:idx+4]
        o = chunk[0]
        c = chunk[-1]
        h = max(chunk) + abs(np.random.normal(0, 0.0005))
        l = min(chunk) - abs(np.random.normal(0, 0.0005))
        
        ohlc_data.append({
            'date': date.strftime('%Y-%m-%d'),
            'open': round(o, 5),
            'high': round(h, 5),
            'low': round(l, 5),
            'close': round(c, 5),
        })
        
        # Skip weekends
        from datetime import timedelta
        date += timedelta(days=1)
        while date.weekday() >= 5:
            date += timedelta(days=1)
    
    return ohlc_data


def mt5_loader(symbol="EURUSD", timeframe_name="D1", n_candles=100):
    """
    Loading data via MetaTrader 5.
    Requires the MT5 terminal to be installed.
    
    pip install MetaTrader5
    """
    try:
        import MetaTrader5 as mt5
        
        if not mt5.initialize():
            print(f"MT5 initialize() failed: {mt5.last_error()}")
            return None
        
        # Timeframe mapping
        tf_map = {
            'M1': mt5.TIMEFRAME_M1, 'M5': mt5.TIMEFRAME_M5,
            'M15': mt5.TIMEFRAME_M15, 'M30': mt5.TIMEFRAME_M30,
            'H1': mt5.TIMEFRAME_H1, 'H4': mt5.TIMEFRAME_H4,
            'D1': mt5.TIMEFRAME_D1, 'W1': mt5.TIMEFRAME_W1,
            'MN1': mt5.TIMEFRAME_MN1,
        }
        
        timeframe = tf_map.get(timeframe_name, mt5.TIMEFRAME_D1)
        rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n_candles)
        
        if rates is None or len(rates) == 0:
            print(f"No data available for {symbol}")
            mt5.shutdown()
            return None
        
        ohlc_data = []
        for r in rates:
            ohlc_data.append({
                'date': datetime.fromtimestamp(r['time']).strftime('%Y-%m-%d'),
                'open': round(float(r['open']), 5),
                'high': round(float(r['high']), 5),
                'low': round(float(r['low']), 5),
                'close': round(float(r['close']), 5),
            })
        
        mt5.shutdown()
        return ohlc_data
        
    except ImportError:
        print("MetaTrader 5 is not installed. Using demo data.")
        return None


# ============================================================
# MAIN: Starting the experiment
# ============================================================
if __name__ == "__main__":
    print("=" * 70)
    print("  EURUSD D1 → secp256k1 Elliptic Curve Mapping")
    print("  Experiment: financial data in the space of the Bitcoin curve")
    print("=" * 70)
    
    # Trying MT5; fallback to demo data
    N_CANDLES = 100
    ohlc = mt5_loader("EURUSD", "D1", N_CANDLES)
    if ohlc is None:
        print("\n[INFO] MT5 is unavailable; generating realistic demo data...")
        ohlc = generate_demo_data(N_CANDLES)
    
    print(f"\n[DATA] Loaded {len(ohlc)} EURUSD D1 candles")
    print(f"  Period: {ohlc[0]['date']} — {ohlc[-1]['date']}")
    print(f"  Price range: {min(c['low'] for c in ohlc):.5f} — {max(c['high'] for c in ohlc):.5f}")
    
    # Mapping to an elliptic curve
    print("\n[MAPPING] Projecting OHLC → secp256k1...")
    mapped = map_ohlc_to_curve(ohlc)
    
    # Statistics
    bull_count = sum(1 for m in mapped if m['direction'] == 'BULL')
    bear_count = len(mapped) - bull_count
    avg_nonce = np.mean([m['curve_points']['close']['nonce'] for m in mapped if m['curve_points']['close']])
    
    print(f"\n[STATS] Mapping results:")
    print(f"  Bullish candles: {bull_count} ({100*bull_count/len(mapped):.1f}%)")
    print(f"  Bearish candles: {bear_count} ({100*bear_count/len(mapped):.1f}%)")
    print(f"  Average nonce (attempts to find a point): {avg_nonce:.2f}")
    
    # Showing the first 5 mappings
    print(f"\n[SAMPLE] First 5 mappings:")
    print("-" * 70)
    for m in mapped[:5]:
        print(f"  {m['date']} | {m['direction']:4s} | "
              f"O={m['ohlc']['open']:.5f} C={m['ohlc']['close']:.5f}")
        cp = m['curve_points']
        print(f"    Open  → x={cp['open']['x']}  nonce={cp['open']['nonce']}")
        print(f"    Close → x={cp['close']['x']}  nonce={cp['close']['nonce']}")
        if m['combined_oc']:
            print(f"    O+C   → x={m['combined_oc']['x']}")
        print()
    
    # Analysis of nonce patterns
    nonces_close = [m['curve_points']['close']['nonce'] for m in mapped if m['curve_points']['close']]
    nonces_open = [m['curve_points']['open']['nonce'] for m in mapped if m['curve_points']['open']]
    
    print(f"[ANALYSIS] Nonce distribution (Close):")
    unique, counts = np.unique(nonces_close, return_counts=True)
    for u, c in zip(unique[:10], counts[:10]):
        bar = "█" * c
        print(f"  nonce={u}: {bar} ({c})")
    
    # Analysis: correlation between nonce and direction
    bull_nonces = [m['curve_points']['close']['nonce'] for m in mapped 
                   if m['direction'] == 'BULL' and m['curve_points']['close']]
    bear_nonces = [m['curve_points']['close']['nonce'] for m in mapped 
                   if m['direction'] == 'BEAR' and m['curve_points']['close']]
    
    print(f"\n[INSIGHT] Average nonce:")
    print(f"  Bullish candles: {np.mean(bull_nonces):.3f}")
    print(f"  Bearish Candlesticks: {np.mean(bear_nonces):.3f}")
    print(f"  Difference: {abs(np.mean(bull_nonces) - np.mean(bear_nonces)):.3f}")
    
    # Export for visualization
    export_data = {
        'metadata': {
            'symbol': 'EURUSD',
            'timeframe': 'D1',
            'n_candles': len(ohlc),
            'curve': 'secp256k1',
            'equation': 'y² = x³ + 7 (mod p)',
        },
        'candles': ohlc,
        'mapped': [{
            'date': m['date'],
            'direction': m['direction'],
            'ohlc': m['ohlc'],
            'nonce_open': m['curve_points']['open']['nonce'] if m['curve_points']['open'] else None,
            'nonce_close': m['curve_points']['close']['nonce'] if m['curve_points']['close'] else None,
            'x_close_prefix': m['curve_points']['close']['x'] if m['curve_points']['close'] else None,
        } for m in mapped],
        'stats': {
            'bull_pct': round(100 * bull_count / len(mapped), 1),
            'bear_pct': round(100 * bear_count / len(mapped), 1),
            'avg_nonce': round(float(avg_nonce), 3),
        }
    }
    
    with open('/home/claude/ec_eurusd_results.json', 'w') as f:
        json.dump(export_data, f, indent=2)
    
    print(f"\n[EXPORT] Results saved to ec_eurusd_results.json")
    print("=" * 70)
    print("  Experiment completed.")
    print("  Each EURUSD price now has its own representation")
    print("  as a point on the Bitcoin elliptic curve (secp256k1)")
    print("=" * 70)
