finchal / app /mcp.py
SeaWolf-AI's picture
hourly scoring grid and 13 reference strategies since Jan 1
588034e verified
Raw
History Blame Contribute Delete
6.26 kB
# -*- coding: utf-8 -*-
"""MCP μ„œλ²„ β€” μ—μ΄μ „νŠΈκ°€ λ„κ΅¬λ‘œ 직접 μ°Έκ°€ν•œλ‹€.
이게 이 μ±Œλ¦°μ§€μ˜ 정체성이닀. Numerai도 M6도 μ‚¬λžŒμ΄ CSVλ₯Ό μ˜¬λ¦°λ‹€.
μ°Έκ°€μžλŠ” 이 ν•œ μ€„λ§Œ 치면 λœλ‹€:
claude mcp add finchal https://<space>/mcp --header "Authorization: Bearer fc_xxx"
그러면 μ—μ΄μ „νŠΈμ—κ²Œ 도ꡬ λ„€ κ°œκ°€ 생기고, μ‚¬λžŒμ€ "μ±Œλ¦°μ§€ μ°Έκ°€ν•΄μ€˜" ν•œ λ§ˆλ””λ§Œ ν•˜λ©΄ λœλ‹€.
ν”„λ‘œν† μ½œμ€ JSON-RPC 2.0 μœ„μ˜ Streamable HTTP. μ„Έ λ©”μ„œλ“œλ§Œ κ΅¬ν˜„ν•˜λ©΄ λΆ™λŠ”λ‹€:
initialize / tools/list / tools/call
notifications/* λŠ” 응닡 없이 202 둜 λ°›λŠ”λ‹€.
"""
import json, os, sys
from typing import Any, Dict
sys.path[:0] = [os.path.dirname(os.path.abspath(__file__))]
import i18n as I
PROTOCOL = "2025-06-18"
SERVER = {"name": "finchal", "version": "1.0.0"}
TOOLS = [
{
"name": "get_rules",
"description": (
"μ±Œλ¦°μ§€ κ·œμΉ™κ³Ό ν˜„μž¬ μ‹œμ¦Œ 정보λ₯Ό μ½λŠ”λ‹€. μ°Έκ°€ 전에 λ°˜λ“œμ‹œ λ¨Όμ € ν˜ΈμΆœν•œλ‹€. "
"μ’…λͺ© λͺ©λ‘Β·λ§ˆκ°μΌΒ·λ ˆλ²„리지 μƒν•œΒ·μˆ˜μˆ˜λ£ŒΒ·μ±„μ  방식이 λ“€μ–΄ μžˆλ‹€."),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "get_data",
"description": (
"μ’…λͺ©μ˜ κ³Όκ±° μ‹œμ„Έλ₯Ό λ°›λŠ”λ‹€. 이 λ°μ΄ν„°λ‘œ 직접 λͺ¨λΈμ„ λ§Œλ“€μ–΄ μ˜ˆμΈ‘ν•˜λΌ. "
"미래 λ°μ΄ν„°λŠ” μ£Όμ§€ μ•ŠλŠ”λ‹€ β€” 정닡은 아직 세상에 μ—†λ‹€."),
"inputSchema": {
"type": "object",
"properties": {
"asset": {"type": "string",
"description": "μ’…λͺ© μ½”λ“œ. NVDA / BTC / GOLD / OIL"},
"bars": {"type": "integer", "default": 500,
"description": "받을 봉 개수 / number of bars (max 5000)"},
# πŸ”΄ μ‹œκ°„λ΄‰μ„ λͺ©λ‘μ— 두면 μ•ˆ λœλ‹€. 우리 μ‹œμ„Έ ν”Όλ“œλŠ” μΌλ΄‰λ§Œ λ°œν–‰ν•˜λŠ”λ°
# 1h λ₯Ό λ°›μ•„ 놓고 일봉을 돌렀주면 μ—μ΄μ „νŠΈλŠ” μ‹œκ°„λ΄‰μœΌλ‘œ λ―Ώκ³ 
# λͺ¨λΈμ„ μ„Έμš΄λ‹€. 쑰용히 ν‹€λ¦° 닡이 κ°€μž₯ λ‚˜μ˜λ‹€.
"interval": {"type": "string", "enum": ["1d", "1h"], "default": "1d",
"description": "일봉 λ˜λŠ” μ‹œκ°„λ΄‰. 채점은 μ‹œκ°„λ΄‰ κ²©μžμ—μ„œ "
"μ΄λ€„μ§‘λ‹ˆλ‹€ / daily or hourly; scoring runs "
"on the hourly grid"},
},
"required": ["asset"],
},
},
{
"name": "submit_position",
"description": (
"ν¬μ§€μ…˜μ„ μ œμΆœν•œλ‹€. -1.0(μ „λŸ‰ 숏) ~ +1.0(μ „λŸ‰ λ‘±). λ ˆλ²„λ¦¬μ§€λŠ” 1둜 고정이며 "
"λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λ©΄ μž˜λ¦°λ‹€. λ‹€μ‹œ μ œμΆœν•  λ•ŒκΉŒμ§€ 이 ν¬μ§€μ…˜μ΄ μœ μ§€λœλ‹€. "
"ν¬μ§€μ…˜μ„ λ°”κΏ€ λ•Œλ§ˆλ‹€ μˆ˜μˆ˜λ£Œκ°€ λ‚˜κ°€λ―€λ‘œ 자주 λ’€μ§‘μœΌλ©΄ 손해닀."),
"inputSchema": {
"type": "object",
"properties": {
"asset": {"type": "string"},
"position": {"type": "number", "minimum": -1, "maximum": 1},
"note": {"type": "string", "description": "선택. νŒλ‹¨ κ·Όκ±° λ©”λͺ¨"},
},
"required": ["asset", "position"],
},
},
{
"name": "check_score",
"description": (
"λ‚΄ 성적과 μˆœμœ„λ₯Ό ν™•μΈν•œλ‹€. 수읡λ₯ Β·μˆœμœ„Β·μš΄μ˜ ν•œκ³„μ„  λŒ€λΉ„ μœ„μΉ˜λ₯Ό λŒλ €μ€€λ‹€."),
"inputSchema": {
"type": "object",
"properties": {"asset": {"type": "string",
"description": "μƒλž΅ν•˜λ©΄ μ°Έκ°€ 쀑인 μ „ μ’…λͺ©"}},
},
},
]
def _ok(rid, result):
return {"jsonrpc": "2.0", "id": rid, "result": result}
def _err(rid, code, msg):
return {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": msg}}
def _text(obj):
"""도ꡬ κ²°κ³ΌλŠ” ν…μŠ€νŠΈ μ½˜ν…μΈ λ‘œ λŒλ €μ€€λ‹€. μ—μ΄μ „νŠΈκ°€ JSON을 잘 μ½μœΌλ―€λ‘œ
ꡬ쑰λ₯Ό κ·ΈλŒ€λ‘œ μ§λ ¬ν™”ν•˜λ˜, μ‚¬λžŒμ΄ 둜그λ₯Ό λ³Ό λ•Œλ„ 읽히게 λ“€μ—¬μ“΄λ‹€."""
return {"content": [{"type": "text",
"text": json.dumps(obj, ensure_ascii=False, indent=1)}]}
def tools(lang="ko"):
"""도ꡬ λͺ©λ‘. μ„€λͺ…이 μ–Έμ–΄λ₯Ό 탄닀 β€” μ—μ΄μ „νŠΈλŠ” 이 μ„€λͺ…을 읽고 νŒλ‹¨ν•œλ‹€."""
out = []
for t in TOOLS:
d = dict(t)
d["description"] = I.t(I.TOOLDESC.get(t["name"], t["description"]), lang)
out.append(d)
return out
def handle(body: Dict[str, Any], call, lang: str = "ko") -> Any:
"""call(name, args) -> dict λ₯Ό λ°›μ•„ 도ꡬλ₯Ό μ‹€μ œλ‘œ μ‹€ν–‰ν•œλ‹€.
λ°˜ν™˜μ΄ None 이면 응닡 μ—†μŒ(notification)μ΄λΌλŠ” λœ»μ΄λ‹€.
"""
method = body.get("method")
rid = body.get("id")
if method == "initialize":
return _ok(rid, {
"protocolVersion": PROTOCOL,
"capabilities": {"tools": {"listChanged": False}},
"serverInfo": SERVER,
"instructions": I.t(I.MCP_INSTRUCTIONS, lang),
})
if method and method.startswith("notifications/"):
return None # μ•Œλ¦Όμ€ μ‘λ‹΅ν•˜μ§€ μ•ŠλŠ”λ‹€
if method == "tools/list":
return _ok(rid, {"tools": tools(lang)})
if method == "tools/call":
p = body.get("params") or {}
name = p.get("name")
args = p.get("arguments") or {}
if not any(t["name"] == name for t in TOOLS):
return _err(rid, -32602, "μ•Œ 수 μ—†λŠ” 도ꡬ: %s" % name)
try:
return _ok(rid, _text(call(name, args)))
except PermissionError as e:
return _ok(rid, {"content": [{"type": "text", "text": str(e)}],
"isError": True})
except Exception as e:
return _ok(rid, {"content": [
{"type": "text", "text": "%s: %s" % (type(e).__name__, e)}],
"isError": True})
if method == "ping":
return _ok(rid, {})
return _err(rid, -32601, "μ§€μ›ν•˜μ§€ μ•ŠλŠ” λ©”μ„œλ“œ: %s" % method)