auto: 일일 백업 2026-08-28 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,12 +35,18 @@
|
||||
바뀌었다. 거래소가 지워주는 스톱주문과 달리 arm 은 우리 파일에만 있으니 우리가 지운다:
|
||||
`is_arm_expired` 판정 + 감시 루프의 다음 거래일 첫 사이클 정리.
|
||||
|
||||
여기에 세 번째 키 `recent` 가 더 있다 — 장 마감 소멸·만료된 예약의 설정을 짧게 보관해
|
||||
자산웹이 "같은 설정으로 다시 등록" 버튼을 띄울 수 있게 하는 **UI 편의용 링버퍼**다.
|
||||
감사 기록이 아니다(그쪽은 state/order_log.jsonl, 무한 append).
|
||||
|
||||
⚠️ **두 갈래를 한 파일에 둔 이유는 발동이 "arm 제거 + 예약 등록"이라는 하나의 원자적
|
||||
전이여야 해서다.** promote_arm 이 락 하나 안에서 os.replace 1회로 끝낸다. 파일을 쪼개면
|
||||
락을 중첩해야 하고(데드락), 중간에 죽으면 둘 다 존재하거나 둘 다 사라진다.
|
||||
|
||||
⚠️ 그 대가로 **_write 는 None 인 쪽을 디스크에서 다시 읽어 보존**한다. 이 계약이 깨지면
|
||||
reservations 쪽 쓰기 6곳이 arms 를 조용히 전멸시킨다 (테스트로 고정해 둠).
|
||||
한쪽 쓰기가 나머지 두 갈래를 조용히 전멸시킨다 — reservations 쪽 쓰기만 6곳이다.
|
||||
`_read_doc` 이 세 키를 하드 정규화하므로 **키를 늘릴 때 _read_doc 과 _write 를 반드시
|
||||
같이** 고쳐야 한다 (테스트로 고정해 둠).
|
||||
|
||||
⚠️ arm 의 steps 에는 **가격·주수를 저장하지 않는다**(정의 n/pct/cum/weight 만). routing_suffix
|
||||
도 저장하지 않는다. 갭 상승으로 발동가를 훌쩍 넘겨 열릴 수 있고, 무기한 대기라 등록 시점
|
||||
@@ -253,16 +259,18 @@ def _read_doc() -> dict:
|
||||
때문이다. 파일이 둘이면 락을 중첩해야 하고, 중간에 죽으면 둘 다 존재하거나 둘 다
|
||||
사라진다.
|
||||
"""
|
||||
empty = {'reservations': [], 'arms': [], 'recent': []}
|
||||
if not STATE_FILE.exists():
|
||||
return {'reservations': [], 'arms': []}
|
||||
return empty
|
||||
try:
|
||||
data = json.loads(STATE_FILE.read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
return {'reservations': [], 'arms': []}
|
||||
return empty
|
||||
if not isinstance(data, dict):
|
||||
return {'reservations': [], 'arms': []}
|
||||
return empty
|
||||
return {'reservations': data.get('reservations') or [],
|
||||
'arms': data.get('arms') or []}
|
||||
'arms': data.get('arms') or [],
|
||||
'recent': data.get('recent') or []}
|
||||
|
||||
|
||||
def _read() -> list:
|
||||
@@ -273,10 +281,15 @@ def _read_arms() -> list:
|
||||
return _read_doc()['arms']
|
||||
|
||||
|
||||
def _write(reservations: Optional[list] = None, arms: Optional[list] = None) -> None:
|
||||
def _read_recent() -> list:
|
||||
return _read_doc()['recent']
|
||||
|
||||
|
||||
def _write(reservations: Optional[list] = None, arms: Optional[list] = None,
|
||||
recent: Optional[list] = None) -> None:
|
||||
"""None 인 쪽은 디스크의 현재 값을 유지한다.
|
||||
|
||||
⚠️ 두 갈래가 한 파일에 있으므로 한쪽만 쓰는 호출이 다른 쪽을 지우면 안 된다.
|
||||
⚠️ 세 갈래가 한 파일에 있으므로 한쪽만 쓰는 호출이 다른 쪽을 지우면 안 된다.
|
||||
반드시 _FileLock 안에서 부를 것 — 여기서 다시 읽어 병합한다.
|
||||
"""
|
||||
doc = _read_doc()
|
||||
@@ -284,6 +297,8 @@ def _write(reservations: Optional[list] = None, arms: Optional[list] = None) ->
|
||||
doc['reservations'] = reservations
|
||||
if arms is not None:
|
||||
doc['arms'] = arms
|
||||
if recent is not None:
|
||||
doc['recent'] = recent
|
||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = STATE_FILE.with_suffix(STATE_FILE.suffix + '.tmp')
|
||||
tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
@@ -334,6 +349,10 @@ def _build_reservation(account: str, symbol: str, symbol_name: str, total_qty: i
|
||||
'qty': s['qty'], 'ord_no': s['ord_no'],
|
||||
'cond_uv': s['cond_uv'], 'ord_uv': s['ord_uv'],
|
||||
'entry_cond_uv': s['cond_uv'], 'modify_count': 0} for s in steps],
|
||||
# 등록 시점 계단 **정의**(가격·주수 없음). steps 는 레그가 체결·소멸할 때마다 줄어들고
|
||||
# 마지막 레그가 빠지면 비어버려서, 재등록용 원본이 여기 없으면 계단을 복원할 수 없다.
|
||||
'entry_steps': [{'n': s['n'], 'pct': s['pct'], 'cum': s['cum'], 'weight': s['weight']}
|
||||
for s in steps],
|
||||
'routing_suffix': routing_suffix,
|
||||
'card_id': card_id,
|
||||
'created_at': _now_iso(),
|
||||
@@ -582,6 +601,73 @@ def mark_arm_firing(arm_id: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 최근 종료 (recent) — 재등록 버튼용 단기 캐시
|
||||
#
|
||||
# 트레일링 스톱은 장 마감에 거래소가 지우고, 대기 예약은 등록일 당일로 우리가 지운다. 둘 다
|
||||
# 사라지는 순간 계좌·수량·계단이 함께 없어져서, 다시 걸려면 처음부터 입력해야 했다.
|
||||
# 종료 시점의 설정을 짧게 보관해 자산웹이 "이전 설정 그대로 채워진 거래 모달" 을 열 수 있게 한다.
|
||||
#
|
||||
# ⚠️ 이건 **UI 편의용 단기 캐시이지 감사 기록이 아니다.** 감사 원장은 state/order_log.jsonl 이고
|
||||
# 그쪽은 무한 append 로 그대로 둔다. 여기는 링버퍼라 오래된 건 지워진다.
|
||||
# ⚠️ 재등록은 **사람이 버튼을 누르고 PIN 을 다시 거친다** — 감시 루프가 스스로 되살리는 경로는
|
||||
# 없다. "자동 재등록 안 함"(2026-07-30 결정)은 그대로 유효하다.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
RECENT_MAX = 20
|
||||
RECENT_DAYS = 7
|
||||
|
||||
|
||||
def list_recent() -> list:
|
||||
"""최근 종료 목록 (최신이 앞). 락 없이 읽기 — 웹 렌더용."""
|
||||
return _read_recent()
|
||||
|
||||
|
||||
def get_recent(key: str) -> Optional[dict]:
|
||||
for e in _read_recent():
|
||||
if e.get('key') == key:
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
def push_recent(entry: dict) -> dict:
|
||||
"""종료된 예약 1건을 최근 목록 맨 앞에 넣는다 (링버퍼).
|
||||
|
||||
⚠️ 제거(remove_step/remove_arm) 와 **같은 락에 묶지 않는다.** 레그 소멸은 레그마다 제거가
|
||||
끝난 **뒤에야** "예약이 죽었다" 를 알 수 있어 애초에 한 락에 못 넣기 때문이다. 그 사이에
|
||||
죽으면 항목 하나를 잃는데, 순서를 뒤집으면(기록 먼저) 같은 자리에서 중복이 남는다 —
|
||||
편의 기능이라 유실이 중복보다 낫다.
|
||||
"""
|
||||
entry = dict(entry)
|
||||
entry.setdefault('ended_at', _now_iso())
|
||||
entry['key'] = _new_id('RC-')
|
||||
cutoff = (datetime.now(KST) - timedelta(days=RECENT_DAYS)).isoformat(timespec='seconds')
|
||||
with _FileLock(_LOCK_FILE):
|
||||
recent = [e for e in _read_recent() if (e.get('ended_at') or '') >= cutoff]
|
||||
recent.insert(0, entry)
|
||||
_write(recent=recent[:RECENT_MAX])
|
||||
return entry
|
||||
|
||||
|
||||
def make_recent_entry(kind: str, reason: str, src: dict, steps: list) -> dict:
|
||||
"""예약/대기예약 스냅샷 → 재등록에 필요한 것만 뽑은 항목.
|
||||
|
||||
steps 는 정의만(n/pct/cum/weight) — 가격·주수는 재등록 시점에 다시 계산된다.
|
||||
"""
|
||||
return {
|
||||
'kind': kind, # 'trailing' | 'arm' → 재등록 order_type 을 가른다
|
||||
'reason': reason,
|
||||
'account': src['account'],
|
||||
'symbol': src['symbol'],
|
||||
'symbol_name': src.get('symbol_name') or src['symbol'],
|
||||
'qty': src.get('qty') or 0,
|
||||
'steps': [{'n': s['n'], 'pct': s['pct'], 'cum': s['cum'], 'weight': s.get('weight')}
|
||||
for s in steps],
|
||||
'min_sell_price': src.get('min_sell_price'),
|
||||
'trigger_price': src.get('trigger_price'), # arm 만. trailing 은 None
|
||||
}
|
||||
|
||||
|
||||
def is_arm_expired(arm: dict, now: Optional[datetime] = None) -> bool:
|
||||
"""등록일의 거래 세션이 끝났으면 만료 (2026-08-25 관리자님 결정으로 무기한 → 당일).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user