# Architecture

Arsitektur teknis, pattern, dan konvensi kode platform Koinkamu/Sahamkamu.

## Stack Overview

```
┌─────────────────────────────────────────────────┐
│                   Browser                        │
│  Tailwind CSS (dark) + Alpine.js + Chart.js     │
└──────────────────────┬──────────────────────────┘
                       │ HTTP
┌──────────────────────▼──────────────────────────┐
│               Flask Application                  │
│                                                  │
│  ┌──────────┐  ┌──────────┐  ┌──────────────┐  │
│  │ Blueprints│  │ Services │  │   Models     │  │
│  │ (Routes)  │  │ (Logic)  │  │ (SQLAlchemy) │  │
│  └──────────┘  └──────────┘  └──────────────┘  │
└──────────────────────┬──────────────────────────┘
                       │ PyMySQL
┌──────────────────────▼──────────────────────────┐
│            MySQL / MariaDB                       │
└─────────────────────────────────────────────────┘
```

## Blueprint Structure

### Admin Blueprint (`app/blueprints/admin/`)
- **routes.py** — 164 route functions untuk semua admin pages
- Decorator: `@admin_required` (dari `app/helpers/auth.py`)
- Semua route return `render_template('admin/xxx.html')`

### API Blueprint (`app/blueprints/api/v1/`)
- **market.py** — 249 API endpoints (~6600 lines)
- Decorator: `@login_required`
- Semua route return `jsonify(result)`
- Pattern: lazy import service → call method → return JSON

### Auth Blueprint (`app/blueprints/auth/`)
- User login/logout (`/login`, `/logout`)
- Admin login/logout (`/admin/login`, `/admin/logout`)
- Field: `login_id` (bukan `username`)

### Market Blueprint (`app/blueprints/market/`)
- Public market pages (coin detail, charts)

## Service Pattern

Semua 166 service files mengikuti pattern konsisten:

```python
import math
from app.extensions import db

class ServiceName:
    """Deskripsi service."""

    def scan_all(self, **filters):
        """Main entry point."""
        # 1. Lazy import models (WAJIB di dalam method)
        from app.models.coin import Coin, CoinProfile
        from app.models.range_score import RangeTradingScore
        from app.models.bullish_score import BullishMomentumScore
        from app.models.signal import TradingSignal

        # 2. Query precomputed data
        coins = Coin.query.filter(Coin.is_active == True).all()
        # ... build lookup dicts by coin_id ...

        # 3. Compute scores per coin
        items = []
        for coin in coins:
            score_data = self._compute_score(...)
            if score_data['score'] >= min_threshold:
                items.append({...})

        # 4. Sort, paginate
        items.sort(key=lambda x: x['score'], reverse=True)
        total = len(items)
        start = (page - 1) * limit
        paged = items[start:start + limit]

        # 5. Return standard format
        return {
            'items': paged,
            'total': total,
            'page': page,
            'limit': limit,
            'has_more': start + limit < total,
            'stats': self._compute_stats(items)
        }

    @staticmethod
    def _safe_float(val, default=0.0):
        """Safe float conversion — handles None, NaN, Inf."""
        if val is None:
            return default
        try:
            f = float(val)
            if math.isnan(f) or math.isinf(f):
                return default
            return f
        except (ValueError, TypeError):
            return default
```

### Konvensi Penting

| Konvensi | Detail |
|----------|--------|
| DB import | `from app.extensions import db` (BUKAN `from app.models.base`) |
| Lazy imports | Semua model import di dalam method, bukan di module level |
| Safe float | `_safe_float(val, default=0.0)` — wajib untuk semua numeric fields |
| Response format | `{'items': [], 'total': int, 'page': int, 'limit': int, 'has_more': bool}` |
| Auth decorator | `@admin_required` (sudah include `@login_required`) |

## Model Reference

### Coin (`app/models/coin.py`)
```
- id (str): Primary key, format namespace (COIN.bitcoin, IDX.BBCA, NASDAQ.AAPL)
- symbol (str): Trading symbol
- name (str): Display name
- asset_type (str): 'crypto', 'stock', 'stock_us'
- is_active (bool)
- lot_size (int): 1 for crypto/US, 100 for IDX
```

### CoinProfile (`app/models/coin.py`)
```
- coin_id (str): FK to coins
- current_price_idr (float)
- market_cap_idr (float)
- ath_idr (float): All-time high
- atl_idr (float): All-time low
- price_change_24h_pct (float)
- circulating_supply (float)
- total_supply (float)
- description (text)
- icon_url (str)
```

### RangeTradingScore (`app/models/range_score.py`)
```
- coin_id (str): FK to coins
- timeframe (str): '1h', '4h', '1D', etc.
- score (float): 0-100
- is_mean_reverting (bool)
- mr_confidence (str): 'HIGH', 'MEDIUM', 'LOW'
- zscore (float)
- bb_position (float)
- range_width_pct (float)
- cycle_success_rate_pct (float)
- est_profit_per_cycle_pct (float)
- support_level (float)
- resistance_level (float)
```

### BullishMomentumScore (`app/models/bullish_score.py`)
```
- coin_id (str): FK to coins
- timeframe (str)
- score (float): 0-100
- bullish_phase (str): 'accumulation', 'markup', 'distribution', 'markdown'
- momentum_state (str)
- velocity_short_pct (float)
- velocity_medium_pct (float)
- acceleration (float)
- upside_pct (float)
- ml_trend (str): 'bullish', 'bearish', 'neutral'
- ml_target_price (float)
- ml_short_pct (float)
- ml_medium_pct (float)
- take_profit_1 (float)
- take_profit_2 (float)
- entry_price (float)
```

### TradingSignal (`app/models/signal.py`)
```
- coin_id (str): FK to coins
- timeframe (str)
- signal_type (str): 'BUY', 'SELL'
- status (str): 'active', 'closed'
- confidence (str): 'High', 'Medium', 'Low' (ENUM string, BUKAN float)
- direction_probability (float)
- entry_price (float)
- stop_loss (float): BUKAN stop_loss_price
- take_profit_1 (float): BUKAN target_price
- take_profit_2 (float)
- take_profit_3 (float)
- regime (str)
- mtf_confirmed (bool)
```

## Template Pattern

### Base Layout (`app/templates/base.html`)
- ~3500 lines, dark theme Tailwind CSS
- Sidebar navigation dengan collapsible wave sections
- Desktop + mobile responsive
- Wave theme colors di navigation links

### Feature Template
```html
{% extends "base.html" %}
{% block title %}Feature Name{% endblock %}

{% block content %}
<div x-data="featurePanel()" x-init="init()">
    <!-- Header + Stats Cards -->
    <!-- Filters -->
    <!-- Data Grid/Cards -->
    <!-- Pagination -->
</div>
{% endblock %}

{% block scripts %}
<script>
function featurePanel() {
    return {
        items: [], loading: false, total: 0,
        page: 1, hasMore: false,
        filters: { /* ... */ },

        init() { this.fetchData(); },

        async fetchData() {
            this.loading = true;
            this.page = 1;
            await this._fetch(1);
        },

        async loadMore() {
            await this._fetch(this.page + 1);
        },

        async _fetch(page) {
            const params = new URLSearchParams({
                page, limit: 20,
                // ...filters
            });
            const res = await fetch(`/api/v1/market/endpoint?${params}`);
            const data = await res.json();

            if (page === 1) {
                this.items = data.items;
            } else {
                this.items = [...this.items, ...data.items];
            }
            this.total = data.total;
            this.hasMore = data.has_more;
            this.page = page;
            this.loading = false;
        }
    };
}
</script>
{% endblock %}
```

### Wave Theme Colors

| Wave | Color | Tailwind Classes |
|------|-------|-----------------|
| 1-17 | Default | `text-white`, `bg-slate-700` |
| 18-19 | Green | `text-green-400`, `bg-green-500/10` |
| 20 | Violet | `text-violet-400`, `bg-violet-500/10` |
| 21 | Cyan | `text-cyan-400`, `bg-cyan-500/10` |
| 22 | Orange | `text-orange-400`, `bg-orange-500/10` |

## API Convention

### Standard Response

```json
{
    "items": [
        {
            "coin_id": "COIN.bitcoin",
            "symbol": "BTC",
            "name": "Bitcoin",
            "score": 82.5,
            "tier": "strong",
            "components": { ... },
            "details": { ... }
        }
    ],
    "total": 45,
    "page": 1,
    "limit": 20,
    "has_more": true,
    "stats": {
        "total_candidates": 45,
        "avg_score": 62.3
    }
}
```

### Endpoint Naming

| Pattern | Contoh | Keterangan |
|---------|--------|------------|
| `/api/v1/market/{feature}` | `/api/v1/market/range-trading` | Fitur utama |
| `/api/v1/market/screener/{name}` | `/api/v1/market/screener/multibagger` | Screener tools |
| `/api/v1/market/{feature}/scan` | `/api/v1/market/range-trading/scan` | POST trigger scan |
| `/api/v1/market/{coin_id}/{feature}` | `/api/v1/market/COIN.bitcoin/chart` | Per-coin data |

### Query Parameters

| Param | Type | Default | Keterangan |
|-------|------|---------|------------|
| `page` | int | 1 | Page number |
| `limit` | int | 20 | Max items (capped at 100) |
| `sort` | str | 'score' | Sort field |
| `q` | str | '' | Search query |
| `asset_type` | str | 'all' | Filter: 'all', 'crypto', 'stock', 'stock_us' |
| `timeframe` | str | '1D' | Timeframe filter |

## Data Flow

```
External APIs                    Database                    Frontend
─────────────                    ────────                    ────────
Yahoo Finance  ──┐
CoinGecko      ──┼── sync_data.py ──→ coins              ──┐
Indodax        ──┘                    coin_profiles         │
                                      ohlcv_data            │
                                      stock_extended_data   │
                                            │               │
                                            ▼               │
                                    Scoring Services        │
                                    (scan scripts)          │
                                            │               │
                                            ▼               │
                                      range_trading_scores  │
                                      bullish_momentum_scores│
                                      trading_signals       ├──→ API ──→ Alpine.js
                                      ml_predictions        │
                                            │               │
                                            ▼               │
                                    Aggregator Services     │
                                    (buy_recommender,       │
                                     multibagger_screener,  │
                                     etc.)                 ─┘
```

## Database

- **Engine:** MySQL / MariaDB
- **Charset:** utf8mb4, collation utf8mb4_unicode_ci
- **ORM:** SQLAlchemy (Flask-SQLAlchemy)
- **Connection:** PyMySQL
- **Database name:** `cornbyt1_investasikamu`

### Key Tables

| Table | Keterangan |
|-------|------------|
| `coins` | Master data aset (crypto + stock) |
| `coin_profiles` | Profile, harga, market cap |
| `ohlcv_data` | Candlestick per timeframe |
| `range_trading_scores` | Hasil scoring range trading |
| `bullish_momentum_scores` | Hasil scoring bullish momentum |
| `trading_signals` | Sinyal BUY/SELL aktif |
| `ml_predictions` | Prediksi ML per coin |
| `stock_extended_data` | Yahoo Finance extended data |
| `market_cache` | Cache market-level data |
| `users` | User accounts |
| `admins` | Admin accounts |
| `portfolios` | User portfolios |
| `watchlists` | User watchlists |
| `trade_history` | Riwayat trading |

## Remote Sync

```
Local Development          Production Server
──────────────────         ─────────────────
Push (lokal → prod):       Receives:
  - OHLCV data              - OHLCV data
  - Scores                   - Scores
  - Signals                  - Signals
  - Profiles                 - Profiles

Pull (prod → lokal):       Sends:
  - User data                - User data
  - Watchlists               - Watchlists
  - Portfolio                - Portfolio
  - Trade history            - Trade history
```

Via REST API: `/api/v1/sync/*` dengan API key authentication.
