DATA/UPBIT/container/daemon_coin_container_4h.php
#!/usr/bin/php
<?php
/**
 * 파일명: daemon_coin_container_240.php
 * 기능: 업비트 4시간봉(240분) 데이터 정순 수집 (과거 -> 현재)
 * 수정사항: 주봉 로직으로 되어 있던 부분을 4시간봉 전용 로직으로 전면 수정
 */

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

// --- [수집 설정] ---
$MARKET    = 'KRW-XRP';

// 파일명에서 확장자(.php)를 제외한 이름을 테이블명으로 사용
$TABLE_NAME = basename(__FILE__, '.php');

// [설정] 수집 범위 (시작점 KST -> 종료점 KST)
// 4시간봉 기준 정렬을 위해 상장일 부근인 09:00:00 시작
$START_KST = "2017-10-24 09: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) {
    $to_param = str_replace(['T', 'Z'], [' ', ''], $to_iso);
    // [수정] 4시간봉(240분) 수집 URL로 변경
    $url = sprintf(
        "https://api.upbit.com/v1/candles/minutes/240?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) {
            $stmt = $pdo_conn->prepare("SELECT MAX(candle_date_time_utc) FROM {$TABLE_NAME} WHERE market = ?");
            $stmt->execute([$MARKET]);
            $max_utc = $stmt->fetchColumn();

            if ($max_utc) {
                $dt = new DateTime($max_utc, new DateTimeZone('UTC'));
                // 4시간봉 간격 정렬을 위해 분/초만 초기화
                $dt->setTime($dt->format('H'), 0, 0); 
                $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;
        }

        // 수집 대상 시점 설정 (200개 캔들 분량 = 800시간 미래 방향 설정)
        $api_dt = clone $pointer_dt;
        $api_dt->modify('+800 hours'); 
        $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회 연속 실패 시 다음 4시간으로 전진)
        if (empty($candles)) {
            $empty_streak++;
            if ($empty_streak < 5) {
                usleep($SLEEP_USEC_EMPTY);
                continue;
            } else {
                $empty_streak = 0;
                // [수정] 주봉(monday)이 아닌 4시간 전진 로직 적용
                $pointer_dt->modify('+4 hours');
                $current_pointer_utc = $pointer_dt->format('Y-m-d\TH:i:s\Z');
                continue;
            }
        }

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

        $sql = "INSERT INTO {$TABLE_NAME} (
                    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'] ?? 240),
                ':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 {
            // 중복 데이터 시 4시간 전진
            $pointer_dt->modify('+4 hours');
            $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);