SYSTEM_CORE_VIEWER

DB TABLE
Target Object / Identifier
daemon_upbit_Ticker_user
업비트 개인 자산 플랫폼
업비트 플랫폼 -> 1초봉 복사
Exchange
업비트
Type / Form
매집 | 개인자산
DAEMON / DB TABLE
DB TABLE
Table Name
daemon_upbit_Ticker_user
Daemon Process
daemon_upbit_Ticker_user.php
Version
1.0
[STATUS] INACTIVE
[ID] #2
[TABLE] daemon_upbit_Ticker_user
[ENGINE] InnoDB
[ROWS] 10
Daemon Source Code (daemon_upbit_Ticker_user.php)
#!/usr/bin/php
<?php
/**
 * ============================================================
 * 업비트 개인 자산 계산 + DB 저장 데몬 (순수 CLI 전용)
 * - [수정] locked 기준으로만 처리하던 버그 수정: balance/locked 합산 기준으로 자산 계산/저장
 * - [수정] 매 루프마다 기존 테이블을 비우고(DELETE) 최신 스냅샷만 재삽입
 * - [수정] DB 재연결(SELECT 1) 방어 추가
 * - [수정] DB 재연결 불가 결함 수정 (include를 통한 PDO 객체 갱신)
 * - [수정] 장기 실행 메모리 정리(gc_collect_cycles) 추가
 * ============================================================
 */

error_reporting(E_ALL);
ini_set('display_errors', 1);
date_default_timezone_set('Asia/Seoul');

// ------------------------------------------------------------
// 0. CLI 전용 가드 및 메모리 관리 활성화
// ------------------------------------------------------------
if (php_sapi_name() !== 'cli') {
    echo "CLI 전용 데몬입니다.\n";
    exit;
}
gc_enable(); // 가비지 컬렉션 활성화

// 반드시 가장 위에
$DAEMON_ID = pathinfo(__FILE__, PATHINFO_FILENAME);

// [수정] 상단 require_once 제거 (함수 내부에서 실시간 재연결을 위해 include 사용)

// ------------------------------------------------------------
// 1. DB 연결 (재연결 지원)
// ------------------------------------------------------------
function get_db_connection() {
    // [수정] 호출될 때마다 파일을 다시 로드하여 새로운 $db_upbit(PDO 객체)를 생성
    // require_once는 재할당이 안 되므로 include를 사용합니다.
    $db_upbit = null;
    @include '/home/www/DB/db_upbit.php'; 

    try {
        if (!($db_upbit instanceof PDO)) {
            return null;
        }
        $pdo = $db_upbit;
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return $pdo;
    } catch (Throwable $e) {
        return null;
    }
}

$pdo = get_db_connection();

// ------------------------------------------------------------
// 2. daemon_record 준비
// ------------------------------------------------------------
$server_ip = trim(shell_exec("hostname -I | awk '{print $1}'")) ?: 'CLI';

$rec_stmt = null;
$stop_stmt = null;

if ($pdo) {
    $rec_stmt = $pdo->prepare("
        INSERT INTO daemon_record (d_id, d_category, d_pid, d_status, d_heartbeat, d_ip, d_start_time)
        VALUES (:id, 'UPBIT', :pid, 'RUNNING', NOW(), :ip, NOW())
        ON DUPLICATE KEY UPDATE
            d_pid = VALUES(d_pid),
            d_status = 'RUNNING',
            d_heartbeat = NOW(),
            d_ip = VALUES(d_ip)
    ");

    $stop_stmt = $pdo->prepare("UPDATE daemon_record SET d_status='STOPPED', d_heartbeat=NOW() WHERE d_id=:id");

    // 최초 등록
    $rec_stmt->execute([':id'=>$DAEMON_ID, ':pid'=>getmypid(), ':ip'=>$server_ip]);
}

// ------------------------------------------------------------
// 3. 개인자산 계산 함수
// ------------------------------------------------------------
function run_upbit_user_asset_once(PDO $pdo) {

    // 간단 HTTP JSON (timeout)
    $http_json = function(string $url) {
        $ctx = stream_context_create([
            'http' => [
                'timeout' => 6,
                'header'  => "User-Agent: upbit-ghost\r\n"
            ]
        ]);
        $raw = @file_get_contents($url, false, $ctx);
        return $raw ? json_decode($raw, true) : null;
    };

    require '/home/www/DB/key_upbit_trade.php';
    $ACCESS_KEY = $UPBIT_ACCESS_KEY ?? '';
    $SECRET_KEY = $UPBIT_SECRET_KEY ?? '';
    $SERVER_URL = $UPBIT_SERVER_URL ?? "https://api.upbit.com";

    if (!$ACCESS_KEY || !$SECRET_KEY) return;

    $b64url = function ($d) { return rtrim(strtr(base64_encode($d), '+/', '-_'), '='); };
    $make_jwt = function ($ak, $sk) use ($b64url) {
        $h = ['alg'=>'HS256','typ'=>'JWT'];
        $p = ['access_key'=>$ak,'nonce'=>uniqid('',true)];
        $hh = $b64url(json_encode($h));
        $pp = $b64url(json_encode($p));
        $ss = $b64url(hash_hmac('sha256', "$hh.$pp", $sk, true));
        return "$hh.$pp.$ss";
    };

    $jwt = $make_jwt($ACCESS_KEY, $SECRET_KEY);

    // 1) 계좌
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $SERVER_URL."/v1/accounts",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["Content-Type: application/json","Authorization: Bearer {$jwt}"],
        CURLOPT_TIMEOUT => 10,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);

    $accounts = json_decode($resp, true);
    if (!is_array($accounts)) return;

    // KRW 잔액
    $cash_balance = 0.0;
    foreach ($accounts as $a) {
        if (($a['currency'] ?? '') === 'KRW') {
            $cash_balance = floatval($a['balance'] ?? 0) + floatval($a['locked'] ?? 0);
            break;
        }
    }

    // 2) 마켓전체
    $markets = $http_json($SERVER_URL."/v1/market/all?isDetails=false");
    $market_map = [];
    if (is_array($markets)) {
        foreach ($markets as $m) {
            if (!isset($m['market'])) continue;
            if (strpos($m['market'], 'KRW-') === 0) {
                $sym = substr($m['market'], 4);
                $market_map[$sym] = $m['market'];
            }
        }
    }

    // 필요 마켓 (balance/locked 합산 > 0)
    $markets_needed = [];
    foreach ($accounts as $row) {
        $sym = (string)($row['currency'] ?? '');
        if ($sym === '' || $sym === 'KRW') continue;

        $bal = floatval($row['balance'] ?? 0);
        $lck = floatval($row['locked'] ?? 0);
        if (($bal + $lck) <= 0) continue;

        if (!empty($market_map[$sym])) {
            $markets_needed[] = $market_map[$sym];
        }
    }
    $markets_needed = array_values(array_unique($markets_needed));

    // 3) 시세 조회
    $ticker_map = [];
    if (!empty($markets_needed)) {
        foreach (array_chunk($markets_needed, 100) as $chunk) {
            $tickers = $http_json($SERVER_URL."/v1/ticker?markets=".implode(",", $chunk));
            if (!is_array($tickers)) continue;
            foreach ($tickers as $t) {
                if (isset($t['market'])) {
                    $ticker_map[$t['market']] = floatval($t['trade_price'] ?? 0);
                }
            }
        }
        if (empty($ticker_map)) return; 
    }

    // 계산
    $collected_at = date("Y-m-d H:i:s");
    $collected_ms = (int)(microtime(true)*1000);

    $rows = [];
    $total_buy = 0.0;
    $total_eval = 0.0;
    $total_profit = 0.0;

    foreach ($accounts as $r) {
        $sym = (string)($r['currency'] ?? '');
        if ($sym === '' || $sym === 'KRW') continue;

        $balance = floatval($r['balance'] ?? 0);
        $locked  = floatval($r['locked'] ?? 0);
        $amount  = $balance + $locked;
        if ($amount <= 0) continue;

        $market  = $market_map[$sym] ?? '';
        $current = $market && isset($ticker_map[$market]) ? floatval($ticker_map[$market]) : 0.0;

        // [추가된 필터링] 현재가가 0이면(거래불가/상폐) 수집 안 함
        if ($current <= 0) continue; 

        $avg_buy = floatval($r['avg_buy_price'] ?? 0);
        $avg_mod = floatval($r['avg_buy_price_modified'] ?? 0);

        $buy_amount = $amount * $avg_buy;
        $eval       = $amount * $current;
        $profit     = $eval - $buy_amount;
        $rate       = $buy_amount > 0 ? ($profit/$buy_amount*100) : 0.0;

        $total_buy    += $buy_amount;
        $total_eval   += $eval;
        $total_profit += $profit;

        $rows[] = [
            'currency'=>$sym,
            'balance'=>$balance,
            'locked'=>$locked,
            'avg'=>$avg_buy,
            'avg_mod'=>$avg_mod,
            'unit'=>$r['unit_currency'] ?? 'KRW',
            'price'=>$current,
            'eval'=>$eval,
            'buy'=>$buy_amount,
            'profit'=>$profit,
            'rate'=>$rate
        ];
    }

    $actual_total_asset = $total_eval + $cash_balance;
    $total_rate = $total_buy > 0 ? ($total_profit/$total_buy*100) : 0.0;

    $pdo->beginTransaction();
    try {
        $pdo->exec("DELETE FROM daemon_upbit_Ticker_user");

        $sql = "
            INSERT INTO daemon_upbit_Ticker_user (
                currency, balance, locked, avg_buy_price, avg_buy_price_modified,
                unit_currency, collected_at, collected_ms, current_price,
                evaluation_amount, buy_amount, profit_rate,
                total_asset_value, total_profit_amount, profit_amount,
                total_profit_rate, cash_balance
            ) VALUES (
                :currency, :balance, :locked, :avg, :avg_mod,
                :unit, :at, :ms, :price,
                :eval, :buy, :rate,
                :tval, :tprofit, :profit,
                :trate, :cash
            )
        ";
        $stmt = $pdo->prepare($sql);

        foreach ($rows as $r) {
            $stmt->execute([
                ':currency'=>$r['currency'],
                ':balance'=>$r['balance'],
                ':locked'=>$r['locked'],
                ':avg'=>$r['avg'],
                ':avg_mod'=>$r['avg_mod'],
                ':unit'=>$r['unit'],
                ':at'=>$collected_at,
                ':ms'=>$collected_ms,
                ':price'=>$r['price'],
                ':eval'=>$r['eval'],
                ':buy'=>$r['buy'],
                ':rate'=>$r['rate'],
                ':tval'=>$actual_total_asset,
                ':tprofit'=>$total_profit,
                ':profit'=>$r['profit'],
                ':trate'=>$total_rate,
                ':cash'=>$cash_balance
            ]);
        }

        $pdo->commit();
    } catch (Throwable $e) {
        $pdo->rollBack();
        return;
    }
}

// ------------------------------------------------------------
// 4. 메인 루프
// ------------------------------------------------------------
echo "====================================================\n";
echo "[{$DAEMON_ID}] 순수 CLI 데몬 시작\n";
echo "PID : ".getmypid()."\n";
echo "====================================================\n";

while (true) {

    // [수정] DB 재연결 로직: 끊겼을 경우 get_db_connection()이 파일을 다시 include함
    if (!$pdo) {
        $pdo = get_db_connection();
        $rec_stmt = null; // PDO가 바뀌었으므로 Statement도 초기화
    } else {
        try { 
            $pdo->query("SELECT 1"); 
        } catch (Throwable $e) { 
            $pdo = get_db_connection(); 
            $rec_stmt = null;
        }
    }

    // daemon_record 준비
    if ($pdo && !$rec_stmt) {
        $rec_stmt = $pdo->prepare("
            INSERT INTO daemon_record (d_id, d_category, d_pid, d_status, d_heartbeat, d_ip, d_start_time)
            VALUES (:id, 'UPBIT', :pid, 'RUNNING', NOW(), :ip, NOW())
            ON DUPLICATE KEY UPDATE
                d_pid = VALUES(d_pid),
                d_status = 'RUNNING',
                d_heartbeat = NOW(),
                d_ip = VALUES(d_ip)
        ");
    }

    // 데몬 하트비트
    if ($pdo && $rec_stmt) {
        try {
            $rec_stmt->execute([
                ':id'=>$DAEMON_ID,
                ':pid'=>getmypid(),
                ':ip'=>$server_ip
            ]);
        } catch (Throwable $e) {
            $pdo = null; // 에러 발생 시 다음 루프에서 재연결 시도
        }
    }

    // 메인 작업 수행
    if ($pdo) {
        run_upbit_user_asset_once($pdo);
    }

    // [수정] 장기 실행 메모리 정리
    gc_collect_cycles();

    // 10초 루프
    usleep(10000000);
}
Table Status
Table Namedaemon_upbit_Ticker_user
EngineInnoDB
Rows10
Data Length16,384 bytes
Index Length32,768 bytes
Comment
Created2026-04-11 09:25:33
Updated2026-04-19 23:37:18
Table Column Definition
# Column Name Type Null Key Default Extra Comment
1idbigint(20) unsignedNOPRINULLauto_increment고유 식별자(PK)
2currencyvarchar(20)NOUNINULL화폐 단위 (예: BTC, ETH)
3balancedecimal(36,18)NO-0.000000000000000000보유 수량
4lockeddecimal(36,18)NO-0.000000000000000000거래 대기 중인 수량
5avg_buy_pricedecimal(36,18)NO-0.000000000000000000매수 평균가
6avg_buy_price_modifieddecimal(36,18)NO-0.000000000000000000수정된 매수 평균가
7unit_currencyvarchar(10)NO-NULL기준 화폐 (KRW)
8collected_atdatetimeNOMULcurrent_timestamp()업비트 데이터 수집 시각
9collected_msbigint(20)NO-0수집 시각(밀리초)
10current_pricedecimal(36,18)YES-0.000000000000000000현재 시장가
11evaluation_amountdecimal(36,18)YES-0.000000000000000000평가 금액 (보유수량 * 현재가)
12buy_amountdecimal(36,18)YES-0.000000000000000000총 매수 금액 (보유수량 * 평단가)
13profit_amountdecimal(36,18)YES-0.000000000000000000평가 손익 (평가금액 - 매수금액)
14profit_ratedecimal(20,10)YES-0.0000000000수익률 (음수면 손실)
15total_asset_valuedecimal(36,18)YES-0.000000000000000000계좌 총 자산 가치
16total_profit_amountdecimal(36,18)YES-0.000000000000000000계좌 총 수익 금액
17total_profit_ratedecimal(20,10)YES-0.0000000000계좌 총 수익률
18cash_balancedecimal(36,18)YES-0.000000000000000000보유 현금 잔고 (KRW)