SYSTEM_CORE_VIEWER
데몬
Target Object / Identifier
daemon_upbit_Ticker
업비트 마켓 플랫폼
업비트 API 마켓 베스트 데이터 수집 데몬
Exchange
업비트
Type
데몬
Form
API
DB TABLE
daemon_upbit_Ticker
TABLE DAEMON
daemon_upbit_Best.php
DB EVENT
[STATUS] INACTIVE
[ID] #19
[TABLE] daemon_upbit_Ticker
[ENGINE] InnoDB
[ROWS] 0
Detailed Description
* 플랫 폼 베스트 데몬
1. 업비트 : API 마켓 베스트 코인 데이터 수집 -> 업비트 : 플랫폼 테이블 저장
2. 플랫폼 테이블 : 덮어쓰기 구조 테이블
3. 중복 데이터 쓰기
가. 중요 베스트 코인 빠른 데이터 수집
1. 업비트 : API 마켓 베스트 코인 데이터 수집 -> 업비트 : 플랫폼 테이블 저장
2. 플랫폼 테이블 : 덮어쓰기 구조 테이블
3. 중복 데이터 쓰기
가. 중요 베스트 코인 빠른 데이터 수집
DB TABLE
Table Status
| Table Name | daemon_upbit_Ticker |
| Engine | InnoDB |
| Rows | 0 |
| Data Length | 49,152 bytes |
| Index Length | 32,768 bytes |
| Comment | 플랫폼_업비트 Ticker 덮어쓰기 완전체 테이블_API : 업비트 |
| Created | 2026-04-11 09:25:33 |
| Updated | 2026-04-21 04:37:28 |
| # | Column Name | Type | Null | Key | Default | Extra | Comment |
|---|---|---|---|---|---|---|---|
| 1 | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | 자동 증가 기본키 |
| 2 | market | varchar(20) | NO | UNI | NULL | 업비트 market (예: KRW-BTC) | |
| 3 | trade_date | varchar(10) | NO | - | NULL | 체결 날짜(세계) | |
| 4 | trade_time | varchar(10) | NO | - | NULL | 체결 시각(세계) | |
| 5 | trade_date_kst | varchar(10) | NO | - | NULL | 체결 날짜(한국) | |
| 6 | trade_time_kst | varchar(10) | NO | - | NULL | 체결 시각(한국) | |
| 7 | opening_price | double | NO | - | 0 | 시가 | |
| 8 | high_price | double | NO | - | 0 | 고가 | |
| 9 | low_price | double | NO | - | 0 | 저가 | |
| 10 | trade_price | double | NO | - | 0 | 현재가 | |
| 11 | prev_closing_price | double | NO | - | 0 | 전일 종가 | |
| 12 | change | varchar(10) | NO | - | NULL | 전일 대비 방향 | |
| 13 | change_price | double | NO | - | 0 | 가격 변화 | |
| 14 | change_rate | double | NO | - | 0 | 변화율 | |
| 15 | signed_change_price | double | NO | - | 0 | 부호 포함 가격 변화 | |
| 16 | signed_change_rate | double | NO | - | 0 | 부호 포함 변화율 | |
| 17 | trade_volume | double | NO | - | 0 | 최근 거래량 | |
| 18 | acc_trade_volume | double | NO | - | 0 | 누적 거래량 | |
| 19 | acc_trade_volume_24h | double | NO | - | 0 | 24시간 누적 거래량 | |
| 20 | acc_trade_price | double | NO | - | 0 | 누적 거래대금 | |
| 21 | acc_trade_price_24h | double | NO | - | 0 | 24시간 누적 거래대금 | |
| 22 | highest_52_week_price | double | NO | - | 0 | 52주 최고가 | |
| 23 | highest_52_week_date | varchar(10) | NO | - | NULL | 52주 최고가 날짜 | |
| 24 | lowest_52_week_price | double | NO | - | 0 | 52주 최저가 | |
| 25 | lowest_52_week_date | varchar(10) | NO | - | NULL | 52주 최저가 날짜 | |
| 26 | collected_at | datetime | NO | - | current_timestamp() | 서버 수집 시각 | |
| 27 | collected_ms | bigint(20) unsigned | NO | - | 0 | 서버 수집 시각(ms) | |
| 28 | ob_timestamp | bigint(20) unsigned | NO | - | 0 | 업비트 orderbook timestamp(ms) | |
| 29 | ob_total_ask_size | double | NO | - | 0 | 전체 매도 잔량 | |
| 30 | ob_total_bid_size | double | NO | - | 0 | 전체 매수 잔량 | |
| 31 | ob_units | longtext | YES | - | NULL | 호가 묶음(JSON: ask_price, bid_price, ask_size, bid_size) | |
| 32 | ob_collected_at | datetime | NO | - | current_timestamp() | orderbook 수집 시각 | |
| 33 | ob_collected_ms | bigint(20) unsigned | NO | - | 0 | orderbook 수집 ms | |
| 34 | tr_trade_timestamp | bigint(20) unsigned | NO | - | 0 | 체결 시간 timestamp(ms) | |
| 35 | tr_trade_price | double | NO | - | 0 | 체결 가격 | |
| 36 | tr_trade_volume | double | NO | - | 0 | 체결 수량 | |
| 37 | tr_ask_bid | varchar(10) | NO | - | '' | 매수/매도 (ASK/BID) | |
| 38 | tr_trade_date_utc | varchar(10) | NO | - | '' | UTC 체결일 | |
| 39 | tr_trade_time_utc | varchar(10) | NO | - | '' | UTC 체결시간 | |
| 40 | tr_trade_date_kst | varchar(10) | NO | - | '' | KST 체결일 | |
| 41 | tr_trade_time_kst | varchar(10) | NO | - | '' | KST 체결시간 | |
| 42 | tr_collected_at | datetime | NO | - | current_timestamp() | trade 수집 시각 | |
| 43 | tr_collected_ms | bigint(20) unsigned | NO | - | 0 | trade 수집 ms | |
| 44 | day_of_week | tinyint(4) | YES | - | NULL | 요일 (0:일, 1:월, ..., 6:토) | |
| 45 | korean_name | varchar(50) | NO | - | '' | 마켓 한글명 (예: 비트코인) |
DAEMON
Table Column Definition
Daemon Source Code (daemon_upbit_Best.php)
<?php
/**
* ============================================================
* 업비트 7종 시세 수집 데몬 (순수 CLI 전용)
* - [수정1] ON DUPLICATE KEY UPDATE: 전체 컬럼 최신값 갱신
* - [수정2] 체결 API 데이터 반영 (tr_* 필드)
* - [수정3] DB 재연결 불가 결함 수정 (@include를 통한 객체 갱신)
* - [수정4] API 호출 폭주 방지 (호가 배치 조회 + 루프 주기 조정)
* - [수정5] Null 참조 경고 방지 로직 추가
* - [수정6] 매집 코인 종목: 그누보드(g5_write_daemon_best_upbit) 연동
* ============================================================
*/
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;
}
// 반드시 가장 위에
$DAEMON_ID = pathinfo(__FILE__, PATHINFO_FILENAME);
// ------------------------------------------------------------
// 2. DB 연결 함수 (재연결 지원 수정)
// ------------------------------------------------------------
function get_db_connection() {
$db_upbit = null;
try {
// [수정] require 대신 include를 사용하여 호출 시마다 PDO 객체 갱신 가능하게 함
@include '/home/www/DB/db_upbit.php';
if (isset($db_upbit) && $db_upbit instanceof PDO) {
$db_upbit->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $db_upbit;
}
} catch (Throwable $e) {
return null;
}
return null;
}
// ------------------------------------------------------------
// 4. API 설정
// ------------------------------------------------------------
$api = require '/home/www/DB/upbit_api_url.php';
$API_TICKER = $api['ticker'] . '?markets=';
$API_TRADES = 'https://api.upbit.com/v1/trades/ticks?market=';
$API_ORDERBOOK = 'https://api.upbit.com/v1/orderbook?markets=';
function http_get_json($url) {
$ctx = stream_context_create([
'http' => [
'timeout' => 5,
'header' => "User-Agent: upbit-cli-daemon\r\n",
]
]);
$raw = @file_get_contents($url, false, $ctx);
return $raw ? json_decode($raw, true) : null;
}
// ------------------------------------------------------------
// 5. 기본 7종 + 그누보드 기반 매집 코인 종목 확장
// ------------------------------------------------------------
$krw = [
'KRW-BTC',
'KRW-ETH',
'KRW-XRP',
'KRW-ADA',
'KRW-SOL',
'KRW-DOGE',
'KRW-BSV'
];
// 그누보드: g5_write_daemon_best_upbit
// - 코인 종목 컬럼 : wr_subject (예: 'KRW-XXX')
// - 실행/미실행 컬럼 : x2_run (실행이면 1)
// - 테이블의 모든 행 중 x2_run = 1 인 wr_subject 값을 $krw 에 추가
try {
@include '/home/www/DB/db_gnu.php';
// db_gnu.php 내부 PDO 변수 이름이 어떤 것인지 안전하게 처리
$pdo_gnu = null;
if (isset($db_gnu) && $db_gnu instanceof PDO) {
$pdo_gnu = $db_gnu;
} elseif (isset($pdo_gnu) && $pdo_gnu instanceof PDO) {
// 이미 정의된 경우 그대로 사용
$pdo_gnu = $pdo_gnu;
}
if ($pdo_gnu instanceof PDO) {
$stmt_kind = $pdo_gnu->prepare("
SELECT wr_subject
FROM g5_write_daemon_best_upbit
WHERE x2_run = 1
");
$stmt_kind->execute();
$rows = $stmt_kind->fetchAll(PDO::FETCH_COLUMN); // wr_subject 만
if (is_array($rows)) {
foreach ($rows as $m) {
$m = trim((string)$m);
if ($m === '') continue;
// 이미 포함된 종목은 스킵
if (!in_array($m, $krw, true)) {
$krw[] = $m;
}
}
}
}
} catch (Throwable $e) {
// 시세 수집 자체는 계속 진행해야 하므로, 여기서는 조용히 무시
// 필요하면 파일 로그 등으로만 기록
}
// ------------------------------------------------------------
// 3. daemon_record / INSERT statement 준비
// ------------------------------------------------------------
$pdo = get_db_connection();
$server_ip = trim(shell_exec("hostname -I | awk '{print $1}'")) ?: 'CLI';
$stmt_hb = null;
$stmt_stop = null;
$stmt_ins = null;
function prepare_statements(PDO $pdo, &$stmt_hb, &$stmt_stop, &$stmt_ins) {
$stmt_hb = $pdo->prepare("
INSERT INTO daemon_record (d_id, d_category, d_pid, d_status, d_heartbeat, d_ip, d_start_time, d_memo)
VALUES (:id, 'UPBIT', :pid, 'RUNNING', NOW(), :ip, NOW(), :memo)
ON DUPLICATE KEY UPDATE
d_pid = VALUES(d_pid),
d_status = 'RUNNING',
d_heartbeat = NOW(),
d_ip = VALUES(d_ip),
d_memo = VALUES(d_memo)
");
$stmt_stop = $pdo->prepare("UPDATE daemon_record SET d_status='STOPPED', d_heartbeat=NOW() WHERE d_id=:id");
$stmt_ins = $pdo->prepare("
INSERT INTO daemon_upbit_Ticker (
market, trade_date, trade_time, trade_date_kst, trade_time_kst,
opening_price, high_price, low_price, trade_price, prev_closing_price,
`change`, change_price, change_rate, signed_change_price, signed_change_rate,
trade_volume, acc_trade_volume, acc_trade_volume_24h, acc_trade_price, acc_trade_price_24h,
highest_52_week_price, highest_52_week_date, lowest_52_week_price, lowest_52_week_date,
collected_at, collected_ms, tr_trade_timestamp, tr_trade_price, tr_trade_volume, tr_ask_bid,
tr_trade_date_utc, tr_trade_time_utc, tr_trade_date_kst, tr_trade_time_kst,
tr_collected_at, tr_collected_ms, ob_timestamp, ob_total_ask_size, ob_total_bid_size, ob_units
)
VALUES (
:market, :trade_date, :trade_time, :trade_date_kst, :trade_time_kst,
:opening_price, :high_price, :low_price, :trade_price, :prev_closing_price,
:change, :change_price, :change_rate, :signed_change_price, :signed_change_rate,
:trade_volume, :acc_trade_volume, :acc_trade_volume_24h, :acc_trade_price, :acc_trade_price_24h,
:highest_52_week_price, :highest_52_week_date, :lowest_52_week_price, :lowest_52_week_date,
:collected_at, :collected_ms, :tr_trade_timestamp, :tr_trade_price, :tr_trade_volume, :tr_ask_bid,
:tr_trade_date_utc, :tr_trade_time_utc, :tr_trade_date_kst, :tr_trade_time_kst,
:tr_collected_at, :tr_collected_ms, :ob_timestamp, :ob_total_ask_size, :ob_total_bid_size, :ob_units
)
ON DUPLICATE KEY UPDATE
trade_date = VALUES(trade_date),
trade_time = VALUES(trade_time),
trade_date_kst = VALUES(trade_date_kst),
trade_time_kst = VALUES(trade_time_kst),
opening_price = VALUES(opening_price),
high_price = VALUES(high_price),
low_price = VALUES(low_price),
trade_price = VALUES(trade_price),
prev_closing_price = VALUES(prev_closing_price),
`change` = VALUES(`change`),
change_price = VALUES(change_price),
change_rate = VALUES(change_rate),
signed_change_price = VALUES(signed_change_price),
signed_change_rate = VALUES(signed_change_rate),
trade_volume = VALUES(trade_volume),
acc_trade_volume = VALUES(acc_trade_volume),
acc_trade_volume_24h = VALUES(acc_trade_volume_24h),
acc_trade_price = VALUES(acc_trade_price),
acc_trade_price_24h = VALUES(acc_trade_price_24h),
highest_52_week_price = VALUES(highest_52_week_price),
highest_52_week_date = VALUES(highest_52_week_date),
lowest_52_week_price = VALUES(lowest_52_week_price),
lowest_52_week_date = VALUES(lowest_52_week_date),
collected_at = VALUES(collected_at),
collected_ms = VALUES(collected_ms),
tr_trade_timestamp = VALUES(tr_trade_timestamp),
tr_trade_price = VALUES(tr_trade_price),
tr_trade_volume = VALUES(tr_trade_volume),
tr_ask_bid = VALUES(tr_ask_bid),
tr_trade_date_utc = VALUES(tr_trade_date_utc),
tr_trade_time_utc = VALUES(tr_trade_time_utc),
tr_trade_date_kst = VALUES(tr_trade_date_kst),
tr_trade_time_kst = VALUES(tr_trade_time_kst),
tr_collected_at = VALUES(tr_collected_at),
tr_collected_ms = VALUES(tr_collected_ms),
ob_timestamp = VALUES(ob_timestamp),
ob_total_ask_size = VALUES(ob_total_ask_size),
ob_total_bid_size = VALUES(ob_total_bid_size),
ob_units = VALUES(ob_units)
");
}
if ($pdo) {
prepare_statements($pdo, $stmt_hb, $stmt_stop, $stmt_ins);
}
// ------------------------------------------------------------
// 7. 메인 루프
// ------------------------------------------------------------
echo "=============================================\n";
echo "[{$DAEMON_ID}] 7종 시세 CLI 데몬 시작\n";
echo "PID: " . getmypid() . "\n";
echo "=============================================\n";
$cycle = 0;
$market_list_str = implode(',', $krw);
while (true) {
$cycle++;
try {
// DB 재확보 + prepare 재생성
if (!$pdo) {
$pdo = get_db_connection();
if ($pdo) prepare_statements($pdo, $stmt_hb, $stmt_stop, $stmt_ins);
} else {
try {
$pdo->query("SELECT 1");
} catch (Throwable $e) {
$pdo = get_db_connection();
if ($pdo) prepare_statements($pdo, $stmt_hb, $stmt_stop, $stmt_ins);
}
}
if (!$pdo || !$stmt_hb || !$stmt_ins) {
sleep(2);
continue;
}
// 하트비트
$stmt_hb->execute([
':id' => $DAEMON_ID,
':pid' => getmypid(),
':ip' => $server_ip,
':memo' => "CLI LOOP"
]);
// -------------------------
// 시세 수집 (최적화)
// -------------------------
$at = date('Y-m-d H:i:s');
$ms = (int)(microtime(true) * 1000);
// 티커와 호가를 배치로 한 번에 조회
$tks = http_get_json($API_TICKER . $market_list_str);
$obs = http_get_json($API_ORDERBOOK . $market_list_str);
// 호가 데이터를 마켓별로 맵핑
$ob_map = [];
if (is_array($obs)) {
foreach ($obs as $o) {
if (!isset($o['market'])) continue;
$ob_map[$o['market']] = $o;
}
}
if (is_array($tks)) {
foreach ($tks as $t) {
$market = $t['market'] ?? '';
if ($market === '') continue;
// Null 참조 방지: 호가 데이터가 없을 경우 빈 배열
$ob = $ob_map[$market] ?? [];
// 체결 데이터 (단건 조회)
$tr0 = null;
$tr = http_get_json($API_TRADES . $market . '&count=1');
if ($tr && isset($tr[0])) $tr0 = $tr[0];
$tr_ts = $tr0['timestamp'] ?? 0;
$tr_pr = $tr0['trade_price'] ?? 0;
$tr_vol = $tr0['trade_volume'] ?? 0;
$ask_bid = $tr0['ask_bid'] ?? '';
$tr_du = $tr0['trade_date_utc'] ?? '';
$tr_tu = $tr0['trade_time_utc'] ?? '';
$tr_dk = $tr0['trade_date_kst'] ?? '';
$tr_tk = $tr0['trade_time_kst'] ?? '';
// KST 보정 (API 값이 없을 경우에만 계산)
if (!$tr_dk && $tr_ts) {
try {
$dt = new DateTime('@' . (int)floor(((int)$tr_ts) / 1000));
$dt->setTimezone(new DateTimeZone('Asia/Seoul'));
$tr_dk = $dt->format('Y-m-d');
$tr_tk = $dt->format('H:i:s');
} catch (Throwable $e) {}
}
$stmt_ins->execute([
':market' => $market,
':trade_date' => $t['trade_date'] ?? '',
':trade_time' => $t['trade_time'] ?? '',
':trade_date_kst' => $t['trade_date_kst'] ?? '',
':trade_time_kst' => $t['trade_time_kst'] ?? '',
':opening_price' => $t['opening_price'] ?? 0,
':high_price' => $t['high_price'] ?? 0,
':low_price' => $t['low_price'] ?? 0,
':trade_price' => $t['trade_price'] ?? 0,
':prev_closing_price' => $t['prev_closing_price'] ?? 0,
':change' => $t['change'] ?? '',
':change_price' => $t['change_price'] ?? 0,
':change_rate' => $t['change_rate'] ?? 0,
':signed_change_price' => $t['signed_change_price'] ?? 0,
':signed_change_rate' => $t['signed_change_rate'] ?? 0,
':trade_volume' => $t['trade_volume'] ?? 0,
':acc_trade_volume' => $t['acc_trade_volume'] ?? 0,
':acc_trade_volume_24h' => $t['acc_trade_volume_24h'] ?? 0,
':acc_trade_price' => $t['acc_trade_price'] ?? 0,
':acc_trade_price_24h' => $t['acc_trade_price_24h'] ?? 0,
':highest_52_week_price' => $t['highest_52_week_price'] ?? 0,
':highest_52_week_date' => $t['highest_52_week_date'] ?? '',
':lowest_52_week_price' => $t['lowest_52_week_price'] ?? 0,
':lowest_52_week_date' => $t['lowest_52_week_date'] ?? '',
':collected_at' => $at,
':collected_ms' => $ms,
':tr_trade_timestamp' => $tr_ts ?: 0,
':tr_trade_price' => $tr_pr ?: 0,
':tr_trade_volume' => $tr_vol ?: 0,
':tr_ask_bid' => $ask_bid ?: '',
':tr_trade_date_utc' => $tr_du ?: '',
':tr_trade_time_utc' => $tr_tu ?: '',
':tr_trade_date_kst' => $tr_dk ?: '',
':tr_trade_time_kst' => $tr_tk ?: '',
':tr_collected_at' => $at,
':tr_collected_ms' => $ms,
':ob_timestamp' => $ob['timestamp'] ?? 0,
':ob_total_ask_size' => $ob['total_ask_size'] ?? 0,
':ob_total_bid_size' => $ob['total_bid_size'] ?? 0,
':ob_units' => isset($ob['orderbook_units'])
? json_encode($ob['orderbook_units'], JSON_UNESCAPED_UNICODE)
: null,
]);
}
}
if ($cycle % 10 === 0) {
echo ".";
flush();
}
if ($cycle % 50 === 0 && function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
} catch (Throwable $e) {
$log = date('[Y-m-d H:i:s] ') . $e->getMessage();
echo "\nERROR: $log\n";
sleep(2);
}
// 루프 속도: 초당 호출 수를 제한하기 위해 0.3초
usleep(300000);
}