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 Name | daemon_upbit_Ticker_user |
| Engine | InnoDB |
| Rows | 10 |
| Data Length | 16,384 bytes |
| Index Length | 32,768 bytes |
| Comment | |
| Created | 2026-04-11 09:25:33 |
| Updated | 2026-04-20 00:57:27 |
Table Column Definition
| # | Column Name | Type | Null | Key | Default | Extra | Comment |
|---|---|---|---|---|---|---|---|
| 1 | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | 고유 식별자(PK) |
| 2 | currency | varchar(20) | NO | UNI | NULL | 화폐 단위 (예: BTC, ETH) | |
| 3 | balance | decimal(36,18) | NO | - | 0.000000000000000000 | 보유 수량 | |
| 4 | locked | decimal(36,18) | NO | - | 0.000000000000000000 | 거래 대기 중인 수량 | |
| 5 | avg_buy_price | decimal(36,18) | NO | - | 0.000000000000000000 | 매수 평균가 | |
| 6 | avg_buy_price_modified | decimal(36,18) | NO | - | 0.000000000000000000 | 수정된 매수 평균가 | |
| 7 | unit_currency | varchar(10) | NO | - | NULL | 기준 화폐 (KRW) | |
| 8 | collected_at | datetime | NO | MUL | current_timestamp() | 업비트 데이터 수집 시각 | |
| 9 | collected_ms | bigint(20) | NO | - | 0 | 수집 시각(밀리초) | |
| 10 | current_price | decimal(36,18) | YES | - | 0.000000000000000000 | 현재 시장가 | |
| 11 | evaluation_amount | decimal(36,18) | YES | - | 0.000000000000000000 | 평가 금액 (보유수량 * 현재가) | |
| 12 | buy_amount | decimal(36,18) | YES | - | 0.000000000000000000 | 총 매수 금액 (보유수량 * 평단가) | |
| 13 | profit_amount | decimal(36,18) | YES | - | 0.000000000000000000 | 평가 손익 (평가금액 - 매수금액) | |
| 14 | profit_rate | decimal(20,10) | YES | - | 0.0000000000 | 수익률 (음수면 손실) | |
| 15 | total_asset_value | decimal(36,18) | YES | - | 0.000000000000000000 | 계좌 총 자산 가치 | |
| 16 | total_profit_amount | decimal(36,18) | YES | - | 0.000000000000000000 | 계좌 총 수익 금액 | |
| 17 | total_profit_rate | decimal(20,10) | YES | - | 0.0000000000 | 계좌 총 수익률 | |
| 18 | cash_balance | decimal(36,18) | YES | - | 0.000000000000000000 | 보유 현금 잔고 (KRW) |