DATA/UPBIT/container/daemon_coin_container.php
#!/usr/bin/php
<?php
/**
 * 파일명: daemon_coin_container.php
 * 경로: /home/www/DATA/UPBIT/container/daemon_coin_container.php
 * 수정사항: 수집 방향을 현재->과거 에서 과거->현재(정순)로 변경
 */

// 1. 환경 설정
date_default_timezone_set('Asia/Seoul');
set_time_limit(0);
if (php_sapi_name() !== 'cli') die("CLI 환경에서만 실행 가능합니다.\n");

// --- [수집 설정] ---
$MARKET    = 'KRW-XRP';
$START_KST = "2017-09-01 00:00:00"; // 수집 시작 시점 (과거)
$END_KST   = "2026-01-19 12:10:00"; // 수집 종료 시점 (현재/미래)

// 종료 지점 계산 (UTC 기준 타임스탬프)
$end_dt_obj = new DateTime($END_KST, new DateTimeZone('Asia/Seoul'));
$end_dt_obj->setTimezone(new DateTimeZone('UTC'));
$END_TS_UTC = $end_dt_obj->getTimestamp();

// --- [루프/백오프 설정] ---
$SLEEP_USEC_NORMAL = 500000;    // 0.5초
$SLEEP_USEC_EMPTY  = 1000000;   // API 빈데이터 시 딜레이 (1s)
$SLEEP_SEC_DBFAIL  = 10;
$SLEEP_SEC_APIFAIL = 10;
$SLEEP_SEC_RETRY   = 2;
$SLEEP_SEC_FATAL   = 300;

// --- [함수: DB 연결] ---
function get_db_connection() {
    try {
        $db_upbit = null; $pdo = null;
        $db_file = '/home/www/DB/db_upbit.php';
        if (file_exists($db_file)) {
            include $db_file;
        }
        $conn = $db_upbit ?? $pdo ?? null;
        if ($conn instanceof PDO) {
            $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            return $conn;
        }
    } catch(Throwable $e) {}
    return null;
}

// --- [함수: API 호출] ---
function fetch_upbit_candles($market, $to_iso, $count = 200) {
    $url = sprintf(
        "https://api.upbit.com/v1/candles/minutes/1?market=%s&count=%d&to=%s",
        $market, $count, urlencode($to_iso)
    );

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Upbit Daemon)');

    $res  = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code === 429) return 'RETRY';
    if ($code !== 200 || !$res) return null;

    $json = json_decode($res, true);
    if (!is_array($json)) return null;

    return $json;
}

// --- [데몬화 시작] ---
$pid = pcntl_fork();
if ($pid == -1) die("Fork Error\n");
if ($pid) exit(0);
if (posix_setsid() == -1) die("Session Error\n");
$pid = pcntl_fork();
if ($pid == -1) die("Fork2 Error\n");
if ($pid) exit(0);

umask(0); chdir('/');
fclose(STDIN); fclose(STDOUT); fclose(STDERR);
$stdin  = fopen('/dev/null', 'r');
$stdout = fopen('/dev/null', 'wb');
$stderr = fopen('/dev/null', 'wb');

$stop = false;
pcntl_async_signals(true);
pcntl_signal(SIGTERM, function() { global $stop; $stop = true; });
pcntl_signal(SIGINT,  function() { global $stop; $stop = true; });

// --- [메인 루프 변수] ---
$pdo_conn = null;
$current_pointer_utc = null;
$empty_streak = 0;
$is_finished = false;

while (!$stop) {

    // 1. DB 연결 보장
    if (!($pdo_conn instanceof PDO)) {
        $pdo_conn = get_db_connection();
        if (!$pdo_conn) { sleep($SLEEP_SEC_DBFAIL); continue; }
        $current_pointer_utc = null;
        $empty_streak = 0;
    }

    if ($is_finished) { sleep(60); continue; }

    try {
        // 2. 수집 시점 결정 (포인터가 없을 때만 DB에서 읽음)
        if ($current_pointer_utc === null) {
            // DB에서 가장 최근(MAX) 시점 확인
            $stmt = $pdo_conn->prepare("SELECT MAX(candle_date_time_utc) FROM daemon_coin_container WHERE market = ?");
            $stmt->execute([$MARKET]);
            $max_utc = $stmt->fetchColumn();

            if ($max_utc) {
                $dt = new DateTime($max_utc, new DateTimeZone('UTC'));
                $current_pointer_utc = $dt->format('Y-m-d\TH:i:s\Z');
            } else {
                // DB 비어있으면 START_KST 부터 시작
                $dt = new DateTime($START_KST, new DateTimeZone('Asia/Seoul'));
                $dt->setTimezone(new DateTimeZone('UTC'));
                $current_pointer_utc = $dt->format('Y-m-d\TH:i:s\Z');
            }
        }

        $pointer_dt = new DateTime($current_pointer_utc, new DateTimeZone('UTC'));
        $pointer_ts = $pointer_dt->getTimestamp();

        // 종료 지점 도달 시 루프 정지
        if ($pointer_ts >= $END_TS_UTC) {
            $is_finished = true;
            continue;
        }

        // 수집 대상 시점 설정 (미래 방향으로 200분 전진)
        $api_dt = clone $pointer_dt;
        $api_dt->modify('+200 minutes');
        if ($api_dt->getTimestamp() > $END_TS_UTC) {
            $api_dt->setTimestamp($END_TS_UTC);
        }
        $target_to = $api_dt->format('Y-m-d\TH:i:s\Z');

        // 3. API 호출
        $candles = fetch_upbit_candles($MARKET, $target_to, 200);

        if ($candles === 'RETRY') { sleep($SLEEP_SEC_RETRY); continue; }
        if ($candles === null)    { sleep($SLEEP_SEC_APIFAIL); continue; }

        // 빈 데이터 시 재시도 로직
        if (empty($candles)) {
            $empty_streak++;
            if ($empty_streak < 5) {
                usleep($SLEEP_USEC_EMPTY);
                continue;
            } else {
                // 5회 연속 실패 시: 구멍으로 인정하고 1일 강제 전진
                $empty_streak = 0;
                $pointer_dt->modify('+1 day');  // ← 1분 → 1일로 변경
                $current_pointer_utc = $pointer_dt->format('Y-m-d\TH:i:s\Z');
                continue;
            }
        }

        $empty_streak = 0;

        // 4. DB 저장
        $pdo_conn->beginTransaction();

        $sql = "INSERT INTO daemon_coin_container (
                    market, candle_date_time_utc, candle_date_time_kst,
                    opening_price, high_price, low_price, trade_price,
                    timestamp, candle_acc_trade_price, candle_acc_trade_volume,
                    unit, prev_closing_price, change_price, change_rate
                ) VALUES (
                    :market, :utc, :kst, :open, :high, :low, :close,
                    :ts, :acc_price, :acc_vol, :unit, :prev_close, :chg_price, :chg_rate
                )
                ON DUPLICATE KEY UPDATE
                    opening_price=VALUES(opening_price),
                    high_price=VALUES(high_price),
                    low_price=VALUES(low_price),
                    trade_price=VALUES(trade_price),
                    timestamp=VALUES(timestamp),
                    candle_acc_trade_price=VALUES(candle_acc_trade_price),
                    candle_acc_trade_volume=VALUES(candle_acc_trade_volume),
                    unit=VALUES(unit),
                    prev_closing_price=VALUES(prev_closing_price),
                    change_price=VALUES(change_price),
                    change_rate=VALUES(change_rate)";

        $stmt_ins = $pdo_conn->prepare($sql);

        // 정순 처리를 위해 오름차순 정렬
        usort($candles, function($a, $b) {
            return strcmp($a['candle_date_time_utc'], $b['candle_date_time_utc']);
        });

        $latest_utc = null;
        foreach ($candles as $c) {
            if (!is_array($c) || empty($c['candle_date_time_kst']) || empty($c['candle_date_time_utc'])) {
                continue;
            }

            $utc_val = str_replace('T', ' ', $c['candle_date_time_utc']);
            $kst_val = str_replace('T', ' ', $c['candle_date_time_kst']);
            $c_ts = strtotime($utc_val . ' UTC');

            if ($c_ts <= $pointer_ts) continue; // 이미 수집된 시점 스킵
            if ($c_ts > $END_TS_UTC) break;

            $stmt_ins->execute([
                ':market'     => $c['market'],
                ':utc'        => $utc_val,
                ':kst'        => $kst_val,
                ':open'       => (double)$c['opening_price'],
                ':high'       => (double)$c['high_price'],
                ':low'        => (double)$c['low_price'],
                ':close'      => (double)$c['trade_price'],
                ':ts'         => (int)$c['timestamp'],
                ':acc_price'  => (double)$c['candle_acc_trade_price'],
                ':acc_vol'    => (double)$c['candle_acc_trade_volume'],
                ':unit'       => (int)($c['unit'] ?? 1),
                ':prev_close' => (double)($c['prev_closing_price'] ?? 0),
                ':chg_price'  => (double)($c['change_price'] ?? 0),
                ':chg_rate'   => (double)($c['change_rate'] ?? 0)
            ]);

            $latest_utc = $c['candle_date_time_utc'];
        }

        $pdo_conn->commit();

        if ($latest_utc) {
            $current_pointer_utc = (strpos($latest_utc, 'Z') === false) ? ($latest_utc . 'Z') : $latest_utc;
        } else {
            // 중복 데이터만 와서 포인터가 안 변할 경우 강제 전진
            $pointer_dt->modify('+1 minute');
            $current_pointer_utc = $pointer_dt->format('Y-m-d\TH:i:s\Z');
        }

        usleep($SLEEP_USEC_NORMAL);

    } catch (Throwable $e) {
        if ($pdo_conn instanceof PDO && $pdo_conn->inTransaction()) $pdo_conn->rollBack();
        $pdo_conn = null;
        sleep($SLEEP_SEC_FATAL);
    }
}

exit(0);