DATA/UPBIT/container/daemon_coin_container_30d.php
#!/usr/bin/php
<?php
/**
 * 파일명: daemon_coin_container_30d.php
 * 수정사항: 테이블명을 daemon_coin_container_30d로 변경 및 5회 재시도 후 건너뛰기 로직 적용
 */

// 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-12-01 00:00:00"; 
$END_KST   = "2026-01-01 00:00: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;      // 2초 (성공 시 대기)
$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) {
    $to_param = str_replace(['T', 'Z'], [' ', ''], $to_iso);
    $url = sprintf(
        "https://api.upbit.com/v1/candles/months?market=%s&count=%d&to=%s",
        $market, $count, urlencode($to_param)
    );

    $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) {

    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 {
        if ($current_pointer_utc === null) {
            // [수정] 테이블명을 daemon_coin_container_30d로 변경하여 조회
            $stmt = $pdo_conn->prepare("SELECT MAX(candle_date_time_utc) FROM daemon_coin_container_30d 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 {
                $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;
        }

        $api_dt = clone $pointer_dt;
        $api_dt->modify('+200 months'); 
        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');

        $candles = fetch_upbit_candles($MARKET, $target_to, 200);

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

        // [수정] 데이터 호출 실패 시 재시도 로직 (5회 연속 실패 시 다음 데이터 수집)
        if (empty($candles)) {
            $empty_streak++;
            if ($empty_streak < 5) {
                usleep($SLEEP_USEC_EMPTY);
                continue;
            } else {
                $empty_streak = 0;
                $pointer_dt->modify('+1 month'); // 실패 인정 후 한 달 전진
                $current_pointer_utc = $pointer_dt->format('Y-m-d\TH:i:s\Z');
                continue;
            }
        }

        $empty_streak = 0;
        $pdo_conn->beginTransaction();

        // [수정] 테이블명을 daemon_coin_container_30d로 변경하여 삽입
        $sql = "INSERT INTO daemon_coin_container_30d (
                    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) {
            $utc_val = str_replace('T', ' ', $c['candle_date_time_utc']);
            $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'       => str_replace('T', ' ', $c['candle_date_time_kst']),
                ':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 month');
            $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);